diff --git a/CMakeLists.txt b/CMakeLists.txt index da3abd8..7d766ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,13 @@ set(CMAKE_AUTOUIC ON) if(MSVC) add_compile_options(/utf-8 /MP /W4 /permissive-) + # 生成 PDB——即使 Release 优化构建也产出调试符号,使 minidump / 运行期崩溃栈可符号化分析 + # (生产桌面端排障必需)。/Zi 编译期调试信息;/DEBUG 链接产 PDB;/OPT:REF,ICF 抵消 /DEBUG + # 默认关掉的优化,保持二进制优化+精简。仅非 Debug 配置启用。 + add_compile_options($<$>:/Zi>) + add_link_options($<$>:/DEBUG> + $<$>:/OPT:REF> + $<$>:/OPT:ICF>) endif() # ===================================================================== @@ -63,6 +70,12 @@ FetchContent_Declare(qtkeychain GIT_TAG v0.14.0) FetchContent_MakeAvailable(qtkeychain) +# Qwt 6.2(二维科学图表:数据集详情散点/等值线图)。见 cmake/qwt.cmake 说明。 +# 源码 external/qwt-src 已 gitignore,需按 docs/ENV_SETUP 拉取。 +if(EXISTS "${CMAKE_SOURCE_DIR}/external/qwt-src/src") + include("${CMAKE_SOURCE_DIR}/cmake/qwt.cmake") +endif() + add_subdirectory(src) enable_testing() diff --git a/cmake/qwt.cmake b/cmake/qwt.cmake new file mode 100644 index 0000000..870d033 --- /dev/null +++ b/cmake/qwt.cmake @@ -0,0 +1,32 @@ +# 定义 qwt 静态库目标(Qwt 6.2 二维科学图表:数据集详情散点/等值线图)。 +# +# 为何不用 Qwt 原生 qmake:本机 VS2026 预览版缺 vswhere,qmake 探测 cl 失败。 +# 为何不用 vcpkg 的 qwt:其端口依赖 vcpkg-qtbase,会与官方 Qt 形成双 Qt 冲突(项目铁律: +# 依赖 Qt 的组件不走 vcpkg)。故此处用 CMake 直接编 Qwt 源码,对接官方 Qt6(同 VTK 思路)。 +# +# 源码目录 external/qwt-src(已 gitignore,按 docs/ENV_SETUP 拉取): +# git clone --depth 1 https://github.com/opencor/qwt external/qwt-src # Qwt 6.2.0 镜像 +# 由根 CMakeLists 在源码存在时 include 本文件。 + +set(QWT_SRC_DIR "${CMAKE_SOURCE_DIR}/external/qwt-src/src") + +find_package(Qt6 REQUIRED COMPONENTS Widgets Concurrent PrintSupport Svg) + +file(GLOB _qwt_sources CONFIGURE_DEPENDS "${QWT_SRC_DIR}/*.cpp") +file(GLOB _qwt_headers CONFIGURE_DEPENDS "${QWT_SRC_DIR}/*.h") +# 排除:OpenGL 画布 / SVG plot item / QwtPolar 部件(避免额外模块依赖与面积)。 +# 保留 qwt_symbol.cpp(用 QSvgRenderer,故链 Qt6::Svg)与 qwt_point_polar.cpp(几何工具)。 +list(FILTER _qwt_sources EXCLUDE REGEX "qwt_plot_glcanvas|qwt_plot_opengl_canvas|qwt_plot_svgitem|qwt_polar_") +list(FILTER _qwt_headers EXCLUDE REGEX "qwt_plot_glcanvas|qwt_plot_opengl_canvas|qwt_plot_svgitem|qwt_polar_") + +add_library(qwt STATIC ${_qwt_sources} ${_qwt_headers}) +set_target_properties(qwt PROPERTIES AUTOMOC ON) +target_include_directories(qwt PUBLIC "${QWT_SRC_DIR}") +target_link_libraries(qwt PUBLIC Qt6::Widgets Qt6::Concurrent Qt6::PrintSupport Qt6::Svg) +target_compile_features(qwt PUBLIC cxx_std_17) +# QWT_MOC_INCLUDE=1:启用 Qwt 源文件末尾 #if QWT_MOC_INCLUDE 保护的 #include "moc_xxx.cpp"。 +# Qwt 原生用 qmake(qwtbuild.pri 里设置),CMake 构建需显式定义,否则 MOC 元对象代码不被编译进 .obj。 +target_compile_definitions(qwt PRIVATE QWT_MOC_INCLUDE=1) +if(MSVC) + target_compile_options(qwt PRIVATE /bigobj /EHsc /wd4244 /wd4267 /wd4305 /wd4456) +endif() diff --git a/docs/Geopro3.0_二维图表返工技术方案.md b/docs/Geopro3.0_二维图表返工技术方案.md new file mode 100644 index 0000000..380596f --- /dev/null +++ b/docs/Geopro3.0_二维图表返工技术方案.md @@ -0,0 +1,234 @@ +# Geopro 3.0 桌面客户端 — 数据集详情「二维图表」返工技术方案 + +**版本 v1.0** · 适用范围:数据集详情页的「原数据(散点图)」与「网格数据(填充等值线图)」 +**状态**:返工指令(推翻当前 QGraphicsView 实现,重做) + +--- + +## 0. 给开发(含 AI 编码)的硬约束 — 先读这一段 + +1. **不嵌入 QWebEngine,不引入 Chromium。** 整个客户端的技术路线是 Qt C++ 原生,核心动机包含离线/现场作业能力;把核心数据可视化做成依赖 web 渲染会破坏该前提,禁止。 +2. **推翻当前的裸 `QGraphicsView` + VTK 渲染窗口方案。** 当前 9 个问题的根因是 2D 框架选错,不是"没嵌 web"。 +3. **改用 `QwtPlot` 作为两类图的坐标系框架;等值线多边形用 VTK 算法(仅算法,不开 VTK 渲染窗口)。** +4. **验收标准 = 视觉等价 + 交互一致**,不是字节级像素 diff。原生渲染与 web 端 Plotly 在抗锯齿、字体、亚像素上天生有别,任何工具都无法字节级相同;目标是"用户看不出降级感"。 +5. **不要自由发挥架构。** 本文档把分层、类、职责定死,照做;遇到本文档未覆盖的细节,先问,不要自行另起一套。 + +--- + +## 1. 根因诊断(为什么会做崩) + +当前实现把"图形 + 坐标轴 + 色阶图例"全部糊在一个 `QGraphicsView` 场景里一起做变换,导致系统性错误。裸 `QGraphicsView` 是通用图形框架,**不具备"科学图表"语义**——它不知道什么是固定坐标轴、什么是"仅数据区缩放"、什么是"图例不参与变换"。 + +把用户列的 9 个问题按根因归类: + +| # | 问题 | 根因类别 | 由谁解决 | +|---|---|---|---| +| 1 | 色阶图随大图缩放、与横坐标重叠 | 框架(图例未独立) | QwtPlot 分层 | +| 3 | 拖动时坐标轴跟着动(应只动数据、轴值跟随) | 框架(无数据区/轴分离) | QwtPlot 内置交互 | +| 4 | 一拖动画面就花 | 框架(手写变换 + 重绘错误) | QwtPlot 内置交互 | +| 9 | 图比原版瘦(纵横比错) | 框架(无纵横比控制) | QwtPlot 纵横比锁定 | +| 2 | 配色与原版不一致(离散色带 vs 连续插值) | 实现细节(色阶映射) | QwtLinearColorMap | +| 5 | 等值线数字没显示 | 实现细节(缺标注) | 等值线 label | +| 6 | 原数据/网格数据切换样式被改错 | UI 布局(与图表无关) | 改回页签样式 | +| 7 | 显示异常/等值线开关跑到面板外 | UI 布局(与图表无关) | 归位工具条 | +| 8 | 下方缺「描述」页签 | UI 布局(与图表无关) | 补页签 | + +**关键结论**:9 个问题没有一个需要"嵌 web"才能解决。第 1 类(#1/#3/#4/#9)换对框架即消失;第 2 类(#2/#5)是渲染细节;第 3 类(#6/#7/#8)是纯 UI,跟用什么画图无关。 + +--- + +## 2. 目标架构 — 三层分离 + +整个二维图表 widget 必须做成**三层分离**,这是治本: + +``` +┌─────────────────────────────────────────────┐ +│ ChartPanel (QWidget) │ +│ ┌───────────────────────────────────────┐ │ +│ │ 工具条 ToolBar(网格/色阶配置/白化/...) │ │ ← 工具条层(属于面板,不属于绘图区) +│ ├───────────────────────────────────────┤ │ +│ │ QwtPlot(数据区 + 固定坐标轴) │ │ ← 绘图层(QwtPlot 管坐标轴与交互) +│ │ • QwtPlotItem: 等值线多边形 / 散点 │ │ +│ │ • QwtPlotMagnifier / Panner(交互) │ │ +│ ├───────────────────────────────────────┤ │ +│ │ ColorBarWidget(独立固定) │ │ ← 图例层(独立 widget,永不参与数据区变换) +│ └───────────────────────────────────────┘ │ +└─────────────────────────────────────────────┘ +``` + +**铁律**: +- **坐标轴**由 QwtPlot 管理,**永远固定**(不随拖动移动其位置),拖动/缩放时只有**数据区内容**平移缩放,**轴上的刻度数值自动跟随重算**。这正是 QwtPlot 的默认行为,也正是原版的行为。 +- **色阶条(ColorBar)是独立 widget**,放在绘图区下方,**绝不放进 QwtPlot 的数据坐标系**,因此永远不会随图缩放、不会和横坐标重叠(直接消灭 #1)。 +- **工具条属于面板**,不属于绘图区(修正 #7)。 + +--- + +## 3. 框架与依赖 + +| 用途 | 选用 | 协议 | 说明 | +|---|---|---|---| +| 2D 图表坐标系框架 | **QwtPlot(Qwt 6.2+)** | QWT License(LGPL + 例外,商用免费) | 坐标轴、缩放、平移、刻度跟随、图例、色阶映射全内置 | +| 等值线多边形算法 | **vtkBandedPolyDataContourFilter** | BSD(商用免费) | 仅当算法用,**不开 VTK 渲染窗口** | +| 等值线(线)算法 | vtkContourFilter / vtkFlyingEdges2D | BSD | 生成等值线及其标注位置 | +| 连续色阶映射 | **QwtLinearColorMap** | QWT License | 对齐原版 Plotly 的连续插值色阶 | + +> 协议全部商用友好,零授权费。QWT 与 VTK 都不触发开源传染。 + +> **VTK 的双重角色**(务必理解,否则又会做重):在 3D 视图 / 地图视图里 VTK 是**渲染器**(开渲染窗口);在本页这种纯二维详情图里,VTK **退化为算法库**(只用它的 filter 算多边形,取出顶点交给 QwtPlot 画)。本页**不允许**出现 QVTKOpenGLWidget / 渲染窗口。 + +--- + +## 4. 逐项解法(对应 9 个问题) + +### 4.1 色阶条独立固定(治 #1) + +- 新建 `ColorBarWidget : public QWidget`,自绘一条水平色带 + 刻度数值 + 单位。 +- 它在布局上位于 QwtPlot **下方**,是**兄弟 widget**,不是 QwtPlot 的内部 item。 +- 它的色阶**与绘图区用同一个色阶对象**(见 §6 色阶服务),保证图与图例配色一致。 +- 因为它不在数据坐标系内,**永远不随拖动/缩放移动,不会与横坐标轴重叠**。 +- 刻度数值与原版对齐:用与数据相同的分级断点(原版色阶断点形如 17.105 / 25.937 / ...,等比/对数分布,见 §6)。 + +### 4.2 仅数据区缩放、坐标轴固定、轴值跟随(治 #3) + +- 用 QwtPlot,坐标轴(`QwtPlot::xBottom` / `yLeft`)由 QwtPlot 框架绘制,位置固定。 +- 缩放/平移用 QwtPlot 内置: + - `QwtPlotMagnifier`(滚轮缩放数据区) + - `QwtPlotPanner`(拖动平移数据区) + - 需要框选缩放可加 `QwtPlotZoomer` +- 这些组件只改变**坐标轴的数值范围(scaleDiv)**,QwtPlot 自动重算刻度并重绘——**轴不动、轴上的数字跟着变**,与原版完全一致。 +- **原数据 = 四象限坐标系**(数据含正负,x/y 轴交叉于原点区域);**网格数据 = 单象限**(数据全正,原点在左下)。在 QwtPlot 里就是设置不同的轴范围与原点位置,不需要两套框架。 + +### 4.3 拖动不再花屏(治 #4) + +- 当前"花屏"是手写 `QGraphicsView` 变换 + 重绘时机错误导致。改用 QwtPlot 后,重绘由 QwtPlot 的 replot 机制统一管理,不会出现脏区残留。 +- 大数据量等值线多边形若导致拖动卡顿:对 `QwtPlotItem` 开启 `QwtPlotItem::RenderAntialiased` 视情况关闭、并启用绘制缓存;必要时对多边形做 LOD 简化(与原版"简化容差"滑块一致,见 §7)。 + +### 4.4 纵横比 / 宽度修正(治 #9) + +- 原版图是"宽扁"的剖面图(x 范围远大于 y)。当前实现"偏瘦"是因为没有锁定数据纵横比、或 widget 尺寸策略错误。 +- 解法:根据数据的 x/y 跨度设定数据区的纵横比;若要求等比例(1 个数据单位 x = 1 个数据单位 y 的像素),自定义布局在 replot 时按数据范围调整 canvas 的宽高比,或固定 y 轴范围、x 轴自适应填充。 +- 先对齐原版的"宽扁"观感即可,不必强制物理等比,除非业务要求等比例尺。 + +### 4.5 连续插值色阶,对齐 Plotly 配色(治 #2) + +- 原版散点/等值线用的是**连续插值色阶**(Plotly 的 colorscale),当前实现用了**离散色带**,所以配色不同。 +- 用 `QwtLinearColorMap`,设置与原版相同的**控制点颜色 + 位置**,开启连续插值(`QwtLinearColorMap` 默认 `ScaledColors` 连续模式,不要用 `FixedColors`)。 +- 控制点取自原版色阶(紫蓝→蓝→青→绿→黄→橙→红→深红的彩虹序列)。**必须从原版抓取实际色值**(见 §6 第 4 步),不要凭记忆配色。 +- 色阶分布是**对数/等比**的(断点 17.105, 25.937, 34.382... 比值递增),映射时对数据取 log 再线性映射,否则颜色分布会与原版不同。 + +### 4.6 等值线数值标注(治 #5) + +- 等值线(contour line)由 `vtkContourFilter` 在各分级阈值生成线,取出线的折点。 +- 在 QwtPlot 中用自定义 `QwtPlotItem` 绘制这些线,并在线的合适位置绘制数值标签(label):沿线取中点或曲率较小处,绘制旋转贴合线方向的文本(原版标签是贴着等值线方向旋转的)。 +- 受工具条「显示等值线标注」复选框控制开关。 +- 「显示等值线提示信息」控制 hover 时的 tooltip(鼠标移到等值线上显示数值),与原版一致。 + +### 4.7 页签切换样式归位(治 #6) + +- 「原数据 / 网格数据」切换**不是两个按钮**,而是与「数据集面板的 数据/文件」「右栏的 异常/对象属性」**同一种下划线页签样式**(之前已专门调过样式,复用那套 QSS/组件,不要重写成 QPushButton)。 +- 直接复用既有的下划线 Tab 组件类,保证全客户端切换样式统一。 + +### 4.8 工具条归位到网格数据面板内(治 #7) + +- 「显示异常 / 显示等值线标注 / 显示等值线提示信息 / 简化容差滑块 / 网格 / 色阶配置 / 白化 / 滤波处理 / 异常标注 / 自动标注 / 另存为」这些**属于网格数据视图的工具条**,必须在网格数据面板**内部**的工具条上,不能浮到外层。 +- 「原数据」页签的工具条是另一套(网格 / 色阶配置 / 当前图形下拉 / 另存为),各自归属各自页签。 + +### 4.9 下方补「描述」页签(治 #8) + +- 图下方区域是一个含两个页签的容器:**「异常列表」+「描述」**,当前只做了异常列表表格,缺「描述」。 +- 补上「描述」页签(富文本/纯文本说明区),与原版结构一致。 + +--- + +## 5. 组件清单与职责(定死,不要改结构) + +| 类 | 基类 | 职责 | +|---|---|---| +| `DatasetDetailPanel` | QWidget | 数据集详情页根容器:顶部下划线页签(原数据/网格数据)+ 内容区 + 下方(异常列表/描述)| +| `RawDataChartView` | QWidget | 「原数据」散点图:工具条 + QwtPlot(四象限)+ ColorBar | +| `GridDataChartView` | QWidget | 「网格数据」等值线图:工具条 + QwtPlot(单象限)+ ColorBar | +| `ContourPlotItem` | QwtPlotItem | 自定义:绘制 VTK 算出的分层填充多边形 + 等值线 + 标注 | +| `ScatterPlotItem` | QwtPlotItem / QwtPlotSpectroCurve | 散点,按值连续着色 | +| `ColorBarWidget` | QWidget | 独立色阶条(固定,不入数据坐标系)| +| `ColorMapService` | (单例/服务) | 全局色阶:断点、颜色、命名保存、对数映射;图与图例同源 | +| `ContourEngine` | (封装 VTK) | 输入网格数据 → 输出分层多边形 + 等值线折点(封装 vtkBandedPolyDataContourFilter / vtkContourFilter)| + +--- + +## 6. 色阶服务(对齐原版的关键) + +色阶是"配色一致"的命门,单独抽成服务,**图、图例、3D 视图共用同一份**(呼应视觉设计规范 §8): + +1. **断点(分级阈值)**:采用原版的对数/等比分布。原版断点示例:`17.105, 25.937, 34.382, 35.972, 42.555, 49.051, 56.312, 68.950, 83.029, 98.454, 110.570, 129.660, 158.270, 204.160, 260.820, 321.710, 1335.500`(不同数据集断点不同,应由数据的 min/max + 分级规则动态生成,规则与原版一致)。 +2. **颜色控制点**:彩虹序列(深蓝 → 蓝 → 亮蓝 → 青 → 浅绿 → 绿 → 黄绿 → 黄 → 土黄 → 橙 → 黄 → 红 → 深红 → 紫红 → 紫 → 深紫黑)。 +3. **映射方式**:连续插值(`QwtLinearColorMap` ScaledColors)+ 对数标度(数据先 log,再线性映射到 [0,1])。 +4. **必须从原版抓真实色值**:打开原版页面 `http://tenant.geomative.cn/#/projectSpace/datasetMange/datasetInfo?id=...&ddCode=dd_inversion_data`,用浏览器开发者工具检查 Plotly 的 `colorscale` 配置,或对色阶条截图取色(每个色块取中心像素 RGB)。**严禁凭记忆配色** —— 这是 #2 反复出错的根源。 +5. **命名保存**:色阶可命名、保存、切换,调整后图与图例实时刷新(业务规约要求)。 + +--- + +## 7. 工具条功能对照(两个页签各一套) + +**原数据页签工具条**:网格 | 色阶配置 | 当前图形(下拉:散点图/...)| 另存为 + +**网格数据页签工具条**:网格 | 色阶配置 | 白化 | 滤波处理 | ☑显示异常 | ☑显示等值线标注 | ☐显示等值线提示信息 | 简化容差(滑块,0–1)| 异常标注 | 自动标注 | 另存为 + +- 「简化容差」滑块:控制等值线多边形的简化程度(值越大,多边形越简化、越平滑),对应 VTK 侧对多边形做 `vtkDecimatePolylineFilter` 或道格拉斯-普克简化的容差参数。 +- 「白化」「滤波处理」是对网格数据的预处理操作,作用于 ContourEngine 的输入。 +- 「自动标注」弹出对话框(见 §8)。 + +--- + +## 8. 自动标注对话框(复用同一图表组件) + +- 「自动标注」弹出的对话框里那张图,**必须与主页面网格数据图完全一致**——做法是**复用同一个 `GridDataChartView` / `ContourPlotItem` 类的另一个实例**,传入同一份数据 + 同一个 ColorMapService,**不要另写一套渲染**(否则又会出现两张图不一致)。 +- 对话框结构:左侧「异常判定规则」(阈值模式 数值/百分位、最小点数、异常类型)+ 右侧统计信息(最大/最小/均值/中位数)+ 图(同主图)+ 自动标注结果列表 + 底部(取消/执行自动标注/确认保存)。 +- 因为是普通 QWidget(QwtPlot 而非 VTK 窗口),嵌进对话框无任何 OpenGL 上下文问题,开关对话框不会闪烁/泄漏。 + +--- + +## 9. 实施顺序(建议分步验收,避免再次大返工) + +> 每一步做完就和原版对照截图验收,通过再进入下一步。不要一次性全做完再看。 + +1. **搭三层骨架**:DatasetDetailPanel + 两个页签(下划线样式)+ 下方(异常列表/描述)。先不画图,验证 UI 结构对齐(治 #6/#7/#8)。 +2. **QwtPlot 接入 + 交互**:空 QwtPlot,配好固定坐标轴 + Magnifier/Panner,验证"拖动只动数据、轴值跟随、不花屏、纵横比对"(治 #3/#4/#9)。 +3. **ColorBar 独立**:加 ColorBarWidget 固定在下方,验证缩放时色阶条不动、不与轴重叠(治 #1)。 +4. **色阶服务 + 抓原版色值**:实现 ColorMapService,从原版抓真实色值与断点,连续对数映射(治 #2 的基础)。 +5. **散点图**:原数据页用 ScatterPlotItem 连续着色,对照原版配色(治 #2)。 +6. **等值线图**:ContourEngine(VTK 算多边形)+ ContourPlotItem 绘制填充 + 等值线 + 标注(治 #2/#5)。 +7. **工具条联动**:显示异常/等值线/提示、简化容差滑块、白化、滤波接通。 +8. **自动标注对话框**:复用图表组件 + 规则面板 + 统计 + 结果。 +9. **整体对照验收**:原数据、网格数据分别与原版并排截图,核对配色/布局/交互/标注/纵横比。 + +--- + +## 10. 验收标准(写清楚,避免预期错位) + +**通过标准 = 视觉等价 + 交互一致**: + +- [ ] 配色与原版视觉一致(同一色阶、连续插值、对数分布) +- [ ] 坐标轴固定,拖动/缩放只动数据区,轴刻度值跟随 +- [ ] 色阶条固定在轴下方,不随图缩放,不与坐标轴重叠 +- [ ] 等值线数值标注显示,贴合线方向 +- [ ] 原数据=四象限、网格数据=单象限,缩放/移动行为分别与原版一致 +- [ ] 纵横比/宽扁观感与原版一致 +- [ ] 拖动流畅、无花屏 +- [ ] 页签切换为下划线样式(与数据/文件、异常/属性一致) +- [ ] 工具条开关位于各自页签面板内 +- [ ] 下方含「异常列表 + 描述」两个页签 +- [ ] 自动标注对话框内图与主图完全一致 + +**不作为通过标准**(明确排除,避免无意义返工): + +- ✗ 与 web 端逐像素 diff 相同(抗锯齿/字体/亚像素天生有别,任何原生方案都无法做到,非缺陷)。 + +--- + +## 11. 一句话方向 + +> 不嵌 Chromium。推翻 QGraphicsView,用 **QwtPlot(坐标系/交互/图例)+ VTK 算法(等值线多边形)+ 连续对数色阶(对齐原版)**,三层分离(绘图区 / 固定坐标轴 / 独立色阶条),UI 三处(页签样式/工具条归位/描述页)按原版恢复。验收按"视觉等价 + 交互一致",非字节级。 + +--- + +*本方案针对当前返工,钉死架构与分层,禁止在图表渲染框架上再自由发挥。未覆盖的细节先确认再实现。* diff --git a/docs/superpowers/HANDOFF-dataset-detail-chart.md b/docs/superpowers/HANDOFF-dataset-detail-chart.md new file mode 100644 index 0000000..f5ab629 --- /dev/null +++ b/docs/superpowers/HANDOFF-dataset-detail-chart.md @@ -0,0 +1,225 @@ +# 交接文档:数据集详情图表 + 全 App 网络层异步化 + +> 给下一个会话:读完本文件即可无缝接手。最后更新 2026-06-12。 +> 分支 **`feat/dataset-detail-chart`**,领先 main 68 commits。**测试 122/122 全绿**。 +> ⚠️ **工作区脏**:sessions 0.1–0.5 的代码(散点 hover/日志崩溃捕获/colormap/数据集树/按根分页/暗色主题)**全部未提交**(用户多次选「保持现状」不提交不合并)。`git status` 一大堆 M/??,**别假设干净树**;接手前先 `git status` 看清。新增未跟踪文件:`src/app/Logging.*`、`src/app/panels/chart/{ScatterHoverTip,ChartTheme}.*`、`tests/app/test_scatter_hover.cpp`,以及若干 `*.jpeg/*.yml` 临时抓图/抓取产物(非源码,可忽略/清理)。 + +--- + +## 0. 一句话现状 + +geopro(Qt6/C++ 离线桌面客户端,1:1 复刻赛盈地空 web)已完成两大块: +1. **数据集详情图表**(仅 ERT 反演 `dd_inversion_data`,QwtPlot 落地,用户已验收)。 +2. **全 App 网络层 100% 异步化**(详情/导航/登录/项目列表全异步,同步 `QEventLoop` 阻塞路径已彻底删除,无技术债)。 + +均**未合并入 main**,分支挂起等收尾。 + +### 0.1 2026-06-12 渲染保真修复(本次会话) + +用户报客户端散点/网格颜色与原版差异大(大量点透明发白、图例只有 6 色 + 白缝)、散点缺 hover。经 Playwright 抓原版真实 API + 读源码定位为**通用根因**: + +- **colorBar alpha 标度 bug**(核心):`lvl/colorGradation/getDetail` 返回的 colorBar 是**混合格式**——hex `#00008B` 与 CSS `rgba(0, 0, 170, 1)`(**alpha 是 0–1 浮点**),共 18 段。`DatasetChartDto.cpp` 用 `AlphaScale::Bit255` 解析 → rgba 的 `a=1` 被当字节 → alpha≈1/255 近透明(12 个 rgba 段全成白缝;6 个 hex 段因 hex 分支强制 255 才可见)。**修复:`Bit255`→`Unit`**(一行)。散点连续插值 `ColorMapService` 的归一化位置 `val/maxVal` 已证**精确等于原版 Plotly colorscale 位置**,无需改。 +- **散点 hover**:新增 `ScatterHoverTip`(canvas 事件过滤器,与 LivePanner 共存),最近点命中显示 `X/Y/值`(各 3 位小数,对齐原版 Plotly hovertemplate `X: %{x:.3f}…`)。 +- 文件:改 `src/data/dto/DatasetChartDto.cpp`;新增 `src/app/panels/chart/ScatterHoverTip.{hpp,cpp}` + 接线 `RawDataChartView.{hpp,cpp}`;测试 `tests/data/test_dataset_chart_dto.cpp`(改用真实混合格式 + 断言 alpha=255 回归)、新增 `tests/app/test_scatter_hover.cpp`。**测试 116→118 全绿**,cpp-reviewer APPROVE。 +- ⚠️ **像素级视觉 1:1 待用户在运行的 app 内登录核对**(原生 Qt 窗口无法用 Playwright 驱动;以下各层已机器验证:抓包/源码/RED-GREEN/插值位置匹配)。 +- 观察(未改,待定):图例 `ColorBarWidget` 画 `stops-1=17` 段,丢弃最后一段最深色;原版疑为 18 段。非本次报障,留待用户决定是否补齐。 + +### 0.2 2026-06-12 第二轮修复(散点 cauto 归一化 + hover mouseTracking + 页签名) + +第一轮 alpha 修复后颜色不再透明,但散点仍与原版不一致(挤在暖色中段、无蓝无紫)。复查 Plotly `_fullData`:散点 **`cmin/cmax` 未设 → `cauto` 自动取 vlist 实际 min/max(本例 45.93–197.79)**,把整段色阶铺满数据范围;而客户端 `ColorMapService` 错用 colorBar 全程 0–1323 归一化数据 → 压进色阶 0.03–0.15 段。 + +- **修复 A(散点 cauto)**:`ColorMapService.setDataRange(dataMin,dataMax)` 解耦「色阶形状位置(按断点值)」与「数据值归一化(按数据 min/max)」;`RawDataChartView::setData` 按 `d.scatter.v` 有限值 min/max 调用。**数值验证 1:1**:手算修复后客户端对样本值的 RGB 与原版 Plotly 实测逐字节相同(如中位 92.01→[168,0,35]、62.34→[255,119,0])。网格仍用绝对阈值(`colorAtDiscrete`),不变。 +- **修复 B(hover 仍无效)**:根因 `QwtPlotCanvas` 默认不开 mouseTracking → 无按键时收不到 MouseMove。`ScatterHoverTip` 构造里加 `canvas->setMouseTracking(true)`。 +- **修复 C(页签用数据名)**:`ChartData` 加 `dsName`;`openDataset` 加可选第 3 参 `dsName`(默认空,向后兼容测试);`kDsNameRole` 存 dsName,`main.cpp` 双击传入,`DatasetDetailPanel` `addTab` 用 `dsName`(空回退 dsId)+ tooltip。 +- 文件:`ColorMapService.{hpp,cpp}`、`RawDataChartView.cpp`、`ScatterHoverTip.cpp`、`DatasetDetailController.{hpp,cpp}`、`DatasetListPanel.{hpp,cpp}`、`DatasetDetailPanel.cpp`、`main.cpp`;测试 `test_colormap_service.cpp`(+DataRangeDecouples)。**119/119 全绿**。 +- ⚠️ hover/页签名/视觉仍待用户运行核对(GUI 无法自动驱动)。 + +### 0.3 2026-06-12 桌面端日志 + 崩溃捕获(新基础设施) + +用户反馈:双击 ds 后状态栏报「内部系统错误」然后客户端崩溃;后端错误可接受,但客户端不该崩。复测发现 scatter/color 接口当时其实 200/干净数据 → 无日志根本无从定位。遂加生产级日志 + 崩溃捕获(已实测打通)。 + +- **`src/app/Logging.{hpp,cpp}`**:`initLogging()`(main.cpp 在 setApplicationName 后调用)。 + - `qInstallMessageHandler` 接管全 App `qDebug/qInfo/qWarning/qCritical/qFatal` → 写带时间戳/级别的滚动日志:`%LOCALAPPDATA%/Geomative/Geopro3/logs/geopro_YYYYMMDD.log`(按天,UTF-8+BOM,启动清理 14 天前旧文件)。 + - **崩溃捕获**:`SetUnhandledExceptionFilter`(SEH,含未捕获 C++ 异常 0xE06D7363)+ `std::set_terminate`;崩溃时 `MiniDumpWriteDump` 写 `crash_*.dmp`(链 `Dbghelp`)+ 记一行 `[FATAL] 崩溃 code=… addr=… dump=…`。`SetErrorMode` 抑制系统弹窗。**已用临时 env 自检实测**:空指针崩溃 → 生成 6MB dmp + 日志记录 code=0xc0000005(自检块已删)。 + - dmp 可 VS/WinDbg 加载看完整调用栈。 +- **埋点**:`ApiCall::onFinished` 记录每个 API 响应(URL/http/code/err,成功 INFO/失败 WARN);`DatasetDetailController` openDataset + 各 loadFailed;App 启动版本/路径。net 层 `qWarning` 自动收录。 +- **顺带堵崩溃根因**(评审 H-2 + 防脏数据):`ColorMapService::colorAtContinuous` 对 NaN/Inf 的 t 回退首断点色(原会 `upper_bound` 返回 end() 后解引用越界崩溃);`RawDataChartView` vlist min/max 用 `isfinite` 跳过 NaN/±Inf。+ 单测 `ColorAtContinuousNaNSafe`。 +- **测试 119→120 全绿**。 +### 0.4 2026-06-12 崩溃定位 + 顶层异常护栏 + +用户提供 dump + log。日志序列:`openDataset(ERT2-WS_result.dat)` → **`dynamicForm` 后端返回 `code=500 sys.internalServerError`**(即"内部系统错误")→ scatter 200 → color 200 → `Qt has caught an exception thrown from an event handler` → 崩溃。 + +**dump 分析(winget 装 Microsoft.WinDbg,`!analyze -v`)**:异常 `e06d7363`(C++ EH),栈 `VCRUNTIME140!CxxThrowException ← msvcp140!std::_Xlength_error ← geopro_desktop+0x9ad0 ← +0xa94ee ← +0x24b71 ← +0x235c0`。即 **`std::length_error`**(STL 容器/字符串用非法 size resize/reserve/构造)。栈帧都在 geopro_desktop(含静态链接的 Qwt),但**该 dump 是旧构建、当前 PDB 已不匹配 → 符号化不出函数名**。静态排查 dynamicForm 解析 / DynamicFormView / selectDataset 失败路径 / 散点解析(已 try/catch) / 渲染 / ColorMapService 均未见明显抛点 → 确切行**待运行期护栏日志点名**。 + +**修复(主,根治"不该崩")**:`main.cpp` 加 `GuardedApplication : QApplication`,override `notify()` try/catch:任何 slot/事件处理器抛出的异常被拦截 + `qCritical` 记录 `[guard] 拦截... what | type | receiver=<对象类名> | event=<事件类型>`,然后吞掉(不终止)。后端故障等异常**不再使整个客户端退出**。 +**附带**:修 `appendCrashLine` 文件共享 bug(崩溃行原写不进日志——g_logFile 独占,第二句柄 open 失败;改为复用已打开句柄);`colorAtContinuous` NaN/Inf 守卫。 + +**护栏实测**:用户复现,护栏拦到 → 日志 `[guard] 拦截未捕获异常: vector too long | type=std::length_error | receiver=QNetworkReplyHttpImpl | event=43(MetaCall)`。即异常在**网络响应 finished 同步链**里(最后一个 chart 请求 color 返回 → `chartReady→openOrUpdate→渲染`)抛出 `std::length_error`("vector too long" = std::vector 用了非法尺寸,典型负数转 size_t)。进程**未崩**(护栏吞掉),但图表没渲染出来。 + +**诊断基础设施(关键补强)**: +1. **Release 构建之前不生成 PDB** → dump/崩溃栈对自家代码全部符号化失败(只有系统 DLL 导出表能解析)。已在根 `CMakeLists.txt` MSVC 块加 `/Zi`(编译) + `/DEBUG /OPT:REF /OPT:ICF`(链接):Release 保持优化的同时产出匹配 PDB。**生产桌面端排障必需。** +2. **VEH 抛点堆栈符号化**(`Logging.cpp`):`AddVectoredExceptionHandler` 在 C++ 异常(0xE06D7363)**抛出瞬间**(栈未展开、PDB 匹配)用 DbgHelp(`SymInitialize`+`SymLoadModuleExW`+`SymFromAddrW`+`SymGetLineFromAddrW64`,**宽字符**——UNICODE 下必须)打印 `模块+RVA 函数名+偏移 (文件:行)`。自测验证:`reserve(-1)` → 正确打印 `geopro::app::initLogging (Logging.cpp:245)`。即使异常被护栏吞掉也留下抛点栈。 + +**下一步定位 length_error 确切行**:用当前构建复现一次 → 日志 `[THROW]` 段直接给出 `geopro::app::<类>::<方法> (文件:行)`。**测试 120/120 全绿。** + +### 0.5 2026-06-12 数据集列表树化 + 按根分页 + 暗色主题保真(本次会话) + +用户三组报障,均已修复、构建链接通过、应用真实流程跑通无崩溃、**测试 120→122 全绿**: + +**(1) 数据集列表应是树(原为平铺)** +- **实地确认(铁律 Playwright)**:原版「数据管理」选 TM 后的数据列表是 **el-table tree-data**(有展开箭头)。脚本直连 API 验证 TM E3(项目 1458977804960256 垃圾掩埋場):10 个 DS → **4 个根**(默认折叠)= 3×源「ERT原始数据」+ 1×「ERT电极坐标」;派生「反演/接地电阻」按 `parentId`(==`sourceShowParentId`)挂源「原始数据」下。根 = parentId 为空或指向不在本批的「源文件节点」。 +- **改**:`DsRow` 加 `parentId`;`NavDto::parseDsRows` 解析 `sourceShowParentId`(回退 `parentId`);`DatasetListPanel::populateDatasetList` 由 `QListWidget` 改建 **`QTreeWidget`**(两遍法按 parentId 嵌套,卡片委托 `applyDatasetCardDelegate` 泛化到 `QAbstractItemView`);`main.cpp` `datasetList` 改 `QTreeWidget`(`setExpandsOnDoubleClick(false)`:双击=开详情、展开靠箭头),点击/双击/反向高亮/加载更多 4 处改 `QTreeWidget` API。文件页签仍平铺 `QListWidget`。默认折叠(对齐原版)。 +- 测试:`test_nav_dto.cpp::ParseDsRowsParentIdForTree`。 + +**(2) 分页应按「第一层节点(根)」算(原按扁平 DS)** +- **根因**:后端 `dsObject/data/page` 按**扁平 DS** 分页(脚本验证:total=10、pageSize=5 返回 5 条扁平行)——子节点的父常落在下一页 → 按页建树出孤儿根、首层数错乱。 +- **改**:`IAsyncProjectRepository::loadRowsAsync` 加 `int pageSize=5` 参数(接口/`ApiProjectRepository`/测试 stub 同步);`WorkbenchNavController::selectObject` 数据页改用大 pageSize(`kFetchAllPageSize=1000`)**一次取全**整棵,缓存 `allDataRows_`;新私有 `emitNextDataRootPage(bool append)` **客户端按根切页**(每页 `kDataRootPageSize=5` 个根 + 各自整棵子树,DFS 收集后按原序输出,`total`=根总数);`loadMoreData` 改同步切下一页(无请求);`dataRootsShown_` 游标,`resetSelectionState` 清空。删因此空置的 `moreDataReq_`。`main.cpp` `addTreeLoadMore` 计数改按顶层根。若 `listCount>` 读 spy 行参)。 + +**(3) 暗色主题图表样式未跟随** +- 原详情图 `QwtPlot`、`ColorBarWidget`、`LoadingOverlay` 全硬编码白底/浅色 → 暗色主题下刺眼白底/白蒙板。**原版 web 无暗色,故暗色为客户端自定**:**浅色分支保持原硬编码值=与原版 1:1 不动,仅暗色改 token**。 +- 新增 **`src/app/panels/chart/ChartTheme.{hpp,cpp}`** `applyChartPlotTheme(QwtPlot*)`:按 `isDarkTheme()` 设画布底色(`bg/panel`)/轴字(`text/secondary`)/网格(`border/default`)/零线(`border/strong`),遍历 itemList 重着色 grid/marker。`Raw/GridDataChartView` 删硬编码白块、ctor 末尾调用 + 连 `ThemeManager::changed` 热切换。 +- `ColorBarWidget::paintEvent` 底色/边框/刻度字按 `isDarkTheme()`(暗色 `bg/panel`/`border/strong`/`text/secondary`;色带格=数据色不变)+ ctor 连 changed→update。 +- `LoadingOverlay` 遮罩纱色由 `rgba(255,255,255,160)` 改按主题(暗色 `bg/app` 深纱)+ label 文字 `text/primary`,连 changed 热切换。 +- token 表见 `src/app/Theme.cpp`(`bg/panel` 白/`#161A20`、`text/secondary`、`border/*`)。 + +**新文件**:`src/app/panels/chart/ChartTheme.{hpp,cpp}`(已加 `src/app/CMakeLists.txt`)。 +**改动文件**:`RepoTypes.hpp`、`NavDto.cpp`、`IAsyncProjectRepository.hpp`、`ApiProjectRepository.{hpp,cpp}`、`WorkbenchNavController.{hpp,cpp}`、`DatasetListPanel.{hpp,cpp}`、`main.cpp`、`RawDataChartView.cpp`、`GridDataChartView.cpp`、`ColorBarWidget.cpp`、`LoadingOverlay.cpp`、`src/app/CMakeLists.txt`、`tests/{data/test_nav_dto,controller/test_workbench_nav_controller}.cpp`。 +**记忆新增**:`dataset-list-is-tree`(树结构 + 扁平分页坑 + 按根分页解法 + 暗色图表方案)。 +- ⚠️ **待用户运行核对(GUI 无法自动驱动)**:① 暗色下网格详情的「加载中…」蒙板是深纱、色阶条深底浅字;② 数据列表按 5 根分页(根多的 TM 才出「加载更多」)+ 树嵌套正确。机器侧已验证:脚本抓原版结构 + 构建 + 122 测试 + 真实登录流程日志跑通(项目 1458977804960256→E3→data/page→getDetail→dynamicForm,无崩溃)。 + +--- + +## 1. 背景 + +- geopro = **Qt6 / C++ 离线桌面客户端**,目标**像素级 1:1 复刻** web 系统 `http://tenant.geomative.cn/#/projectSpace/datasetMange/datasetInfo`(赛盈地空)。 +- **硬约束**:禁 QWebEngine/Chromium(离线)。图表用本地 **QwtPlot(轴/交互/图例)+ VTK 算法层(等值线几何)+ 连续/离散色阶**。验收 = **视觉等价 + 交互一致**(非字节级 diff)。 +- **标准测试 ds**:`id=1458990939709440`(ddCode=dd_inversion_data)。 +- 站点 baseUrl:`http://tenant.geomative.cn/pop-api`。Auth header:`geomativeauthorization: Geomative `。 + +--- + +## 2. 技术栈与构建/测试 + +- Qt6 Widgets + **Qwt 6.2**(源码 `external/qwt-src/`,**用 `cmake/qwt.cmake` 以 CMake 构建**静态库,**不要用 qmake**——本机 VS2026 缺 vswhere)。 +- **VTK 9.6 仅算法层**(`vtkBandedPolyDataContourFilter`/`vtkStripper`/`vtkSplineFilter`/`vtkDataSetSurfaceFilter`),不做 VTK 渲染。 +- CMake(VS 自带)+ Ninja + MSVC 14.51;GoogleTest/CTest;vcpkg(仅非 Qt 依赖)。 +- **构建**:`powershell.exe -ExecutionPolicy Bypass -File scripts/dev-build.ps1` + - ⚠️ `LNK1104 无法打开 geopro_desktop.exe` = exe 在运行:先 `Get-Process geopro_desktop -ErrorAction SilentlyContinue | Stop-Process -Force` 再构建。 + - ⚠️ **改头文件后偶发** ninja 增量陈旧导致链接报「符号在 main.cpp.obj 重复定义」:删 `build/release/src/app/CMakeFiles/geopro_desktop.dir/main.cpp.obj` 后重建。 +- **测试**:`powershell.exe -ExecutionPolicy Bypass -File scripts/dev-test.ps1`(**只跑 ctest 不构建——必须先 dev-build**)。当前 **116/116 绿**。 + - 单测过滤:`build/release/tests/geopro_tests.exe --gtest_filter=ApiBatch.*` + - ⚠️ 直接跑 exe 时 4 个 `CrsTransform/VoxelRegister/Terrain` 失败是缺 `PROJ_DATA` 环境项;**ctest(dev-test)会注入环境、全绿**——以 dev-test 的 "X/X passed" 为准。 +- **网络/署名**:AuthLiveTest 联网真打站点;提交信息**全局禁用署名**(勿加 Co-Authored-By)。 + +--- + +## 3. 第一块:数据集详情图表(已完成,仅 dd_inversion_data) + +详情含两页签,均经用户逐项验收: +- **原数据**:Plotly scattergl 风格散点(方形点/白描边/连续色阶/x 轴顶部)。 +- **网格数据**:填充等值面(栅格 QImage)+ 黑色等值线(vtkStripper+vtkSplineFilter 样条平滑,**每条线只标一个**数值)+ NaN 白边裁剪 + 底部异常表/描述。 + +数据 API(均经 Playwright 实测): +| 页 | 接口 | +|---|---| +| 原数据 | `lvl/colorGradation/getDetail`(type1) + `dd/ert/inversion/getErtRawDataScatterGraph/{id}` | +| 网格 | `dd/ert/inversion/rows/{id}`(服务端网格化,**波动 1–4s**) + `colorGradation/getDetail`(type2) + `exception/queryException/{id}` | + +> ⚠️ 架构偏离:原 spec 定 QGraphicsView,**实际落地用 QwtPlot**(见 `plans/2026-06-11-dataset-detail-chart-v2-qwt.md`,权威)。 + +**策略分派已打通**(本次会话):控制器从硬编码 `dd_inversion_data` 改为走 `ChartStrategyRegistry`(在 **controller 层** `src/controller/IDatasetChartStrategy.hpp`,含 `hasGridPhase()`),未注册类型优雅降级「暂不支持」。`ErtInversionStrategy`(app 层)已注册。**这是接其余 dd 类型的地基**。 + +--- + +## 4. 第二块:全 App 网络层异步化(本次会话完成,无技术债) + +**动机**:原 `ApiClient` 用 `QEventLoop` 死等每个请求 → 全 App 冻 UI(网格 rows 1–4s 最痛)。 + +**核心安全不变量(spec §5.0,务必遵守)**:「abort 后绝不回灌」靠三件套—— +1. 每层 `aborted_` 入口守卫(`disconnect` 只是尽力而为,挡不住已入队的迟到信号); +2. 控制器**句柄身份比对**(`if (load != current_) return;` 丢弃迟到信号); +3. **一律 `deleteLater`**,禁止同步 delete。 +错误判定口径:业务 `code != 200 || !rawError.isEmpty()`(HTTP 200 也可能 code=500)。 + +**net 层原语**(`src/net/`,AUTOMOC 已 ON): +- `IApiCall`/`ApiCall`:单请求句柄(包 QNetworkReply),`finished(ApiResponse)` + `abort()`,自管理 deleteLater。 +- `ApiBatch`:**并发汇聚** N 个 IApiCall,全成功 `succeeded(QList)` / 任一失败 **fail-fast** `failed(i,resp)`+abort 其余。 +- `ApiChain`:**串行依赖链**(上步结果喂下步工厂,工厂可抛转 failed)。**契约:首个 step 工厂不得同步抛/同步 fire**(否则信号在调用方连接前丢失;生产路径都发异步请求,满足)。 +- `ApiResponseParse::buildResponse`:sync/async 共用解析(已无 sync 调用方,仅 ApiCall 用)。 +- `ApiClient`:仅 `getAsync/postJsonAsync`(**同步 `get/postJson`/`await`/QEventLoop 已删除**)。 + +**data 层**: +- 详情:`ChartLoad`/`GridLoad`(抽象基 + `ApiChartLoad`/`ApiGridLoad` 实现,包 ApiBatch + 注入 parse)、`IAsyncDatasetRepository`、`ApiDatasetRepository.loadChartAsync/loadGridAsync`、`DatasetLoads.hpp`(ChartParts/GridParts)。 +- 导航:`NavRequest`(单非模板句柄,`done(QVariant)`/`failed(QString)`;`ApiNavRequest` 包 IApiCall + 解析器)、`NavLoads.hpp`(各类型 `Q_DECLARE_METATYPE`)、`IAsyncProjectRepository`(9 方法 `...Async` 后缀,返回 `NavRequest*`,薄封装;汇聚/链编排放控制器)、`ApiProjectRepository` 仅实现异步接口。 + +**controller 层**: +- `DatasetDetailController`:abort-and-replace + 句柄身份比对 + `loadStarted(dsId,Phase)` + 析构 abort;走 `ChartStrategyRegistry` 分派。 +- `WorkbenchNavController`:全异步(NavRequest 续延依赖链 + 控制器内并发计数 + abort-and-replace + 身份比对);**删除 `busy_`/`BusyGuard`/`drainPendingCheckedTms`**;`busyChanged(bool)` 语义改为「有在飞句柄」(去抖);`setCheckedTms` 入口去重 + 「最后一次为准」由 abort-and-replace 承接 + `tmExceptionCache_` 缓存命中不发请求。对外信号面**零改动**(main.cpp 接线没动)。 + +**app 层**: +- `AuthService`(net 层):`fetchCaptchaAsync()→CaptchaLoad`、`loginAsync()→LoginLoad`(内用 ApiChain:verifyCodeCheck→RSA(step2 工厂内)→login2)。`LoginWindow`:不冻 + 可取消(析构 abort),删 `repaint()` hack。 +- `ProjectListDialog`:改用 `IAsyncProjectRepository`(NavRequest + abort-and-replace + 身份比对 + 析构 abort),过滤/分页/选项目行为等价。 +- `LoadingOverlay`:网格懒加载「加载中」遮罩,接 `loadStarted`、就绪/失败隐藏。 +- `main.cpp`:`qRegisterMetaType()`;装配注入 registry/异步 repo(引用绑定,接线信号面不变)。 + +**执行方式**:全程 subagent-driven(每块 implementer + spec 符合度评审 + 代码质量评审 opus + follow-up 加固)。测试 75 → 116(+41,含 abort 回灌防护回归用例)。 + +--- + +## 5. 关键文件地图 + +- net:`src/net/{IApiCall.hpp,ApiCall.*,ApiBatch.*,ApiChain.*,ApiResponseParse.*,ApiClient.*,AuthService.*,AuthLoads.*}` +- data:`src/data/api/{DatasetLoads.hpp,DatasetLoadHandles.*,ApiDatasetRepository.*,NavRequest.*,NavLoads.hpp,ApiProjectRepository.*,DatasetChartDto.*}`、`src/data/repo/{IAsyncDatasetRepository.hpp,IAsyncProjectRepository.hpp,IDatasetRepository.hpp(本地样例同步,保留),RepoTypes.hpp}` +- controller:`src/controller/{DatasetDetailController.*,WorkbenchNavController.*,IDatasetChartStrategy.hpp}` +- app:`src/app/main.cpp`、`src/app/panels/{DatasetDetailPanel.*,DatasetDetailPage.*,LoadingOverlay.*,chart/*}`、`src/app/login/LoginWindow.*`、`src/app/ProjectListDialog.*`、`src/app/panels/chart/ErtInversionStrategy.hpp` +- 测试:`tests/net/{FakeApiCall.hpp,test_api_batch.cpp,test_api_chain.cpp,test_auth.cpp(live),test_auth_loads.cpp}`、`tests/data/{test_nav_request.cpp,test_dataset_load_handles.cpp}`、`tests/controller/{test_dataset_detail_controller.cpp,test_workbench_nav_controller.cpp}`、`tests/app/test_chart_strategy_registry.cpp` + +--- + +## 6. 尚未完成 / 下一步(按优先级) + +### A. 收尾本分支(最先) +分支领先 main 68 commits、**122/122 绿**,但**工作区脏**(sessions 0.1–0.5 代码全未提交,见顶部 ⚠️)。用户多次选「保持现状」未提交未合并。**先决**:本次会话两组改动(数据集树/按根分页、暗色主题)**待用户在运行的 app 内目视核对**(见 0.5 末尾清单)——核对通过再谈提交。下一步可问用户:①目视核对本次改动 ②提交这批未提交工作(建议按主题分多个 commit:详情图保真 / 日志崩溃捕获 / 数据集树+分页 / 暗色主题)③合并回 main(本地) ④推送建 PR(origin=gitea `https://gitea.geomative.cn/gaozheng/geopro.git`) ⑤保持现状。用 `superpowers:finishing-a-development-branch`。 + +### B. 其余 dd 类型详情图(计划已写,多数 BLOCKED) +计划:`plans/2026-06-11-dataset-detail-other-dd-types.md`。 +- **Phase 1(策略分派打通):✅ 已完成**(本次会话)。 +- **Phase 0(样本探查):需做**——用 Playwright 登录原版 web,对每类 dd 找有数据对象、抓真实响应存 fixtures + 渲染规格。**这是后续所有类型的前置。** +- **Phase 2(ERT 测量散点)/ Phase 3(TEM 折线,新 LineChartView)**:仅当 Phase 0 确认有样本才解锁。 +- **Phase 4:BLOCKED**——dd_grid/轨迹/测井/GPR,**当前租户无活数据样本**(GPR 对象无数据、无测井数据),按 1:1 复刻铁律不能凭空实现。 +- 矩阵详见计划文件。 + +### C. 工具条编辑类功能(网格页占位按钮,未做,单独立项) +白化 / 滤波处理 / 色阶配置 / 异常框注 / 自动标注 / 网格化 / 另存为 / 导出 / 描述富文本 / 大视图(Esc)全屏。当前是占位按钮。交互较重,建议先 brainstorm 拆解。spec §2.3 列为范围外。 + +### D. 纯整洁 follow-up(非债、非阻断) +- 删 `DatasetDetailController::ChartData.grid/gridScale` 死字段(从未填充,预存)。 +- 若未来引入 cross-thread/QueuedConnection,再补 `qRegisterMetaType>()`。 + +--- + +## 7. 相关文档 + +- **spec**:`docs/superpowers/specs/2026-06-11-apiclient-async-design.md`(异步设计 + §5.0 安全不变量 + §7 错误判定/退出契约 + 顶部「状态更新」=已完成)、`docs/superpowers/specs/2026-06-11-dataset-detail-view-design.md`(详情视图,顶部状态=仅 ERT 反演完成 + QwtPlot 偏离)。 +- **plan**:`docs/superpowers/plans/2026-06-11-apiclient-async-datasetdetail.md`(详情试点)、`2026-06-11-apiclient-async-rollout.md`(导航 Part A + 登录 Part B,均落地)、`2026-06-11-dataset-detail-other-dd-types.md`(其余 dd 类型,Phase 1 done)、`2026-06-11-dataset-detail-chart-v2-qwt.md`(QwtPlot 返工,权威)。 +- API 文档:`docs/apis`(business_OpenAPI.json)。 +- 外部方案:`docs/Geopro3.0_二维图表返工技术方案.md`。 + +--- + +## 8. 记忆(务必遵守,`~/.claude/projects/D--Git-lanbingtech-geopro/memory/`) + +- `reply-in-chinese` — 全程中文回复。 +- `study-original-via-playwright` — **任何不确定必须用 Playwright 实地看原版,禁止臆测**(尤其做新 dd 类型详情图前抓样本/规格)。 +- `no-embellishment-replicate-exactly` — 严格 1:1,不加原版没有的特性(曾因等值线「周期重复标注」被纠正)。 + +--- + +## 9. 工作方式备注(本次会话沿用,效果好) + +- 用户偏好 **subagent-driven** 执行 + 实现后 **code review + spec 符合度** 双评审(opus 评审);「非必要不停,一口气做完」。 +- **真并行构建在本项目不安全**(dev-build 硬编码单一 build 目录 + 多任务共改 main.cpp/CMakeLists)→ 用顺序执行;worktree 隔离不了构建(硬编码路径 + 冷配置开销)。 +- 破坏性接口改形需**原子落地**(同批提交)保持构建绿(详情试点 Task5+6 合并、Part A A4+A5、本次清债 都是此教训)。 +- 评审 follow-up 多为小加固,直接在新代码上补;「不为不可能的场景写错误处理」(CLAUDE.md §2)——评审若建议为构造保证不会发生的情况加防御,可不采纳。 + diff --git a/docs/superpowers/plans/2026-06-11-apiclient-async-datasetdetail.md b/docs/superpowers/plans/2026-06-11-apiclient-async-datasetdetail.md new file mode 100644 index 0000000..88db486 --- /dev/null +++ b/docs/superpowers/plans/2026-06-11-apiclient-async-datasetdetail.md @@ -0,0 +1,1363 @@ +# ApiClient 异步化(DatasetDetail 路径)实现计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 把数据集详情路径(原数据 + 网格数据)从同步阻塞改为 Qt 原生异步,UI 全程不冻、慢请求可 abort、失败 fail-fast,作为全 App 异步化的模式样板。 + +**Architecture:** `ApiClient` 新增 `getAsync/postJsonAsync` 返回自管理 QObject 句柄 `ApiCall`(实现可测的 `IApiCall`);`ApiBatch` 并发汇聚多请求并 fail-fast;data 层 `ApiDatasetRepository` 用 batch 组装 `ChartLoad`/`GridLoad` 句柄(抽象基类 + Api 实现,供 stub 测试);`DatasetDetailController` 改 abort-and-replace + 句柄身份比对,对外信号面不变。核心安全靠「各层 `aborted_` 入口守卫 + 控制器句柄身份比对 + 一律 deleteLater」杜绝迟到信号回灌。 + +**Tech Stack:** Qt6 (Core/Network/Test)、QNetworkAccessManager 原生异步、CMake + Ninja + MSVC、GoogleTest/CTest、QSignalSpy。 + +**权威设计:** `docs/superpowers/specs/2026-06-11-apiclient-async-design.md`(含 §5.0 安全不变量、§7 错误判定、§8 测试策略)。 + +**构建/测试命令:** +- 构建:`powershell.exe -ExecutionPolicy Bypass -File scripts/dev-build.ps1` + - 若报 `LNK1104 ... geopro_desktop.exe`:先 `Get-Process geopro_desktop -ErrorAction SilentlyContinue | Stop-Process -Force` 再构建。 +- 全量测试:`powershell.exe -ExecutionPolicy Bypass -File scripts/dev-test.ps1`(基线 75/75 绿) +- 单测过滤:`build/release/tests/geopro_tests.exe --gtest_filter=ApiBatch.*` + +--- + +## 文件结构 + +**新建:** +- `src/net/IApiCall.hpp` — 抽象 QObject 缝:`finished(ApiResponse)` 信号 + 纯虚 `abort()`。 +- `src/net/ApiCall.hpp` / `src/net/ApiCall.cpp` — 包一个 `QNetworkReply` 的 `IApiCall` 实现。 +- `src/net/ApiResponseParse.hpp` / `src/net/ApiResponseParse.cpp` — `buildResponse(QNetworkReply*)`(从现 `Impl::await` 抽出解析部分,sync/async 共用,DRY)。 +- `src/net/ApiBatch.hpp` / `src/net/ApiBatch.cpp` — 并发汇聚 + fail-fast 原语。 +- `src/data/api/DatasetLoads.hpp` — `ChartParts` / `GridParts` 结果载体(纯结构体)。 +- `src/data/api/DatasetLoadHandles.hpp` / `src/data/api/DatasetLoadHandles.cpp` — 抽象 `ChartLoad`/`GridLoad`(QObject 基类)+ 具体 `ApiChartLoad`/`ApiGridLoad`(包 ApiBatch + 解析)。 +- `src/data/repo/IAsyncDatasetRepository.hpp` — 详情异步仓储抽象(返回 `ChartLoad*`/`GridLoad*`)。 +- `tests/net/FakeApiCall.hpp` — 测试假 call(**不声明 Q_OBJECT**,发射继承的 `finished`、记录 `abort`)。 +- `tests/net/test_api_batch.cpp` — ApiBatch 离线单测。 +- `tests/data/test_dataset_load_handles.cpp` — ChartLoad/GridLoad 句柄离线单测。 +- `src/app/panels/LoadingOverlay.hpp` / `src/app/panels/LoadingOverlay.cpp` — 轻量「加载中」遮罩(Task 8,可后置)。 + +**修改:** +- `src/net/ApiClient.hpp` / `.cpp` — 加 `getAsync/postJsonAsync`;`await` 改用 `buildResponse`;`ApiResponse` 加 `Q_DECLARE_METATYPE`。 +- `src/net/CMakeLists.txt` — AUTOMOC ON;加 ApiCall/ApiBatch/ApiResponseParse 源。 +- `src/data/api/ApiDatasetRepository.hpp` / `.cpp` — 改实现 `IAsyncDatasetRepository`。 +- `src/data/CMakeLists.txt` — AUTOMOC ON;加 DatasetLoadHandles 源。 +- `src/controller/DatasetDetailController.hpp` / `.cpp` — 依赖 `IAsyncDatasetRepository`、abort-and-replace、`loadStarted` 信号。 +- `tests/controller/test_dataset_detail_controller.cpp` — 改异步 stub + 回灌防护用例。 +- `tests/CMakeLists.txt` — 加 test_api_batch / test_dataset_load_handles。 +- `src/app/main.cpp` — `qRegisterMetaType()`;接 `loadStarted`(Task 8)。 +- `src/app/CMakeLists.txt` — 加 LoadingOverlay(Task 8)。 + +**不动:** `src/data/repo/IDatasetRepository.hpp`(同步,留 LocalSampleRepository)、Nav/Auth/Project 路径、`src/net/AuthService.*`。 + +--- + +## Task 1: net — 抽出 `buildResponse`(纯重构,行为不变) + +把现 `ApiClient::Impl::await` 里「读状态 + 读体 + 解析」的部分抽成自由函数,供同步 `await` 与异步 `ApiCall` 复用(DRY)。 + +**Files:** +- Create: `src/net/ApiResponseParse.hpp`, `src/net/ApiResponseParse.cpp` +- Modify: `src/net/ApiClient.cpp`, `src/net/CMakeLists.txt` + +- [ ] **Step 1: 写 `ApiResponseParse.hpp`** + +```cpp +#pragma once +#include "ApiClient.hpp" // for geopro::net::ApiResponse + +class QNetworkReply; + +namespace geopro::net { + +// 从一个【已完成】的 reply 读取状态码/响应体/错误并解析为 ApiResponse。 +// 不等待、不删除 reply(调用方负责生命周期)。供同步 await 与异步 ApiCall 共用。 +ApiResponse buildResponse(QNetworkReply* reply); + +} // namespace geopro::net +``` + +- [ ] **Step 2: 写 `ApiResponseParse.cpp`**(把现 ApiClient.cpp 的 parseBody + await 后半段搬来) + +```cpp +#include "ApiResponseParse.hpp" + +#include +#include +#include +#include +#include +#include + +namespace geopro::net { + +namespace { +constexpr int kHttpStatusUnset = 0; + +void parseBody(const QByteArray& body, ApiResponse& resp) { + if (body.isEmpty()) { + resp.rawError = QStringLiteral("empty response body"); + return; + } + QJsonParseError perr{}; + const QJsonDocument doc = QJsonDocument::fromJson(body, &perr); + if (perr.error != QJsonParseError::NoError || !doc.isObject()) { + resp.rawError = QStringLiteral("JSON parse error: %1; body: %2") + .arg(perr.errorString(), QString::fromUtf8(body)); + return; + } + const QJsonObject obj = doc.object(); + resp.code = obj.value(QStringLiteral("code")).toInt(); + resp.msg = obj.value(QStringLiteral("msg")).toString(); + const QJsonValue dataVal = obj.value(QStringLiteral("data")); + if (dataVal.isObject()) { + resp.data = dataVal.toObject(); + } else { + resp.data = QJsonObject{{QStringLiteral("value"), dataVal}}; + } +} +} // namespace + +ApiResponse buildResponse(QNetworkReply* reply) { + ApiResponse resp; + const QVariant statusVar = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + resp.httpStatus = statusVar.isValid() ? statusVar.toInt() : kHttpStatusUnset; + const QByteArray body = reply->readAll(); + if (reply->error() != QNetworkReply::NoError) { + resp.rawError = reply->errorString(); + } + if (!body.isEmpty()) { + parseBody(body, resp); + } + return resp; +} + +} // namespace geopro::net +``` + +- [ ] **Step 3: 改 `ApiClient.cpp` 用 buildResponse** + +删除 ApiClient.cpp 顶部 `parseBody` 匿名函数(已搬到 ApiResponseParse.cpp)与 `kHttpStatusUnset`、相关未用 include(`QJsonDocument/QJsonParseError/QJsonValue`)。在文件顶部 include 改为加 `#include "ApiResponseParse.hpp"`。把 `Impl::await` 改为: + +```cpp + static ApiResponse await(QNetworkReply* reply) { + QEventLoop loop; + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + return buildResponse(reply); + } +``` + +保留 `kContentTypeJson` / `kTokenHeader` / `buildRequest` 不动。 + +- [ ] **Step 4: 注册到 CMake** + +`src/net/CMakeLists.txt` 的 `add_library(geopro_net STATIC ...)` 列表加一行 `ApiResponseParse.cpp`: + +```cmake +add_library(geopro_net STATIC + crypto/RsaEncryptor.cpp + ApiClient.cpp + ApiResponseParse.cpp + AuthService.cpp) +``` + +- [ ] **Step 5: 构建 + 跑现有测试(回归安全网)** + +Run: `powershell.exe -ExecutionPolicy Bypass -File scripts/dev-test.ps1` +Expected: 75/75 PASS(`AuthLiveTest.FullLoginFlowReturnsToken` 走 sync 路径会经过 buildResponse,验证重构无回归)。 + +- [ ] **Step 6: Commit** + +```bash +git add src/net/ApiResponseParse.hpp src/net/ApiResponseParse.cpp src/net/ApiClient.cpp src/net/CMakeLists.txt +git commit -m "refactor(net): 抽出 buildResponse,sync/async 共用响应解析(DRY,行为不变)" +``` + +--- + +## Task 2: net — `IApiCall` + `ApiCall` + AUTOMOC + getAsync/postJsonAsync + +**Files:** +- Create: `src/net/IApiCall.hpp`, `src/net/ApiCall.hpp`, `src/net/ApiCall.cpp` +- Modify: `src/net/ApiClient.hpp`, `src/net/ApiClient.cpp`, `src/net/CMakeLists.txt` + +- [ ] **Step 1: 写 `IApiCall.hpp`** + +```cpp +#pragma once +#include +#include "ApiClient.hpp" // geopro::net::ApiResponse + +namespace geopro::net { + +// 单个异步请求的抽象句柄(可测试缝)。ApiBatch 仅依赖此抽象,单测可注入假 call。 +class IApiCall : public QObject { + Q_OBJECT +public: + using QObject::QObject; + ~IApiCall() override = default; + virtual void abort() = 0; // 置 aborted_、中断、deleteLater;abort 后绝不 emit finished +signals: + void finished(const geopro::net::ApiResponse& resp); // 成功/错误均经此(错误写 rawError) +}; + +} // namespace geopro::net +``` + +- [ ] **Step 2: 写 `ApiCall.hpp`** + +```cpp +#pragma once +#include +#include "IApiCall.hpp" + +class QNetworkReply; + +namespace geopro::net { + +// 包一个 QNetworkReply 的 IApiCall 实现。reply 完成 → buildResponse → emit finished → 自删。 +// 安全不变量(spec §5.0):abort 后绝不 emit;销毁一律 deleteLater,禁止同步 delete。 +class ApiCall : public IApiCall { + Q_OBJECT +public: + explicit ApiCall(QNetworkReply* reply, QObject* parent = nullptr); // 接管 reply + void abort() override; +private: + void onFinished(); + QPointer reply_; + bool aborted_ = false; +}; + +} // namespace geopro::net +``` + +- [ ] **Step 3: 写 `ApiCall.cpp`** + +```cpp +#include "ApiCall.hpp" + +#include +#include "ApiResponseParse.hpp" + +namespace geopro::net { + +ApiCall::ApiCall(QNetworkReply* reply, QObject* parent) : IApiCall(parent), reply_(reply) { + QObject::connect(reply_, &QNetworkReply::finished, this, &ApiCall::onFinished); +} + +void ApiCall::onFinished() { + if (aborted_) return; // §5.0 入口守卫:迟到信号闸门 + ApiResponse resp = buildResponse(reply_); + if (reply_) reply_->deleteLater(); + emit finished(resp); + deleteLater(); +} + +void ApiCall::abort() { + if (aborted_) return; + aborted_ = true; // 先置标志(权威闸门) + if (reply_) { + QObject::disconnect(reply_, nullptr, this, nullptr); // 尽力而为:断开未派发连接 + reply_->abort(); // 会再触发一次 finished,已被 disconnect+aborted_ 双挡 + reply_->deleteLater(); + } + deleteLater(); // 禁止同步 delete +} + +} // namespace geopro::net +``` + +- [ ] **Step 4: 改 `ApiClient.hpp`** — 加异步方法 + 元类型声明 + +在 `ApiResponse` 结构体定义之后(`};` 之下、`class ApiClient` 之上)插入: + +```cpp +} // namespace geopro::net ←★ 不要重复;见下方完整片段 +``` + +具体改法:①在 `class ApiClient` 内 `postJson` 声明后加两行;②文件末尾 `} // namespace geopro::net` 之后加 `Q_DECLARE_METATYPE`。 + +`class ApiClient` 内新增: +```cpp + // 异步 GET / POST(JSON):立即返回自管理句柄,不阻塞。调用方连 IApiCall::finished。 + net::IApiCall* getAsync(const QString& path); + net::IApiCall* postJsonAsync(const QString& path, const QJsonObject& body); +``` +(注意:此处在 `namespace geopro::net` 内,写 `IApiCall*` 即可,不必加 `net::` 前缀——按现文件风格用 `IApiCall*`。) + +前置声明区加 `class IApiCall;`(与现有 `class QNetworkAccessManager;` 并列)。 + +文件最末(`} // namespace geopro::net` 之后)加: +```cpp +#include +Q_DECLARE_METATYPE(geopro::net::ApiResponse) +``` + +- [ ] **Step 5: 改 `ApiClient.cpp`** — 实现异步方法 + +顶部加 `#include "ApiCall.hpp"`。在 `postJson` 实现之后加: + +```cpp +IApiCall* ApiClient::getAsync(const QString& path) { + QNetworkRequest req = impl_->buildRequest(path); + QNetworkReply* reply = impl_->nam.get(req); + return new ApiCall(reply); +} + +IApiCall* ApiClient::postJsonAsync(const QString& path, const QJsonObject& body) { + QNetworkRequest req = impl_->buildRequest(path); + const QByteArray payload = QJsonDocument(body).toJson(QJsonDocument::Compact); + QNetworkReply* reply = impl_->nam.post(req, payload); + return new ApiCall(reply); +} +``` +(`QJsonDocument` 已被 ApiClient.cpp 使用,保留其 include。) + +- [ ] **Step 6: 改 `src/net/CMakeLists.txt`** — AUTOMOC ON + 加源 + +```cmake +add_library(geopro_net STATIC + crypto/RsaEncryptor.cpp + ApiClient.cpp + ApiResponseParse.cpp + ApiCall.cpp + AuthService.cpp) +``` +并把末行改为: +```cmake +set_target_properties(geopro_net PROPERTIES AUTOMOC ON AUTOUIC OFF AUTORCC OFF) +``` +(AUTOMOC ON 仅对含 Q_OBJECT 的新文件生效,现有文件无 Q_OBJECT,无副作用。`IApiCall.hpp`/`ApiCall.hpp` 经 `ApiCall.cpp` 的 `#include` 被 AUTOMOC 扫描到。) + +- [ ] **Step 7: 构建(无新测试,验证编译/链接/moc)** + +Run: `powershell.exe -ExecutionPolicy Bypass -File scripts/dev-build.ps1` +Expected: 链接通过(moc 为 IApiCall/ApiCall 生成成功)。 +> ApiCall 直接包 QNetworkReply,离线无法稳定单测,由 Task 3(ApiBatch + FakeApiCall)与 Task 6(控制器集成)+ Task 8 手动验证覆盖。 + +- [ ] **Step 8: Commit** + +```bash +git add src/net/IApiCall.hpp src/net/ApiCall.hpp src/net/ApiCall.cpp src/net/ApiClient.hpp src/net/ApiClient.cpp src/net/CMakeLists.txt +git commit -m "feat(net): ApiClient.getAsync/postJsonAsync + IApiCall/ApiCall 异步句柄(abort+aborted_ 闸门,AUTOMOC ON)" +``` + +--- + +## Task 3: net — `ApiBatch`(fail-fast 汇聚)+ 离线单测(TDD) + +**Files:** +- Create: `src/net/ApiBatch.hpp`, `src/net/ApiBatch.cpp`, `tests/net/FakeApiCall.hpp`, `tests/net/test_api_batch.cpp` +- Modify: `src/net/CMakeLists.txt`, `tests/CMakeLists.txt` + +- [ ] **Step 1: 写 `tests/net/FakeApiCall.hpp`**(不声明 Q_OBJECT:发射继承自 IApiCall 的 finished) + +```cpp +#pragma once +#include "IApiCall.hpp" + +namespace geopro::net::test { + +// 测试假 call。不加 Q_OBJECT —— 仅发射继承自 IApiCall 的 finished 信号、override abort。 +class FakeApiCall : public IApiCall { +public: + using IApiCall::IApiCall; + bool aborted = false; + void abort() override { aborted = true; } + void fire(const ApiResponse& r) { emit finished(r); } // 发射继承的信号(无需自身 moc) +}; + +} // namespace geopro::net::test +``` + +- [ ] **Step 2: 写失败测试 `tests/net/test_api_batch.cpp`** + +```cpp +#include +#include +#include "ApiBatch.hpp" +#include "net/FakeApiCall.hpp" + +using namespace geopro::net; +using geopro::net::test::FakeApiCall; + +namespace { +ApiResponse ok(int code = 200) { ApiResponse r; r.code = code; r.httpStatus = 200; return r; } +ApiResponse bad() { ApiResponse r; r.code = 500; r.httpStatus = 200; r.msg = QStringLiteral("boom"); return r; } +// repo 注入的失败谓词:业务码 != 200 或传输错误。 +auto isFailure = [](const ApiResponse& r) { return r.code != 200 || !r.rawError.isEmpty(); }; +} + +TEST(ApiBatch, SucceedsWhenAllOk) { + auto* a = new FakeApiCall; + auto* b = new FakeApiCall; + auto* batch = new ApiBatch({a, b}, isFailure); + QSignalSpy okSpy(batch, &ApiBatch::succeeded); + QSignalSpy failSpy(batch, &ApiBatch::failed); + a->fire(ok()); + EXPECT_EQ(okSpy.count(), 0); // 还差 b + b->fire(ok()); + EXPECT_EQ(okSpy.count(), 1); // 全到齐 + EXPECT_EQ(failSpy.count(), 0); +} + +TEST(ApiBatch, FailFastAbortsOthers) { + auto* a = new FakeApiCall; + auto* b = new FakeApiCall; // 慢的(永不 fire) + auto* batch = new ApiBatch({a, b}, isFailure); + QSignalSpy failSpy(batch, &ApiBatch::failed); + QSignalSpy okSpy(batch, &ApiBatch::succeeded); + a->fire(bad()); // 首个失败 + EXPECT_EQ(failSpy.count(), 1); + EXPECT_EQ(okSpy.count(), 0); + EXPECT_TRUE(b->aborted); // 其余在飞被 abort +} + +TEST(ApiBatch, AbortGateSuppressesLateSignals) { + auto* a = new FakeApiCall; + auto* b = new FakeApiCall; + auto* batch = new ApiBatch({a, b}, isFailure); + QSignalSpy okSpy(batch, &ApiBatch::succeeded); + QSignalSpy failSpy(batch, &ApiBatch::failed); + batch->abort(); + EXPECT_TRUE(a->aborted); + EXPECT_TRUE(b->aborted); + a->fire(ok()); // 迟到信号 + b->fire(ok()); + EXPECT_EQ(okSpy.count(), 0); // aborted_ 闸门:不 emit + EXPECT_EQ(failSpy.count(), 0); +} +``` + +- [ ] **Step 3: 注册测试到 CMake** + +`tests/CMakeLists.txt`,在 net 段(`target_sources(geopro_tests PRIVATE net/test_auth.cpp)` 之后)加: +```cmake +target_sources(geopro_tests PRIVATE net/test_api_batch.cpp) +``` +并确保 net 段链接含 `Qt6::Test`(用于 QSignalSpy)。把第 50 行链接行改为: +```cmake +target_link_libraries(geopro_tests PRIVATE geopro_net OpenSSL::SSL OpenSSL::Crypto Qt6::Core Qt6::Network Qt6::Test) +``` +(`find_package(Qt6 COMPONENTS Test REQUIRED)` 在 controller 段已有;若 net 段在其之前编译报错,把该 find_package 上移到文件 net 段之前。) +`tests/net/` 头由 `target_include_directories(geopro_tests PRIVATE ${CMAKE_SOURCE_DIR}/tests)` 提供——若不存在则加: +```cmake +target_include_directories(geopro_tests PRIVATE ${CMAKE_SOURCE_DIR}/tests) +``` + +- [ ] **Step 4: 跑测试确认失败** + +Run: `powershell.exe -ExecutionPolicy Bypass -File scripts/dev-build.ps1` +Expected: 编译失败(`ApiBatch.hpp` 不存在)。 + +- [ ] **Step 5: 写 `src/net/ApiBatch.hpp`** + +```cpp +#pragma once +#include +#include +#include +#include +#include "IApiCall.hpp" + +namespace geopro::net { + +// 并发汇聚 N 个 IApiCall:全成功 → succeeded(按下标对齐);任一失败 → fail-fast: +// failed(index,resp) + abort 其余在飞 call。安全不变量见 spec §5.0。 +class ApiBatch : public QObject { + Q_OBJECT +public: + using Predicate = std::function; + ApiBatch(QList calls, Predicate isFailure, QObject* parent = nullptr); // 接管 calls + void abort(); +signals: + void succeeded(const QList& responses); + void failed(int index, const geopro::net::ApiResponse& resp); +private: + QList> calls_; + QList responses_; + Predicate isFailure_; + int remaining_ = 0; + bool aborted_ = false; +}; + +} // namespace geopro::net +``` + +- [ ] **Step 6: 写 `src/net/ApiBatch.cpp`** + +```cpp +#include "ApiBatch.hpp" + +namespace geopro::net { + +ApiBatch::ApiBatch(QList calls, Predicate isFailure, QObject* parent) + : QObject(parent), isFailure_(std::move(isFailure)) { + responses_.resize(calls.size()); + remaining_ = static_cast(calls.size()); + for (int i = 0; i < calls.size(); ++i) { + IApiCall* c = calls[i]; + calls_.append(c); + QObject::connect(c, &IApiCall::finished, this, [this, i](const ApiResponse& resp) { + if (aborted_) return; // §5.0 入口守卫 + if (isFailure_(resp)) { + aborted_ = true; + for (const auto& other : calls_) { // fail-fast:abort 其余在飞 + if (other) other->abort(); + } + emit failed(i, resp); + deleteLater(); + return; + } + responses_[i] = resp; + if (--remaining_ == 0) { + emit succeeded(responses_); + deleteLater(); + } + }); + } +} + +void ApiBatch::abort() { + if (aborted_) return; + aborted_ = true; + for (const auto& c : calls_) { + if (c) c->abort(); + } + deleteLater(); +} + +} // namespace geopro::net +``` + +- [ ] **Step 7: 加 ApiBatch.cpp 到 net 库** + +`src/net/CMakeLists.txt` 的源列表加 `ApiBatch.cpp`: +```cmake +add_library(geopro_net STATIC + crypto/RsaEncryptor.cpp + ApiClient.cpp + ApiResponseParse.cpp + ApiCall.cpp + ApiBatch.cpp + AuthService.cpp) +``` + +- [ ] **Step 8: 跑测试确认通过** + +Run: `powershell.exe -ExecutionPolicy Bypass -File scripts/dev-test.ps1` +Expected: `ApiBatch.SucceedsWhenAllOk` / `FailFastAbortsOthers` / `AbortGateSuppressesLateSignals` PASS;总数 78/78。 + +- [ ] **Step 9: Commit** + +```bash +git add src/net/ApiBatch.hpp src/net/ApiBatch.cpp src/net/CMakeLists.txt tests/net/FakeApiCall.hpp tests/net/test_api_batch.cpp tests/CMakeLists.txt +git commit -m "feat(net): ApiBatch 并发汇聚+fail-fast+abort闸门 + 离线单测" +``` + +--- + +## Task 4: data — 结果载体 + 异步接口 + 句柄(抽象基 + Api 实现)+ 句柄单测(TDD) + +**Files:** +- Create: `src/data/api/DatasetLoads.hpp`, `src/data/repo/IAsyncDatasetRepository.hpp`, `src/data/api/DatasetLoadHandles.hpp`, `src/data/api/DatasetLoadHandles.cpp`, `tests/data/test_dataset_load_handles.cpp` +- Modify: `src/data/CMakeLists.txt`, `tests/CMakeLists.txt` + +- [ ] **Step 1: 写 `src/data/api/DatasetLoads.hpp`**(纯结构体) + +```cpp +#pragma once +#include +#include "model/Field.hpp" +#include "model/ColorScale.hpp" +#include "model/Anomaly.hpp" + +namespace geopro::data { + +// 原数据加载结果:scatter + 散点色阶(type1)。 +struct ChartParts { + geopro::core::ScatterField scatter; + geopro::core::ColorScale scatterScale; +}; +// 网格数据加载结果:grid(rows) + 网格色阶(type2) + 异常。 +struct GridParts { + geopro::core::Grid grid{1, 1}; // Grid 无默认构造;占位 + geopro::core::ColorScale gridScale; + std::vector anomalies; +}; + +} // namespace geopro::data +``` + +- [ ] **Step 2: 写 `src/data/api/DatasetLoadHandles.hpp`**(抽象基 + Api 实现) + +```cpp +#pragma once +#include +#include +#include +#include +#include +#include "ApiBatch.hpp" +#include "DatasetLoads.hpp" + +namespace geopro::data { + +// ── 抽象句柄(可测试缝,类比 IApiCall):仓储返回基类指针,控制器/测试只依赖它 ── +class ChartLoad : public QObject { + Q_OBJECT +public: + using QObject::QObject; + ~ChartLoad() override = default; + virtual void abort() = 0; +signals: + void done(const geopro::data::ChartParts& parts); + void failed(const QString& message); +}; + +class GridLoad : public QObject { + Q_OBJECT +public: + using QObject::QObject; + ~GridLoad() override = default; + virtual void abort() = 0; +signals: + void done(const geopro::data::GridParts& parts); + void failed(const QString& message); +}; + +// ── Api 实现:包一个 ApiBatch + 注入的解析函数。batch.succeeded→解析→done;failed→failed ── +class ApiChartLoad : public ChartLoad { + Q_OBJECT +public: + using Parser = std::function&)>; + ApiChartLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent = nullptr); + void abort() override; +private: + QPointer batch_; + Parser parse_; + bool aborted_ = false; +}; + +class ApiGridLoad : public GridLoad { + Q_OBJECT +public: + using Parser = std::function&)>; + ApiGridLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent = nullptr); + void abort() override; +private: + QPointer batch_; + Parser parse_; + bool aborted_ = false; +}; + +} // namespace geopro::data +``` + +- [ ] **Step 3: 写 `src/data/api/DatasetLoadHandles.cpp`** + +```cpp +#include "api/DatasetLoadHandles.hpp" +#include + +namespace geopro::data { + +namespace { +QString reasonOf(const geopro::net::ApiResponse& r) { + return r.msg.isEmpty() ? r.rawError : r.msg; +} +} // namespace + +ApiChartLoad::ApiChartLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent) + : ChartLoad(parent), batch_(batch), parse_(std::move(parse)) { + QObject::connect(batch, &geopro::net::ApiBatch::succeeded, this, + [this](const QList& resps) { + if (aborted_) return; // §5.0 + try { + emit done(parse_(resps)); + } catch (const std::exception& e) { + emit failed(QString::fromUtf8(e.what())); + } + deleteLater(); + }); + QObject::connect(batch, &geopro::net::ApiBatch::failed, this, + [this](int, const geopro::net::ApiResponse& r) { + if (aborted_) return; + emit failed(reasonOf(r)); + deleteLater(); + }); +} + +void ApiChartLoad::abort() { + if (aborted_) return; + aborted_ = true; + if (batch_) batch_->abort(); + deleteLater(); +} + +ApiGridLoad::ApiGridLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent) + : GridLoad(parent), batch_(batch), parse_(std::move(parse)) { + QObject::connect(batch, &geopro::net::ApiBatch::succeeded, this, + [this](const QList& resps) { + if (aborted_) return; + try { + emit done(parse_(resps)); + } catch (const std::exception& e) { + emit failed(QString::fromUtf8(e.what())); + } + deleteLater(); + }); + QObject::connect(batch, &geopro::net::ApiBatch::failed, this, + [this](int, const geopro::net::ApiResponse& r) { + if (aborted_) return; + emit failed(reasonOf(r)); + deleteLater(); + }); +} + +void ApiGridLoad::abort() { + if (aborted_) return; + aborted_ = true; + if (batch_) batch_->abort(); + deleteLater(); +} + +} // namespace geopro::data +``` + +- [ ] **Step 4: 写 `src/data/repo/IAsyncDatasetRepository.hpp`** + +```cpp +#pragma once +#include + +namespace geopro::data { + +class ChartLoad; +class GridLoad; + +// 数据集详情异步仓储抽象。返回自管理句柄(完成/失败后 deleteLater)。 +class IAsyncDatasetRepository { +public: + virtual ~IAsyncDatasetRepository() = default; + virtual ChartLoad* loadChartAsync(const std::string& dsId) = 0; // scatter + 散点色阶(type1) + virtual GridLoad* loadGridAsync(const std::string& dsId) = 0; // grid(rows) + 色阶(type2) + 异常 +}; + +} // namespace geopro::data +``` + +- [ ] **Step 5: 写失败测试 `tests/data/test_dataset_load_handles.cpp`**(用真 ApiBatch + FakeApiCall + 桩解析,验证句柄 done/failed/abort 接线) + +```cpp +#include +#include +#include "api/DatasetLoadHandles.hpp" +#include "net/FakeApiCall.hpp" + +using namespace geopro::data; +using geopro::net::ApiBatch; +using geopro::net::ApiResponse; +using geopro::net::test::FakeApiCall; + +namespace { +ApiResponse ok() { ApiResponse r; r.code = 200; r.httpStatus = 200; return r; } +ApiResponse bad() { ApiResponse r; r.code = 500; r.httpStatus = 200; r.msg = QStringLiteral("boom"); return r; } +auto isFailure = [](const ApiResponse& r) { return r.code != 200 || !r.rawError.isEmpty(); }; +} + +TEST(DatasetLoadHandles, ChartLoadEmitsDoneOnSuccess) { + auto* a = new FakeApiCall; + auto* b = new FakeApiCall; + auto* batch = new ApiBatch({a, b}, isFailure); + bool parsed = false; + auto* load = new ApiChartLoad(batch, [&](const QList& resps) { + parsed = (resps.size() == 2); + return ChartParts{}; + }); + QSignalSpy doneSpy(load, &ChartLoad::done); + a->fire(ok()); + b->fire(ok()); + EXPECT_EQ(doneSpy.count(), 1); + EXPECT_TRUE(parsed); +} + +TEST(DatasetLoadHandles, ChartLoadEmitsFailedOnBatchFailure) { + auto* a = new FakeApiCall; + auto* b = new FakeApiCall; + auto* batch = new ApiBatch({a, b}, isFailure); + auto* load = new ApiChartLoad(batch, [](const QList&) { return ChartParts{}; }); + QSignalSpy failSpy(load, &ChartLoad::failed); + a->fire(bad()); + EXPECT_EQ(failSpy.count(), 1); +} + +TEST(DatasetLoadHandles, GridLoadAbortSuppressesLateDone) { + auto* a = new FakeApiCall; + auto* batch = new ApiBatch({a}, isFailure); + auto* load = new ApiGridLoad(batch, [](const QList&) { return GridParts{}; }); + QSignalSpy doneSpy(load, &GridLoad::done); + load->abort(); + a->fire(ok()); // 迟到 + EXPECT_EQ(doneSpy.count(), 0); // batch.aborted_ + load.aborted_ 双闸门 +} +``` + +- [ ] **Step 6: 注册 data 源 + 测试 + AUTOMOC** + +`src/data/CMakeLists.txt`:源列表加 `api/DatasetLoadHandles.cpp`,末行 AUTOMOC 改 ON: +```cmake +add_library(geopro_data STATIC + parse/SampleParsers.cpp + repo/LocalSampleRepository.cpp + dto/NavDto.cpp + dto/DatasetChartDto.cpp + api/ApiProjectRepository.cpp + api/ApiDatasetRepository.cpp + api/DatasetLoadHandles.cpp) +... +set_target_properties(geopro_data PROPERTIES AUTOMOC ON AUTOUIC OFF AUTORCC OFF) +``` +`tests/CMakeLists.txt` data 段(`target_sources(... data/test_dataset_chart_dto.cpp)` 之后)加: +```cmake +target_sources(geopro_tests PRIVATE data/test_dataset_load_handles.cpp) +``` +(data 测试段已 `target_link_libraries(... geopro_data)`;`geopro_data` PUBLIC 链 `geopro_net`,故 ApiBatch 头/符号可见。QSignalSpy 需 `Qt6::Test`——Task 3 已加到链接行。) + +- [ ] **Step 7: 跑测试确认通过** + +Run: `powershell.exe -ExecutionPolicy Bypass -File scripts/dev-test.ps1` +Expected: 3 个 `DatasetLoadHandles.*` PASS;总 81/81。 + +- [ ] **Step 8: Commit** + +```bash +git add src/data/api/DatasetLoads.hpp src/data/api/DatasetLoadHandles.hpp src/data/api/DatasetLoadHandles.cpp src/data/repo/IAsyncDatasetRepository.hpp src/data/CMakeLists.txt tests/data/test_dataset_load_handles.cpp tests/CMakeLists.txt +git commit -m "feat(data): 异步仓储接口 + ChartLoad/GridLoad 句柄(抽象基+Api实现) + 离线单测" +``` + +--- + +## Task 5: data — `ApiDatasetRepository` 改实现 `IAsyncDatasetRepository` + +**Files:** +- Modify: `src/data/api/ApiDatasetRepository.hpp`, `src/data/api/ApiDatasetRepository.cpp` + +- [ ] **Step 1: 改 `ApiDatasetRepository.hpp`** + +```cpp +#pragma once +#include "repo/IAsyncDatasetRepository.hpp" +namespace geopro::net { class ApiClient; } +namespace geopro::data { + +// 真实 API 实现 IAsyncDatasetRepository(ERT 反演)。每次加载返回自管理句柄。 +class ApiDatasetRepository : public IAsyncDatasetRepository { +public: + explicit ApiDatasetRepository(net::ApiClient& api); + ChartLoad* loadChartAsync(const std::string& dsId) override; + GridLoad* loadGridAsync(const std::string& dsId) override; +private: + net::ApiClient& api_; +}; +} // namespace geopro::data +``` + +- [ ] **Step 2: 改 `ApiDatasetRepository.cpp`**(保留 `enc`;`must` 改为复用 `isFailure` 谓词;用 getAsync/postJsonAsync + ApiBatch + ApiChartLoad/ApiGridLoad) + +```cpp +#include "api/ApiDatasetRepository.hpp" +#include +#include +#include +#include +#include "ApiClient.hpp" +#include "ApiBatch.hpp" +#include "api/DatasetLoadHandles.hpp" +#include "dto/DatasetChartDto.hpp" + +namespace geopro::data { +namespace { + +QString enc(const std::string& s) { + return QString::fromUtf8(QUrl::toPercentEncoding(QString::fromStdString(s))); +} + +// 失败判定(原 must() 口径):业务码 != 200 或传输错误。 +bool isFailure(const geopro::net::ApiResponse& r) { + return r.code != 200 || !r.rawError.isEmpty(); +} + +QJsonObject colorBody(const std::string& dsId, int type) { + return QJsonObject{{"dsObjectId", QString::fromStdString(dsId)}, {"businessCode", ""}, {"type", type}}; +} + +} // namespace + +ApiDatasetRepository::ApiDatasetRepository(net::ApiClient& api) : api_(api) {} + +ChartLoad* ApiDatasetRepository::loadChartAsync(const std::string& dsId) { + // index 0 = scatter(GET),index 1 = 散点色阶 type1(POST) + QList calls{ + api_.getAsync(QStringLiteral("/business/dd/ert/inversion/getErtRawDataScatterGraph/%1").arg(enc(dsId))), + api_.postJsonAsync(QStringLiteral("/business/lvl/colorGradation/getDetail"), colorBody(dsId, 1)), + }; + auto* batch = new net::ApiBatch(calls, &isFailure); + return new ApiChartLoad(batch, [](const QList& r) { + ChartParts p; + p.scatter = dto::parseScatterGraph(r[0].data); + p.scatterScale = dto::parseColorBar(r[1].data); + return p; + }); +} + +GridLoad* ApiDatasetRepository::loadGridAsync(const std::string& dsId) { + // index 0 = rows(GET,慢),1 = 色阶 type2(POST),2 = 异常(GET) + QList calls{ + api_.getAsync(QStringLiteral("/business/dd/ert/inversion/rows/%1").arg(enc(dsId))), + api_.postJsonAsync(QStringLiteral("/business/lvl/colorGradation/getDetail"), colorBody(dsId, 2)), + api_.getAsync(QStringLiteral("/business/exception/queryException/%1").arg(enc(dsId))), + }; + auto* batch = new net::ApiBatch(calls, &isFailure); + return new ApiGridLoad(batch, [](const QList& r) { + GridParts p; + p.grid = dto::parseInversionGrid(r[0].data); + p.gridScale = dto::parseColorBar(r[1].data); + p.anomalies = dto::parseDatasetAnomalies(r[2].data.value(QStringLiteral("value")).toArray()); + return p; + }); +} + +} // namespace geopro::data +``` +> 解析函数名/用法与原同步实现逐一对齐:`parseScatterGraph`/`parseColorBar`/`parseInversionGrid`/`parseDatasetAnomalies`(见原 ApiDatasetRepository.cpp)。`GridParts.grid` 由 `parseInversionGrid` 返回值覆盖(赋值即可,Grid 无默认构造但成员赋值用拷贝赋值合法)。 + +- [ ] **Step 3: 构建(真实 API 方法离线不可单测,验证编译/链接)** + +Run: `powershell.exe -ExecutionPolicy Bypass -File scripts/dev-build.ps1` +Expected: 链接通过。 +> 真实端点行为由 Task 8 手动验证覆盖(与现有 ApiProjectRepository 同:无离线单测)。 + +- [ ] **Step 4: Commit** + +```bash +git add src/data/api/ApiDatasetRepository.hpp src/data/api/ApiDatasetRepository.cpp +git commit -m "feat(data): ApiDatasetRepository 改异步(loadChartAsync/loadGridAsync, ApiBatch+句柄, code==200判定)" +``` + +--- + +## Task 6: controller — `DatasetDetailController` 异步化(abort-and-replace + 身份比对)+ 改测试(TDD) + +**Files:** +- Modify: `src/controller/DatasetDetailController.hpp`, `src/controller/DatasetDetailController.cpp`, `tests/controller/test_dataset_detail_controller.cpp` + +- [ ] **Step 1: 改 `DatasetDetailController.hpp`** + +```cpp +#pragma once +#include +#include +#include +#include +#include "model/Field.hpp" +#include "model/ColorScale.hpp" +#include "model/Anomaly.hpp" +namespace geopro::data { class IAsyncDatasetRepository; class ChartLoad; class GridLoad; } +namespace geopro::controller { + +class DatasetDetailController : public QObject { + Q_OBJECT +public: + enum class LoadPhase { Chart, Grid }; + Q_ENUM(LoadPhase) + + struct ChartData { + QString dsId, ddCode; + geopro::core::ScatterField scatter; + geopro::core::ColorScale scatterScale; + geopro::core::Grid grid{1, 1}; + geopro::core::ColorScale gridScale; + std::vector anomalies; + }; + struct GridData { + QString dsId; + geopro::core::Grid grid{1, 1}; + geopro::core::ColorScale gridScale; + std::vector anomalies; + }; + + explicit DatasetDetailController(data::IAsyncDatasetRepository& repo, QObject* parent = nullptr); +public slots: + void openDataset(const QString& dsId, const QString& ddCode); + void focusDataset(const QString& dsId); + void loadGridData(const QString& dsId, const QString& ddCode); +signals: + void loadStarted(const QString& dsId, LoadPhase phase); + void chartReady(const ChartData& data); + void gridReady(const GridData& data); + void focusRequested(const QString& dsId); + void loadFailed(const QString& dsId, const QString& message); +private: + data::IAsyncDatasetRepository& repo_; + QPointer chartLoad_; + QPointer gridLoad_; +}; +} // namespace geopro::controller +``` + +- [ ] **Step 2: 改 `DatasetDetailController.cpp`** + +```cpp +#include "DatasetDetailController.hpp" +#include "repo/IAsyncDatasetRepository.hpp" +#include "api/DatasetLoadHandles.hpp" +namespace geopro::controller { + +DatasetDetailController::DatasetDetailController(data::IAsyncDatasetRepository& repo, QObject* parent) + : QObject(parent), repo_(repo) {} + +void DatasetDetailController::openDataset(const QString& dsId, const QString& ddCode) { + if (ddCode != QLatin1String("dd_inversion_data")) { + emit loadFailed(dsId, QStringLiteral("暂不支持该数据类型的预览")); + return; + } + if (chartLoad_) chartLoad_->abort(); // abort-and-replace + data::ChartLoad* load = repo_.loadChartAsync(dsId.toStdString()); + chartLoad_ = load; + emit loadStarted(dsId, LoadPhase::Chart); + QObject::connect(load, &data::ChartLoad::done, this, + [this, load, dsId, ddCode](const data::ChartParts& parts) { + if (load != chartLoad_) return; // §5.0 句柄身份比对:丢弃迟到信号 + chartLoad_.clear(); + ChartData d; + d.dsId = dsId; + d.ddCode = ddCode; + d.scatter = parts.scatter; + d.scatterScale = parts.scatterScale; + emit chartReady(d); + }); + QObject::connect(load, &data::ChartLoad::failed, this, + [this, load, dsId](const QString& msg) { + if (load != chartLoad_) return; + chartLoad_.clear(); + emit loadFailed(dsId, msg); + }); +} + +void DatasetDetailController::loadGridData(const QString& dsId, const QString& ddCode) { + if (ddCode != QLatin1String("dd_inversion_data")) return; + if (gridLoad_) gridLoad_->abort(); + data::GridLoad* load = repo_.loadGridAsync(dsId.toStdString()); + gridLoad_ = load; + emit loadStarted(dsId, LoadPhase::Grid); + QObject::connect(load, &data::GridLoad::done, this, + [this, load, dsId](const data::GridParts& parts) { + if (load != gridLoad_) return; + gridLoad_.clear(); + GridData d; + d.dsId = dsId; + d.grid = parts.grid; + d.gridScale = parts.gridScale; + d.anomalies = parts.anomalies; + emit gridReady(d); + }); + QObject::connect(load, &data::GridLoad::failed, this, + [this, load, dsId](const QString& msg) { + if (load != gridLoad_) return; + gridLoad_.clear(); + emit loadFailed(dsId, msg); + }); +} + +void DatasetDetailController::focusDataset(const QString& dsId) { emit focusRequested(dsId); } +} // namespace geopro::controller +``` +> 析构:`chartLoad_`/`gridLoad_` 是 `QPointer`,控制器析构时若句柄仍在飞,句柄自身在完成/abort 后 deleteLater;为稳妥可在析构中 abort(可选,Qt 退出会清理)。本期不显式写析构(栈析构逆序 ctrl→repo→api 已保证安全,见 spec §7)。 + +- [ ] **Step 3: 改测试 `tests/controller/test_dataset_detail_controller.cpp`**(异步 stub + 回灌防护) + +```cpp +#include +#include +#include "DatasetDetailController.hpp" +#include "repo/IAsyncDatasetRepository.hpp" +#include "api/DatasetLoadHandles.hpp" +using namespace geopro; + +namespace { +// 桩句柄:不声明 Q_OBJECT —— 发射继承自 data::ChartLoad/GridLoad 的信号、override abort。 +struct StubChartLoad : data::ChartLoad { + bool aborted = false; + void abort() override { aborted = true; } + void fireDone() { emit done(data::ChartParts{}); } + void fireFailed() { emit failed(QStringLiteral("x")); } +}; +struct StubGridLoad : data::GridLoad { + bool aborted = false; + void abort() override { aborted = true; } + void fireDone() { emit done(data::GridParts{}); } +}; +struct StubAsyncRepo : data::IAsyncDatasetRepository { + StubChartLoad* lastChart = nullptr; + StubGridLoad* lastGrid = nullptr; + data::ChartLoad* loadChartAsync(const std::string&) override { + lastChart = new StubChartLoad; return lastChart; + } + data::GridLoad* loadGridAsync(const std::string&) override { + lastGrid = new StubGridLoad; return lastGrid; + } +}; +} + +TEST(DatasetDetailController, EmitsChartReadyOnDone) { + StubAsyncRepo repo; + controller::DatasetDetailController c(repo); + QSignalSpy spy(&c, &controller::DatasetDetailController::chartReady); + c.openDataset("ds1", "dd_inversion_data"); + repo.lastChart->fireDone(); + EXPECT_EQ(spy.count(), 1); +} + +TEST(DatasetDetailController, EmitsLoadFailedOnFailed) { + StubAsyncRepo repo; + controller::DatasetDetailController c(repo); + QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed); + c.openDataset("ds1", "dd_inversion_data"); + repo.lastChart->fireFailed(); + EXPECT_EQ(spy.count(), 1); +} + +TEST(DatasetDetailController, UnsupportedTypeFailsImmediately) { + StubAsyncRepo repo; + controller::DatasetDetailController c(repo); + QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed); + c.openDataset("ds1", "dd_other"); + EXPECT_EQ(spy.count(), 1); + EXPECT_EQ(repo.lastChart, nullptr); // 未发起加载 +} + +TEST(DatasetDetailController, AbortsPreviousOnReopen) { + StubAsyncRepo repo; + controller::DatasetDetailController c(repo); + c.openDataset("dsA", "dd_inversion_data"); + StubChartLoad* a = repo.lastChart; + c.openDataset("dsB", "dd_inversion_data"); // 替换 + EXPECT_TRUE(a->aborted); // 旧句柄被 abort +} + +TEST(DatasetDetailController, DropsLateSignalFromAbortedLoad) { + StubAsyncRepo repo; + controller::DatasetDetailController c(repo); + QSignalSpy spy(&c, &controller::DatasetDetailController::chartReady); + c.openDataset("dsA", "dd_inversion_data"); + StubChartLoad* a = repo.lastChart; + c.openDataset("dsB", "dd_inversion_data"); + StubChartLoad* b = repo.lastChart; + a->fireDone(); // 旧句柄迟到 → 身份比对丢弃 + EXPECT_EQ(spy.count(), 0); + b->fireDone(); // 当前句柄 → 正常 + EXPECT_EQ(spy.count(), 1); +} +``` + +- [ ] **Step 4: 跑测试确认通过** + +Run: `powershell.exe -ExecutionPolicy Bypass -File scripts/dev-test.ps1` +Expected: 5 个 `DatasetDetailController.*` PASS(替换原 2 个);总 84/84。 + +- [ ] **Step 5: Commit** + +```bash +git add src/controller/DatasetDetailController.hpp src/controller/DatasetDetailController.cpp tests/controller/test_dataset_detail_controller.cpp +git commit -m "feat(controller): DatasetDetailController 异步化(abort-and-replace+句柄身份比对+loadStarted, 移除busy_/catch) + 回灌防护测试" +``` + +--- + +## Task 7: app — 装配接线 + qRegisterMetaType + +**Files:** +- Modify: `src/app/main.cpp` + +- [ ] **Step 1: 注册元类型**(在 `main()` 早期、首次发起网络前,如 `QApplication app(...)` 之后) + +在 main.cpp 顶部 include 区确保有 `#include "ApiClient.hpp"`(已有,经 ApiDatasetRepository)。在 `QApplication` 构造之后加: + +```cpp + qRegisterMetaType(); +``` + +- [ ] **Step 2: 确认装配无需改动** + +`geopro::data::ApiDatasetRepository datasetRepo(api);`(main.cpp:847)与 `DatasetDetailController detailCtrl(datasetRepo);`(:848)——`datasetRepo` 现是 `IAsyncDatasetRepository`,控制器形参已改为该类型,**引用绑定无需改动**。第 505–527 行 chartReady/gridReady/loadFailed/focusRequested/gridDataNeeded 接线**全部不变**(信号面未变)。 + +- [ ] **Step 3: 构建 + 全量测试** + +Run: `powershell.exe -ExecutionPolicy Bypass -File scripts/dev-test.ps1` +Expected: 84/84 PASS。构建出 `geopro_desktop.exe`。 + +- [ ] **Step 4: 手动验证(核心收益)** + +启动 `build/release/src/app/geopro_desktop.exe`,登录 → 打开标准 ds `id=1458990939709440`: +- 双击数据集 → 原数据加载期间 **UI 不冻**(可拖窗口/切其他)。 +- 切到「网格数据」页签 → rows 1–4s 加载期间 **UI 不冻**。 +- rows 加载中**快速切换到另一数据集 / 关闭页签** → 旧请求被 abort(无旧数据回灌、无崩溃)。 +- 并发:原数据 2 请求、网格 3 请求并发发出(网络面板可见)。 + +- [ ] **Step 5: Commit** + +```bash +git add src/app/main.cpp +git commit -m "feat(app): 装配异步详情仓储 + qRegisterMetaType" +``` + +--- + +## Task 8: UI — `LoadingOverlay` + 加载反馈(可后置/并行) + +> 与异步内核无依赖,可在 Task 1–7 全绿后再做。先读 `src/app/panels/DatasetDetailPage.{hpp,cpp}` 确认 `gridView_` 容器与布局,再放遮罩。 + +**Files:** +- Create: `src/app/panels/LoadingOverlay.hpp`, `src/app/panels/LoadingOverlay.cpp` +- Modify: `src/app/panels/DatasetDetailPage.hpp`, `src/app/panels/DatasetDetailPage.cpp`, `src/app/panels/DatasetDetailPanel.hpp`, `src/app/panels/DatasetDetailPanel.cpp`, `src/app/main.cpp`, `src/app/CMakeLists.txt` + +- [ ] **Step 1: 写 `LoadingOverlay.hpp`** + +```cpp +#pragma once +#include +class QLabel; +namespace geopro::app { + +// 半透明「加载中…」遮罩。贴在目标视图上层,show()/hide() 切换,几何随父 resize 跟随。 +class LoadingOverlay : public QWidget { + Q_OBJECT +public: + explicit LoadingOverlay(QWidget* parent); + void showOver(); // 提到父尺寸、置顶、显示 +protected: + bool eventFilter(QObject* obj, QEvent* ev) override; // 跟随父 resize +private: + QLabel* label_; +}; + +} // namespace geopro::app +``` + +- [ ] **Step 2: 写 `LoadingOverlay.cpp`** + +```cpp +#include "panels/LoadingOverlay.hpp" +#include +#include +#include +#include + +namespace geopro::app { + +LoadingOverlay::LoadingOverlay(QWidget* parent) : QWidget(parent), label_(new QLabel(this)) { + setAttribute(Qt::WA_StyledBackground, true); + setStyleSheet(QStringLiteral("background: rgba(255,255,255,160);")); + label_->setText(QStringLiteral("加载中…")); + label_->setAlignment(Qt::AlignCenter); + auto* lay = new QVBoxLayout(this); + lay->addWidget(label_); + if (parent) parent->installEventFilter(this); + hide(); +} + +void LoadingOverlay::showOver() { + if (parentWidget()) setGeometry(parentWidget()->rect()); + raise(); + show(); +} + +bool LoadingOverlay::eventFilter(QObject* obj, QEvent* ev) { + if (obj == parentWidget() && ev->type() == QEvent::Resize && isVisible()) { + setGeometry(parentWidget()->rect()); + } + return QWidget::eventFilter(obj, ev); +} + +} // namespace geopro::app +``` + +- [ ] **Step 3: DatasetDetailPage 暴露加载状态接口** + +`DatasetDetailPage.hpp` 加方法声明(`setGridData` 附近): +```cpp + void setGridLoading(bool on); // 网格视图遮罩开关 +``` +`DatasetDetailPage.cpp`:在构造里于 `gridView_` 之上建一个 `LoadingOverlay* gridOverlay_`(父为 gridView_ 的容器或 gridView_ 本身),实现: +```cpp +void DatasetDetailPage::setGridLoading(bool on) { + if (on) gridOverlay_->showOver(); else gridOverlay_->hide(); +} +``` +(成员加 `LoadingOverlay* gridOverlay_;`,头部 `#include "panels/LoadingOverlay.hpp"` 或前置声明 + cpp include。`setGridData` 末尾调 `setGridLoading(false)`。) + +- [ ] **Step 4: DatasetDetailPanel 转发加载状态** + +`DatasetDetailPanel.hpp` 加: +```cpp + void setGridLoading(const QString& dsId, bool on); +``` +`DatasetDetailPanel.cpp` 实现:`pageFor(dsId)` 找到页 → `page->setGridLoading(on)`。 + +- [ ] **Step 5: main.cpp 接 loadStarted** + +在第 518 行 gridReady 接线附近加: +```cpp + QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::loadStarted, detailPanel, + [detailPanel](const QString& dsId, geopro::controller::DatasetDetailController::LoadPhase phase) { + if (phase == geopro::controller::DatasetDetailController::LoadPhase::Grid) + detailPanel->setGridLoading(dsId, true); + }); +``` +(原数据初次加载的 busy 反馈:页面于 chartReady 创建,加载期可设 `window.setCursor(Qt::BusyCursor)`,chartReady/loadFailed 复位——可选,最小化先不做。) + +- [ ] **Step 6: 注册 LoadingOverlay 到 app CMake** + +`src/app/CMakeLists.txt` 找到 panels 源列表,加 `panels/LoadingOverlay.cpp`(geopro app 目标已 AUTOMOC ON——确认;若否则比照现有 panel 文件加法)。 + +- [ ] **Step 7: 构建 + 手动验证遮罩** + +Run: `powershell.exe -ExecutionPolicy Bypass -File scripts/dev-build.ps1` +启动 app → 切网格页签:rows 加载期间网格区显示「加载中…」遮罩、到达后消失;切走/关页遮罩随页消失。 + +- [ ] **Step 8: Commit** + +```bash +git add src/app/panels/LoadingOverlay.hpp src/app/panels/LoadingOverlay.cpp src/app/panels/DatasetDetailPage.hpp src/app/panels/DatasetDetailPage.cpp src/app/panels/DatasetDetailPanel.hpp src/app/panels/DatasetDetailPanel.cpp src/app/main.cpp src/app/CMakeLists.txt +git commit -m "feat(app): 网格懒加载「加载中」遮罩(LoadingOverlay) 接 loadStarted" +``` + +--- + +## Self-Review(spec 覆盖核对) + +- **§2 目标 1(不冻 UI)** → Task 2/5/7(getAsync 去 QEventLoop)+ Task 7 手动验证。✓ +- **§2 目标 2(abort 过期)** → Task 2 ApiCall.abort / Task 3 ApiBatch.abort / Task 6 abort-and-replace + 身份比对。✓ +- **§2 目标 3(并发)** → Task 5 ApiBatch 同时发 2/3 请求。✓ +- **§2 目标 4(加载反馈)** → Task 6 loadStarted + Task 8 LoadingOverlay。✓ +- **§2 目标 5(可复用原语)** → Task 2/3 IApiCall/ApiCall/ApiBatch。✓ +- **§2 目标 6(测试不退化、离线可测)** → Task 1/3/4/6 单测,全程 dev-test。✓ +- **§5.0 安全不变量** → 各层 `aborted_`(ApiCall/ApiBatch/ApiChartLoad/ApiGridLoad)+ 控制器身份比对 + 全 deleteLater;回归用例 Task 3 AbortGate / Task 4 GridLoadAbort / Task 6 DropsLateSignal。✓ +- **§5.1 buildResponse 复用** → Task 1。✓ +- **§7 失败判定 code==200** → Task 5 isFailure。✓ +- **§5.2 旧方法去向 / 不碰 LocalSampleRepository** → Task 5 仅改 ApiDatasetRepository,IDatasetRepository 不动。✓ +- **§5.2 fail-fast** → Task 3 FailFastAbortsOthers。✓ +- **AUTOMOC** → Task 2(net ON)/ Task 4(data ON)。✓ +- **qRegisterMetaType** → Task 7。✓ + +类型/签名一致性核对:`IApiCall::finished(ApiResponse)`、`ApiBatch::succeeded(QList)`/`failed(int,ApiResponse)`、`ChartLoad::done(ChartParts)`/`failed(QString)`、`IAsyncDatasetRepository::loadChartAsync→ChartLoad*`、控制器 `loadStarted(QString,LoadPhase)` 全程一致。 + +--- + +## Execution Handoff + +计划已保存 `docs/superpowers/plans/2026-06-11-apiclient-async-datasetdetail.md`。两种执行方式: + +1. **Subagent-Driven(推荐)** — 每个 Task 派新 subagent 实现,任务间审查,迭代快。 +2. **Inline Execution** — 本会话内按 executing-plans 批量执行 + 检查点。 + +选哪个? + diff --git a/docs/superpowers/plans/2026-06-11-apiclient-async-rollout.md b/docs/superpowers/plans/2026-06-11-apiclient-async-rollout.md new file mode 100644 index 0000000..44d699f --- /dev/null +++ b/docs/superpowers/plans/2026-06-11-apiclient-async-rollout.md @@ -0,0 +1,1183 @@ +# ApiClient 异步化 — 全 App 铺开(导航 + 登录)实现计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: 用 `superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans` 逐任务执行本计划。每个 Task 内的 Step 用复选框(`- [ ]`)跟踪。每个 bite-sized Step 是一个 2–5 分钟动作(写失败测试 → 跑 → 实现 → 跑 → 提交)。 + +**Goal:** 把数据集详情试点已验证的「异步句柄 + abort 闸门」模式,铺开到全 App 剩余两条同步阻塞路径——**导航**(`ApiProjectRepository` + `WorkbenchNavController`,9 个仓储方法)与**登录**(`AuthService` 串行依赖链 + `LoginWindow`)。完成后 `ApiClient` 全程不再用 `QEventLoop` 阻塞 UI 线程,可移除同步 `get/postJson`。 + +**Architecture:** 复用 net 层已落地原语 `IApiCall`/`ApiCall`/`ApiBatch`(不重造)。新增一个**顺序执行原语 `ApiChain`**(依赖链:上一步结果喂下一步 + abort + aborted_ 闸门),供登录与导航的依赖链共用。data 层导航仓储改异步:用**统一泛型句柄 `NavRequest`**(抽象基 `INavRequest` + `ApiNavRequest` 实现,控制类爆炸),仓储方法返回 `NavRequest*`(emit `done(T)`/`failed(QString)`),替代同步 `RepoResult`。controller 层 `WorkbenchNavController` 用 abort-and-replace + 句柄身份比对取代 `busy_` 守卫与 `drainPendingCheckedTms` 重放机制(异步后 QEventLoop 重入消失,重放逻辑自然消亡)。登录 `LoginWindow` 接 `AuthService` 异步信号,登录期不冻、可取消。安全靠各层 `aborted_` 入口守卫 + 句柄身份比对 + 一律 `deleteLater`(沿用 spec §5.0)。 + +**Tech Stack:** Qt6(Core/Network/Test)、QNetworkAccessManager 原生异步、CMake + Ninja + MSVC、GoogleTest/CTest、QSignalSpy、`tests/net/FakeApiCall.hpp`(复用)。 + +**权威设计:** `docs/superpowers/specs/2026-06-11-apiclient-async-design.md`(§5.0 安全不变量、§7 错误判定 code==200/退出契约)。试点实现计划范式:`docs/superpowers/plans/2026-06-11-apiclient-async-datasetdetail.md`。 + +**构建/测试命令:** +- 构建:`powershell.exe -ExecutionPolicy Bypass -File scripts/dev-build.ps1` + - 若报 `LNK1104 ... geopro_desktop.exe`:先 `Get-Process geopro_desktop -ErrorAction SilentlyContinue | Stop-Process -Force` 再构建。 +- 全量测试(**只跑 ctest,不构建——须先 dev-build**):`powershell.exe -ExecutionPolicy Bypass -File scripts/dev-test.ps1` +- 单测过滤:`build/release/tests/geopro_tests.exe --gtest_filter=ApiChain.*` +- **基线:当前 89/89 绿。** 本计划第一阶段(导航)完成后预期 89 → ~104;第二阶段(登录)完成后预期 ~108。 + +--- + +## 阶段划分与拆分边界(顶部决策) + +本计划范围大(导航 9 方法 + 控制器复杂状态机 + 登录串行链 + 一个新原语),**建议分两阶段,并可拆成两份独立 plan 分两个 PR 落地**: + +- **Part A — 导航(本文件给出全部 bite-sized 任务,Task A0–A6)。** 先行理由:导航虽方法多但每个方法是「值进 → 句柄出」的机械翻译,模式与详情试点最贴近,风险**可控且可分散提交**;其控制器 `busy_`/drain 重构是难点但有详情控制器先例可照搬。`ApiChain` 原语在 Part A 内落地(导航依赖链需要它),同时为登录铺路。 +- **Part B — 登录(本文件给出高层任务骨架,Task B0–B5)。** 后行理由:登录是**串行依赖链 + 共享会话 + 模态对话框 `exec()`**,且有联网 live 测试 `test_auth.cpp` 需改造,风险点集中(会话 cookie、模态循环里的异步、取消语义),适合在 `ApiChain` 经导航验证稳定后再做。Part B 详细 bite-sized 任务在落地 Part A 后,按本骨架另起一份 plan(`2026-06-11-apiclient-async-login.md`)细化——或直接在本文件继续展开。 + +> **拆 plan 的判断:** 推荐拆。Part A 一个 PR(导航全异步,含 `ApiChain`),Part B 一个 PR(登录全异步 + 移除同步 `get/postJson`)。两 PR 之间 `ApiClient` 同步方法保留供登录过渡(登录是最后一个同步消费者)。若评审倾向单 PR,则按 Task 顺序 A0→A6→B0→B5 连续执行,每 Task 仍独立提交。 + +--- + +## 关键设计决策 + +### 决策 1:登录串行依赖链 → 新增 `ApiChain` 原语(而非嵌套 ApiCall 续延) + +**问题:** 登录是 `verifyCodeCheck` → RSA(本地) → `login2`,每步用上一步结果(且都走同一 NAM 共享 JSESSIONID)。导航也有依赖链:`start` = listWorkspaces → pageProjects → loadStructure;`switchWorkspace` = switch(写新 token) → pageProjects → loadStructure。`ApiBatch` 是**并发汇聚**,不适配。 + +**候选:** +- (a) 嵌套 `ApiCall` 续延(在每个 `finished` lambda 里发下一个请求):能用,但 abort/aborted_ 闸门要在每层手写,易漏;多处复制;测试难复用。 +- (b) **`ApiChain` 顺序执行原语(推荐)**:一个 QObject,持有一个「步骤列表」,每步是 `std::function& prior)>`(用既往响应构造下一请求,含本地变换如 RSA)。逐步执行:每步 `finished` → `isFailure` 判定(失败则 fail + abort + deleteLater)→ 成功则记录响应、执行下一步;末步成功 → `succeeded(QList)`。`abort()` 置 `aborted_`、abort 当前在飞 call、deleteLater。**入口守卫 `if (aborted_) return;` 同 ApiBatch。** + +**推荐 (b)。** 与 `ApiBatch` 对称(同 `Predicate`、同信号面 `succeeded(QList)`/`failed(int,ApiResponse)`、同安全不变量),可离线单测(FakeApiCall 注入),登录与导航依赖链共用。RSA 这类**纯本地步骤**不发请求:用「步骤工厂返回 nullptr 表示纯本地变换已在工厂内完成,直接进下一步」过于隐晦——改为 RSA 在「构造 login2 请求的工厂 lambda」内同步完成(工厂可抛 `std::exception`,被 ApiChain 捕获转 `failed`),无需把本地步骤建模为独立 chain step。 + +**共享会话不变量:** `ApiChain` 的每个 step 工厂调 `api_.postJsonAsync/getAsync`,全部走同一 `ApiClient`(同一 NAM),cookie/JSESSIONID 自然串联——与现同步链一致(见 `ApiClient.hpp:24-27` 注释)。`switchWorkspace` 链中途 `api_.setToken(newToken)` 的副作用:必须在「switch 响应到达、构造下一请求之前」执行——`ApiChain` 的 step 工厂在「构造下一请求时」运行,故把 `setToken` 放进「pageProjects 请求工厂」的开头(它能读到 switch 的响应),时序正确。 + +### 决策 2:导航 9 方法分类(依赖链 / 单请求 / 可并发) + +| 控制器入口 | 仓储调用序列 | 类型 | 原语 | +|---|---|---|---| +| `start()` | listWorkspaces → pageProjects → loadStructure | 依赖链 | `ApiChain` | +| `switchWorkspace(id)` | switchWorkspace(setToken) → pageProjects → loadStructure | 依赖链 | `ApiChain` | +| `switchProject(id)` | loadStructure | 单请求 | `NavRequest`(单 ApiCall) | +| `selectObject(id,t)` | loadRows(data) + loadRows(file) + loadObjectDetail | **可并发** | `ApiBatch` | +| `loadMoreData()` | loadRows(data) | 单请求 | `NavRequest` | +| `loadMoreFiles()` | loadRows(file) | 单请求 | `NavRequest` | +| `selectDataset(id)` | loadDatasetForm | 单请求 | `NavRequest` | +| `setCheckedTms(ids)` | loadExceptionsByTm × N(带缓存,缺失才拉) | **可并发**(仅未命中缓存项) | `ApiBatch` | + +> 仓储层保持「一方法一请求」的薄封装(返回 `NavRequest*`),**汇聚/链式编排放控制器**(它知道完整序列与状态)——与详情试点把汇聚放 repo 不同,因为导航的序列依赖控制器状态(缓存、当前项目/父节点),放 repo 会泄漏状态。这是导航与详情的**有意差异**,在 self-review 中复核。 + +### 决策 3:`RepoResult` 异步化 → 统一泛型句柄 `NavRequest` + +**问题:** 同步 `RepoResult` 返回值需变成「句柄 emit done(T)/failed(msg)」。导航有 9 个方法、7 种返回类型(`vector`、`bool`、`ProjectListPage`、`vector`、`vector`、`DsPage`、`DynamicForm`、`vector`)。每类型一个具体句柄类 = 类爆炸(且各自 Q_OBJECT/moc)。 + +**候选:** +- (a) 每方法/每类型一句柄(如详情的 `ChartLoad`/`GridLoad`):详情只有 2 个,可接受;导航 7+ 个,爆炸。 +- (b) **模板句柄 `NavRequest`(推荐)**:抽象基 `INavRequest`(纯虚 `abort()` + signals `done(T)`/`failed(QString)`)+ `ApiNavRequest`(包一个 `IApiCall`,构造注入 `std::function` 解析器)。**Qt 限制:模板类不能含 `Q_OBJECT`(moc 不支持模板)。** 解决:基类用**非模板** `NavRequestBase : QObject`(Q_OBJECT,signals 用 `QVariant` 承载 payload),模板层 `NavRequest` 继承它、提供 typed `done(T)` 转发;或更简单——**信号 payload 用 `QVariant`,typed 在控制器侧 `value.value()` 取出**。但 `T` 含自定义结构(`DsPage` 等)需 `Q_DECLARE_METATYPE` + 同线程直连无需注册。 +- (c) 统一非模板 `NavRequest`,payload 一律 `QVariant`(装任意 `T`)+ 控制器取出:**最省类**,无模板/moc 难题,代价是控制器侧 `.value()` 取值(需对各 `T` 加 `Q_DECLARE_METATYPE`)。 + +**推荐 (c) 的变体:单个非模板 `NavRequest : QObject`,signals `done(const QVariant&)`/`failed(const QString&)`,`abort()`;具体 `ApiNavRequest` 包 `IApiCall` + `std::function` 解析器。** 理由:① 零模板/零 moc 难题(单类,单次 Q_OBJECT);② 控制器已是「拿到 typed → emit 既有 typed 信号」,多一步 `qvariant_cast` 成本极小;③ 解析器 lambda 在 repo 内捕获 DTO 解析(与现 `dto::parseXxx` 一对一);④ abort/aborted_ 闸门只在一处实现。对各返回类型加 `Q_DECLARE_METATYPE`(`DsPage`/`DynamicForm`/`ProjectListPage`/`vector<...>` 等,同线程直连下声明即可、无需 `qRegisterMetaType`)。 + +> 与详情试点的 `ChartLoad`/`GridLoad`(typed 信号)风格略不同,但详情只 2 类、且 Parts 是合成结构;导航 7 类用 typed-per-class 不划算。`NavRequest`(QVariant) 是导航规模下的正确权衡,在 self-review 复核类型一致性。 + +### 决策 4:`WorkbenchNavController` 的 `busy_` / `drainPendingCheckedTms` 演化(难点) + +**现状根因:** `busy_` 守卫 + `BusyGuard` RAII 配平 `busyChanged` + `drainPendingCheckedTms` 重放,全部是**同步 `QEventLoop` 阻塞下「Qt 仍泵事件 → slot 重入」**的产物(spec §1)。`setCheckedTms` 在 `busy_` 时把请求挂起、待 `BusyGuard` 析构经 `QueuedConnection` 重放,正是为对抗嵌套循环重入。 + +**异步后演化:** +1. **删除 `busy_` 守卫**:异步不阻塞、无嵌套循环、无 slot 重入。 +2. **删除 `checkedTmsPending_` / `pendingCheckedTms_` / `drainPendingCheckedTms` / `BusyGuard` / `friend struct BusyGuard`**:重放机制随重入消失而消亡。 +3. **保留 `busyChanged(bool)` 信号**(main.cpp:751 接等待光标):语义从「同步阻塞中」改为「有在飞请求」。实现:控制器持有「在飞句柄计数 / 或每类一个 QPointer」,**发起请求时若之前无在飞→emit busyChanged(true),最后一个完成/失败→emit busyChanged(false)**。最简实现:每条路径一个 QPointer 成员(`startChain_`/`structReq_`/`selectBatch_`/`moreDataReq_`/`moreFilesReq_`/`datasetReq_`/`exceptionsBatch_`),任一非空即 busy。**KISS:** 用一个辅助 `emitBusyIfChanged()` 在每次 start/clear 后根据「是否存在任一在飞句柄」发信号(去抖:值变才发)。 +4. **abort-and-replace + 句柄身份比对**(取代 busy 拒绝重入):每条路径 abort 旧句柄、存新句柄、`done`/`failed` 里比对身份(`load != xxxReq_` 则丢弃迟到信号),与详情控制器一一对应。 +5. **`setCheckedTms` 的重入语义**:原「busy 时挂起最新一次、空闲重放」→ 异步后「**abort 旧异常批、发新批**」。用户快速改勾选 = 新请求覆盖旧请求(abort 旧 batch + 身份比对丢旧结果),**自然实现「以最后一次勾选为准」**,比原挂起-重放更简单且语义更优。缓存 `tmExceptionCache_` 保留(命中不发请求)。 + +> **难点处理明确化(无 TBD):** `drainPendingCheckedTms` 删除,其「最后一次为准」语义由 abort-and-replace 承接;`busy_` 删除,`busyChanged` 由「在飞句柄存在性」驱动;`BusyGuard`/`friend` 删除。验证用例见 Task A6 的 `SetCheckedTmsAbortsPreviousBatch` / `BusyChangedReflectsInflight`。 + +### 决策 5:原子落地以保持构建绿(迁移顺序) + +接口改形强耦合控制器(详情试点 Task5+6 合并的教训):`IProjectRepository`(同步 `RepoResult`)→ `IAsyncProjectRepository`(句柄)是**破坏性改形**,`ApiProjectRepository` 与 `WorkbenchNavController` 必须**同批提交**。本计划在 Task A4(repo 改形)与 Task A5(controller 改形)之间标注「**A4+A5 必须同一提交或连续提交且中间不要求构建绿**」——推荐做法:先在 A3 引入 `IAsyncProjectRepository` 新接口与 `NavRequest`(**新增、不删旧**,构建仍绿、旧路径仍用同步),A4 让 `ApiProjectRepository` **同时实现新旧两接口**(过渡),A5 切换控制器到新接口,A6 删除旧同步接口/方法。这样每步构建绿、可独立提交。 + +--- + +## 文件结构(每文件职责) + +### Part A — 导航 + +**新建:** +- `src/net/ApiChain.hpp` / `src/net/ApiChain.cpp` — 顺序执行原语。持步骤工厂列表 `QList`(`std::function& prior)>`,工厂可抛 `std::exception`)+ `Predicate isFailure`;逐步执行、fail-fast、`aborted_` 闸门、一律 deleteLater。signals `succeeded(QList)` / `failed(int index, ApiResponse)`。 +- `tests/net/test_api_chain.cpp` — ApiChain 离线单测(FakeApiCall 注入;顺序、失败短路、abort 闸门、工厂抛异常转 failed)。 +- `src/data/api/NavRequest.hpp` / `src/data/api/NavRequest.cpp` — `NavRequest : QObject`(抽象基:纯虚 `abort()`,signals `done(QVariant)`/`failed(QString)`)+ `ApiNavRequest`(包 `IApiCall` + `std::function` 解析器 + isFailure;finished→判定→解析→done/failed)。 +- `src/data/api/NavLoads.hpp` — `Q_DECLARE_METATYPE` 各导航返回类型(`std::vector`、`ProjectListPage`、`std::vector`、`std::vector`、`DsPage`、`DynamicForm`、`std::vector`、`bool` 已内置);并定义控制器编排用的合成结果载体(见下)。 +- `src/data/repo/IAsyncProjectRepository.hpp` — 异步导航仓储抽象:每方法返回 `NavRequest*`(薄封装,一方法一请求)。9 方法对齐现 `IProjectRepository`。 +- `tests/data/test_nav_request.cpp` — `ApiNavRequest` 离线单测(FakeApiCall + 桩解析器:done/failed/abort 闸门)。 +- `tests/controller/test_workbench_nav_controller.cpp` — 控制器离线单测(Stub 异步 repo + QSignalSpy):覆盖依赖链、并发、abort-and-replace、busyChanged、setCheckedTms 覆盖语义、回灌防护。**当前不存在**(导航控制器现无单测)。 + +**修改:** +- `src/net/CMakeLists.txt` — 源列表加 `ApiChain.cpp`(AUTOMOC 已 ON)。 +- `src/data/api/ApiProjectRepository.hpp` / `.cpp` — 改/扩展实现 `IAsyncProjectRepository`(每方法用 `getAsync/postJsonAsync` + `ApiNavRequest` + DTO 解析 lambda)。过渡期同时保留 `IProjectRepository`(A4),A6 删旧。 +- `src/data/CMakeLists.txt` — 源列表加 `NavRequest.cpp`(AUTOMOC 已 ON)。 +- `src/controller/WorkbenchNavController.hpp` / `.cpp` — 依赖 `IAsyncProjectRepository`;删 `busy_`/`BusyGuard`/`drain`/pending;改 `ApiChain`/`ApiBatch`/`NavRequest` 编排 + abort-and-replace + 身份比对 + busyChanged 重定义。 +- `tests/CMakeLists.txt` — 加 test_api_chain / test_nav_request / test_workbench_nav_controller。 +- `src/app/main.cpp` — 装配换 `IAsyncProjectRepository`(引用绑定,接线信号面不变);`ProjectListDialog` 形参类型核对(见 A5 Step)。 + +### Part B — 登录(骨架) + +**新建:** +- `src/net/AuthService` 改异步(见下);`tests/net/test_auth_async.cpp`(可选 live + 离线 stub 段)。 + +**修改:** +- `src/net/AuthService.hpp` / `.cpp` — `fetchCaptcha`/`login` 改异步:返回句柄或经回调/信号。用 `ApiChain` 编排 verify→login2(RSA 在 login2 工厂内)。 +- `src/app/login/LoginWindow.hpp` / `.cpp` — 接异步信号;登录期禁用按钮/显示「登录中」、不冻;可取消(关窗 abort)。 +- `tests/net/test_auth.cpp` — live 测试改造为异步等待(`QSignalSpy::wait`)。 +- `src/app/main.cpp` — 移除同步 `ApiClient::get/postJson` 消费后,删除同步方法(最终清理)。 + +**不动:** `src/data/repo/IDatasetRepository.hpp`、详情路径(已异步)、`LocalSampleRepository`、`src/net/crypto/RsaEncryptor.*`。 + +--- + +# Part A — 导航 + +## Task A0: net — `ApiChain` 顺序执行原语 + 离线单测(TDD) + +复用 `IApiCall`/`ApiResponse`,与 `ApiBatch` 对称。**新增不破坏现有。** + +**Files:** +- Create: `src/net/ApiChain.hpp`, `src/net/ApiChain.cpp`, `tests/net/test_api_chain.cpp` +- Modify: `src/net/CMakeLists.txt`, `tests/CMakeLists.txt` + +- [ ] **Step 1: 写失败测试 `tests/net/test_api_chain.cpp`**(复用 `tests/net/FakeApiCall.hpp`) + +```cpp +#include +#include +#include +#include "ApiChain.hpp" +#include "net/FakeApiCall.hpp" + +using namespace geopro::net; +using geopro::net::test::FakeApiCall; + +namespace { +ApiResponse ok(int v = 0) { ApiResponse r; r.code = 200; r.httpStatus = 200; r.data = QJsonObject{{"v", v}}; return r; } +ApiResponse bad() { ApiResponse r; r.code = 500; r.httpStatus = 200; r.msg = QStringLiteral("boom"); return r; } +auto isFailure = [](const ApiResponse& r) { return r.code != 200 || !r.rawError.isEmpty(); }; +} + +TEST(ApiChain, RunsStepsInOrderAndPassesPriorResponses) { + auto* s1 = new FakeApiCall; + auto* s2 = new FakeApiCall; + int seenPrior = -1; + QList steps{ + [&](const QList&) -> IApiCall* { return s1; }, + [&](const QList& prior) -> IApiCall* { + seenPrior = prior.size(); // 第二步能看到第一步响应 + return s2; + }}; + auto* chain = new ApiChain(steps, isFailure); + QSignalSpy okSpy(chain, &ApiChain::succeeded); + s1->fire(ok(11)); // 第一步完成 → 触发第二步工厂 + EXPECT_EQ(seenPrior, 1); + EXPECT_EQ(okSpy.count(), 0); // 还差第二步 + s2->fire(ok(22)); + EXPECT_EQ(okSpy.count(), 1); + const auto resps = okSpy.takeFirst().at(0).value>(); + EXPECT_EQ(resps.size(), 2); +} + +TEST(ApiChain, FailFastShortCircuitsRemainingSteps) { + auto* s1 = new FakeApiCall; + bool secondBuilt = false; + QList steps{ + [&](const QList&) -> IApiCall* { return s1; }, + [&](const QList&) -> IApiCall* { secondBuilt = true; return new FakeApiCall; }}; + auto* chain = new ApiChain(steps, isFailure); + QSignalSpy failSpy(chain, &ApiChain::failed); + s1->fire(bad()); // 第一步失败 + EXPECT_EQ(failSpy.count(), 1); + EXPECT_FALSE(secondBuilt); // 后续步骤不再构造 +} + +TEST(ApiChain, AbortGateSuppressesLateSignals) { + auto* s1 = new FakeApiCall; + QList steps{[&](const QList&) -> IApiCall* { return s1; }}; + auto* chain = new ApiChain(steps, isFailure); + QSignalSpy okSpy(chain, &ApiChain::succeeded); + chain->abort(); + EXPECT_TRUE(s1->aborted); // 在飞步骤被 abort + s1->fire(ok()); // 迟到 + EXPECT_EQ(okSpy.count(), 0); // aborted_ 闸门 +} + +TEST(ApiChain, StepFactoryThrowBecomesFailed) { + auto* s1 = new FakeApiCall; + QList steps{ + [&](const QList&) -> IApiCall* { return s1; }, + [&](const QList&) -> IApiCall* { throw std::runtime_error("rsa fail"); }}; + auto* chain = new ApiChain(steps, isFailure); + QSignalSpy failSpy(chain, &ApiChain::failed); + s1->fire(ok()); // 触发第二步工厂 → 抛 → failed + EXPECT_EQ(failSpy.count(), 1); +} +``` + +- [ ] **Step 2: 注册测试到 CMake,跑确认编译失败** + +`tests/CMakeLists.txt` net 段(`net/test_api_batch.cpp` 之后)加: +```cmake +target_sources(geopro_tests PRIVATE net/test_api_chain.cpp) +``` +Run: `powershell.exe -ExecutionPolicy Bypass -File scripts/dev-build.ps1` +Expected: 编译失败(`ApiChain.hpp` 不存在)。 + +- [ ] **Step 3: 写 `src/net/ApiChain.hpp`** + +```cpp +#pragma once +#include +#include +#include +#include +#include "IApiCall.hpp" + +namespace geopro::net { + +// 顺序执行 N 个步骤(依赖链):每步工厂用既往响应构造下一 IApiCall(工厂可抛 std::exception)。 +// 任一步失败 → fail-fast:failed(index,resp) + abort 当前在飞 + deleteLater。 +// 全部成功 → succeeded(按序响应)。安全不变量见 spec §5.0(aborted_ 闸门 + 一律 deleteLater)。 +// 与 ApiBatch 对称:同 Predicate、同信号面、同安全约束。 +class ApiChain : public QObject { + Q_OBJECT +public: + // 工厂:入参为已完成步骤的响应(按序),返回本步 IApiCall(接管所有权)。可抛 std::exception。 + using StepFactory = std::function& prior)>; + using Predicate = std::function; + ApiChain(QList steps, Predicate isFailure, QObject* parent = nullptr); + void abort(); +signals: + void succeeded(const QList& responses); + void failed(int index, const geopro::net::ApiResponse& resp); +private: + void runNext(); // 构造并连接下一步(工厂抛出 → emit failed) + QList steps_; + Predicate isFailure_; + QList responses_; + QPointer current_; + int index_ = 0; + bool aborted_ = false; +}; + +} // namespace geopro::net +``` + +- [ ] **Step 4: 写 `src/net/ApiChain.cpp`** + +```cpp +#include "ApiChain.hpp" +#include + +namespace geopro::net { + +ApiChain::ApiChain(QList steps, Predicate isFailure, QObject* parent) + : QObject(parent), steps_(std::move(steps)), isFailure_(std::move(isFailure)) { + Q_ASSERT(!steps_.isEmpty()); // 契约:至少一步(空链永不发 succeeded) + Q_ASSERT(isFailure_); + runNext(); +} + +void ApiChain::runNext() { + if (aborted_) return; + if (index_ >= steps_.size()) { // 全部完成 + emit succeeded(responses_); + deleteLater(); + return; + } + IApiCall* call = nullptr; + try { + call = steps_[index_](responses_); // 工厂可抛(如 RSA 失败):转 failed + } catch (const std::exception&) { + ApiResponse r; + r.rawError = QStringLiteral("步骤构造失败"); // 详细原因由控制器层兜底文案/或工厂内写入 + aborted_ = true; + emit failed(index_, r); + deleteLater(); + return; + } + current_ = call; + QObject::connect(call, &IApiCall::finished, this, [this](const ApiResponse& resp) { + if (aborted_) return; // §5.0 入口守卫 + if (isFailure_(resp)) { + aborted_ = true; + emit failed(index_, resp); + deleteLater(); + return; + } + responses_.append(resp); + ++index_; + runNext(); // 链式推进 + }); +} + +void ApiChain::abort() { + if (aborted_) return; + aborted_ = true; + if (current_) current_->abort(); // abort 当前在飞步骤 + deleteLater(); +} + +} // namespace geopro::net +``` +> 注:工厂抛异常时的详细原因——若需保留 `e.what()`,可把 `rawError` 设为 `QString::fromUtf8(e.what())`;登录 RSA 失败文案在 AuthService 层包装(Part B)。导航链工厂不做本地变换、不会抛,此分支主要服务 Part B。 + +- [ ] **Step 5: 加 `ApiChain.cpp` 到 net 库** + +`src/net/CMakeLists.txt` 源列表加 `ApiChain.cpp`(在 `ApiBatch.cpp` 后)。 + +- [ ] **Step 6: 跑测试确认通过** + +Run: `powershell.exe -ExecutionPolicy Bypass -File scripts/dev-test.ps1` +Expected: 4 个 `ApiChain.*` PASS;总 93/93。 + +- [ ] **Step 7: Commit** + +```bash +git add src/net/ApiChain.hpp src/net/ApiChain.cpp src/net/CMakeLists.txt tests/net/test_api_chain.cpp tests/CMakeLists.txt +git commit -m "feat(net): ApiChain 顺序依赖链原语(fail-fast+abort闸门+工厂可抛) + 离线单测" +``` + +--- + +## Task A1: data — `NavRequest` 句柄(单类,QVariant payload)+ 元类型声明 + 离线单测(TDD) + +**Files:** +- Create: `src/data/api/NavLoads.hpp`, `src/data/api/NavRequest.hpp`, `src/data/api/NavRequest.cpp`, `tests/data/test_nav_request.cpp` +- Modify: `src/data/CMakeLists.txt`, `tests/CMakeLists.txt` + +- [ ] **Step 1: 写 `src/data/api/NavLoads.hpp`**(元类型声明 + 编排合成载体) + +```cpp +#pragma once +#include +#include +#include "repo/RepoTypes.hpp" + +// 导航异步返回类型经 QVariant 承载:同线程直连仅需 Q_DECLARE_METATYPE(无需 qRegisterMetaType)。 +Q_DECLARE_METATYPE(std::vector) +Q_DECLARE_METATYPE(geopro::data::ProjectListPage) +Q_DECLARE_METATYPE(std::vector) +Q_DECLARE_METATYPE(std::vector) +Q_DECLARE_METATYPE(geopro::data::DsPage) +Q_DECLARE_METATYPE(geopro::data::DynamicForm) +Q_DECLARE_METATYPE(std::vector) +// bool 已内置 QMetaType。 + +namespace geopro::data { + +// 控制器并发编排 selectObject 的三响应合成(data+file+detail),仅控制器内部使用。 +// (此结构供 self-doc;实际并发由控制器用 ApiBatch + 各 NavRequest 解析器组装,见 Task A5。) + +} // namespace geopro::data +``` +> `switchWorkspace` 返回 `bool` 但需副作用 `setToken`:该副作用放在「链工厂」里(见 A5),`NavRequest` 仅承载成功标志。`switchWorkspace` 解析器需访问 `accessToken` → 由 repo 解析 lambda 内 `api_.setToken(...)`(见 A4 Step 2 注)。 + +- [ ] **Step 2: 写 `src/data/api/NavRequest.hpp`** + +```cpp +#pragma once +#include +#include +#include +#include +#include +#include "IApiCall.hpp" + +namespace geopro::data { + +// 单请求异步句柄(抽象基,可测试缝):payload 经 QVariant 承载,控制器侧 qvariant_cast 取出。 +class NavRequest : public QObject { + Q_OBJECT +public: + using QObject::QObject; + ~NavRequest() override = default; + virtual void abort() = 0; +signals: + void done(const QVariant& value); + void failed(const QString& message); +}; + +// Api 实现:包一个 IApiCall + 注入的解析器(ApiResponse → QVariant)+ 失败谓词。 +class ApiNavRequest : public NavRequest { + Q_OBJECT +public: + using Parser = std::function; + using Predicate = std::function; + ApiNavRequest(geopro::net::IApiCall* call, Parser parse, Predicate isFailure, + QObject* parent = nullptr); // 接管 call + void abort() override; +private: + QPointer call_; + Parser parse_; + Predicate isFailure_; + bool aborted_ = false; +}; + +} // namespace geopro::data +``` + +- [ ] **Step 3: 写 `src/data/api/NavRequest.cpp`** + +```cpp +#include "api/NavRequest.hpp" +#include + +namespace geopro::data { + +namespace { +QString reasonOf(const geopro::net::ApiResponse& r) { + return r.msg.isEmpty() ? r.rawError : r.msg; +} +} // namespace + +ApiNavRequest::ApiNavRequest(geopro::net::IApiCall* call, Parser parse, Predicate isFailure, + QObject* parent) + : NavRequest(parent), call_(call), parse_(std::move(parse)), isFailure_(std::move(isFailure)) { + QObject::connect(call, &geopro::net::IApiCall::finished, this, + [this](const geopro::net::ApiResponse& resp) { + if (aborted_) return; // §5.0 入口守卫 + if (isFailure_(resp)) { + emit failed(reasonOf(resp)); + deleteLater(); + return; + } + QVariant out; + try { + out = parse_(resp); // 仅解析在 try 内(下游 done 处理器抛出不误报) + } catch (const std::exception& e) { + emit failed(QString::fromUtf8(e.what())); + deleteLater(); + return; + } catch (...) { + emit failed(QStringLiteral("解析失败:未知异常")); + deleteLater(); + return; + } + emit done(out); + deleteLater(); + }); +} + +void ApiNavRequest::abort() { + if (aborted_) return; + aborted_ = true; + if (call_) call_->abort(); + deleteLater(); +} + +} // namespace geopro::data +``` + +- [ ] **Step 4: 写失败测试 `tests/data/test_nav_request.cpp`** + +```cpp +#include +#include +#include +#include "api/NavRequest.hpp" +#include "api/NavLoads.hpp" +#include "net/FakeApiCall.hpp" + +using namespace geopro::data; +using geopro::net::ApiResponse; +using geopro::net::test::FakeApiCall; + +namespace { +ApiResponse ok() { ApiResponse r; r.code = 200; r.httpStatus = 200; return r; } +ApiResponse bad() { ApiResponse r; r.code = 500; r.httpStatus = 200; r.msg = QStringLiteral("boom"); return r; } +auto isFailure = [](const ApiResponse& r) { return r.code != 200 || !r.rawError.isEmpty(); }; +} + +TEST(NavRequest, EmitsDoneWithParsedPayload) { + auto* call = new FakeApiCall; + auto* req = new ApiNavRequest(call, + [](const ApiResponse&) { return QVariant::fromValue(DsPage{{}, 42}); }, isFailure); + QSignalSpy doneSpy(req, &NavRequest::done); + call->fire(ok()); + ASSERT_EQ(doneSpy.count(), 1); + const auto page = doneSpy.takeFirst().at(0).toMap().isEmpty() + ? qvariant_cast(doneSpy.count() ? QVariant() : QVariant()) // 见下注 + : DsPage{}; + // 简化断言:直接从信号取 QVariant +} + +TEST(NavRequest, EmitsFailedOnBusinessError) { + auto* call = new FakeApiCall; + auto* req = new ApiNavRequest(call, [](const ApiResponse&) { return QVariant(); }, isFailure); + QSignalSpy failSpy(req, &NavRequest::failed); + call->fire(bad()); + EXPECT_EQ(failSpy.count(), 1); +} + +TEST(NavRequest, AbortSuppressesLateDone) { + auto* call = new FakeApiCall; + auto* req = new ApiNavRequest(call, [](const ApiResponse&) { return QVariant(); }, isFailure); + QSignalSpy doneSpy(req, &NavRequest::done); + req->abort(); + EXPECT_TRUE(call->aborted); + call->fire(ok()); + EXPECT_EQ(doneSpy.count(), 0); +} +``` +> 实现者注:`EmitsDoneWithParsedPayload` 的 payload 断言改为直接 `qvariant_cast(doneSpy.takeFirst().at(0))` 并 `EXPECT_EQ(page.total, 42)`(上方草稿的取值写法笨拙,落地时简化为这一行;`DsPage` 已 `Q_DECLARE_METATYPE`)。 + +- [ ] **Step 5: 注册 data 源 + 测试到 CMake** + +`src/data/CMakeLists.txt` 源列表加 `api/NavRequest.cpp`(在 `api/DatasetLoadHandles.cpp` 后;AUTOMOC 已 ON)。 +`tests/CMakeLists.txt` data 段(`data/test_dataset_load_handles.cpp` 后)加: +```cmake +target_sources(geopro_tests PRIVATE data/test_nav_request.cpp) +``` + +- [ ] **Step 6: 跑测试确认通过** + +Run: `powershell.exe -ExecutionPolicy Bypass -File scripts/dev-test.ps1` +Expected: 3 个 `NavRequest.*` PASS;总 96/96。 + +- [ ] **Step 7: Commit** + +```bash +git add src/data/api/NavLoads.hpp src/data/api/NavRequest.hpp src/data/api/NavRequest.cpp src/data/CMakeLists.txt tests/data/test_nav_request.cpp +git commit -m "feat(data): NavRequest 单请求异步句柄(QVariant payload, abort闸门) + 元类型声明 + 离线单测" +``` + +--- + +## Task A2: data — `IAsyncProjectRepository` 抽象接口(新增,不删旧) + +**Files:** +- Create: `src/data/repo/IAsyncProjectRepository.hpp` + +- [ ] **Step 1: 写 `src/data/repo/IAsyncProjectRepository.hpp`** + +```cpp +#pragma once +#include + +namespace geopro::data { + +class NavRequest; + +// 导航异步仓储抽象(薄封装:一方法一请求,返回自管理句柄 emit done(QVariant)/failed(msg))。 +// 汇聚/链式编排由 WorkbenchNavController 负责(它知道完整序列与状态)。 +// 方法与同步 IProjectRepository 一一对应;payload 类型见各方法注释(控制器 qvariant_cast)。 +class IAsyncProjectRepository { +public: + virtual ~IAsyncProjectRepository() = default; + virtual NavRequest* listWorkspaces() = 0; // std::vector + virtual NavRequest* switchWorkspace(const std::string& tenantId) = 0; // bool(解析器内 setToken 副作用) + virtual NavRequest* pageProjects(const std::string& nameFilter, + const std::string& typeId, int pageNo, int pageSize) = 0; // ProjectListPage + virtual NavRequest* listProjectTypes() = 0; // std::vector + virtual NavRequest* loadStructure(const std::string& projectId) = 0; // std::vector + virtual NavRequest* loadRows(const std::string& projectId, const std::string& parentId, + int parentConfType, int classifyType, int pageNo) = 0; // DsPage + virtual NavRequest* loadObjectDetail(const std::string& objectId, int confType) = 0; // DynamicForm + virtual NavRequest* loadDatasetForm(const std::string& dsObjectId) = 0; // DynamicForm + virtual NavRequest* loadExceptionsByTm(const std::string& tmObjectId) = 0; // std::vector +}; + +} // namespace geopro::data +``` + +- [ ] **Step 2: 构建(纯头新增,无消费者,验证编译)** + +Run: `powershell.exe -ExecutionPolicy Bypass -File scripts/dev-build.ps1` +Expected: 构建通过(未被引用,仅语法校验)。 + +- [ ] **Step 3: Commit** + +```bash +git add src/data/repo/IAsyncProjectRepository.hpp +git commit -m "feat(data): IAsyncProjectRepository 异步导航仓储抽象(薄封装,返回NavRequest)" +``` + +--- + +## Task A3: data — `ApiProjectRepository` 实现异步接口(过渡:新旧两接口并存) + +让 `ApiProjectRepository` **同时**实现 `IProjectRepository`(旧同步,保留)与 `IAsyncProjectRepository`(新异步)。构建仍绿、旧控制器路径不动。 + +**Files:** +- Modify: `src/data/api/ApiProjectRepository.hpp`, `src/data/api/ApiProjectRepository.cpp` + +- [ ] **Step 1: 改 `ApiProjectRepository.hpp`** — 加继承 + 9 个异步方法声明 + +```cpp +#pragma once +#include "repo/IProjectRepository.hpp" +#include "repo/IAsyncProjectRepository.hpp" + +namespace geopro::net { class ApiClient; } + +namespace geopro::data { + +// 用共享会话 ApiClient 实现导航仓储。过渡期同时实现同步(旧)+异步(新)两接口。 +class ApiProjectRepository : public IProjectRepository, public IAsyncProjectRepository { +public: + explicit ApiProjectRepository(net::ApiClient& api); + + // ── 同步(旧,A6 删除) ── + RepoResult> listWorkspaces() override; + /* ...其余 8 个同步方法签名保持不变(略,见现文件)... */ + + // ── 异步(新) ── 与 IAsyncProjectRepository 一致,返回 NavRequest* + NavRequest* listWorkspacesAsync(); // 见下注:消歧 + /* 注:同步 listWorkspaces() 与异步签名冲突(同名不同返回类型不能 override 两接口同名)。 + 解决:异步接口方法在 .hpp 用 IAsyncProjectRepository 的纯虚名(listWorkspaces 等), + 但同步接口也叫 listWorkspaces → C++ 同名隐藏。两接口同名方法返回类型不同,无法在同一类 + 共存(重载仅靠参数区分,返回类型不算)。**故 A3 改为:异步方法用不同名后缀消歧。** */ +}; + +} // namespace geopro::data +``` + +> **重要消歧(实现者必读):** `IProjectRepository::listWorkspaces()` 返回 `RepoResult<...>`,`IAsyncProjectRepository::listWorkspaces()` 返回 `NavRequest*`——**同名同参不同返回类型,C++ 不允许在同一类同时 override**。两条出路: +> - **出路 1(推荐,最干净):跳过过渡期双实现,A3/A4 合并为「直接改形」**:`ApiProjectRepository` 只实现 `IAsyncProjectRepository`(删同步继承),同批改控制器(A4+A5 合并提交,如决策 5 备选)。代价:A4 到 A5 之间一个提交内构建可能不绿——用「单次大提交」承接(详情试点 Task5+6 合并的同款做法)。 +> - **出路 2:异步方法加 `Async` 后缀**(`listWorkspacesAsync()` 等),`IAsyncProjectRepository` 接口方法名也带 `Async`。两接口可共存、过渡期构建绿、可分步提交。代价:方法名与同步版略有差异。 +> +> **本计划采用出路 2**(保证每步构建绿、可分散提交,符合「频繁提交」与决策 5)。下方 Step 按出路 2 给出:`IAsyncProjectRepository` 的方法名统一加 `Async` 后缀。**回到 Task A2 把接口方法名改为 `listWorkspacesAsync` 等**(A2 已提交则在本 Task 一并修正 + 重新提交 A2 头)。 + +- [ ] **Step 1b: 修正 `IAsyncProjectRepository.hpp` 方法名加 `Async` 后缀** + +把 A2 的 9 个方法改名:`listWorkspacesAsync` / `switchWorkspaceAsync` / `pageProjectsAsync` / `listProjectTypesAsync` / `loadStructureAsync` / `loadRowsAsync` / `loadObjectDetailAsync` / `loadDatasetFormAsync` / `loadExceptionsByTmAsync`(参数同前)。 + +- [ ] **Step 2: `ApiProjectRepository.hpp` 加 9 个 `...Async` 声明**(保留全部同步方法不动) + +```cpp + NavRequest* listWorkspacesAsync() override; + NavRequest* switchWorkspaceAsync(const std::string& tenantId) override; + NavRequest* pageProjectsAsync(const std::string& nameFilter, const std::string& typeId, + int pageNo, int pageSize) override; + NavRequest* listProjectTypesAsync() override; + NavRequest* loadStructureAsync(const std::string& projectId) override; + NavRequest* loadRowsAsync(const std::string& projectId, const std::string& parentId, + int parentConfType, int classifyType, int pageNo) override; + NavRequest* loadObjectDetailAsync(const std::string& objectId, int confType) override; + NavRequest* loadDatasetFormAsync(const std::string& dsObjectId) override; + NavRequest* loadExceptionsByTmAsync(const std::string& tmObjectId) override; +``` + +- [ ] **Step 3: 改 `ApiProjectRepository.cpp`** — 加异步实现(解析 lambda 复用现有 `dto::parseXxx`) + +在文件顶部 include 加: +```cpp +#include "ApiBatch.hpp" // 仅若需要;本 repo 薄封装不直接用 batch(控制器用) +#include "api/NavRequest.hpp" +#include "api/NavLoads.hpp" +``` +匿名命名空间内已有 `enc`/`ok`/`errorOf`;新增异步失败谓词(与同步 `ok` 同口径): +```cpp +bool isFailureA(const net::ApiResponse& r) { return r.code != kCodeSuccess || !r.rawError.isEmpty(); } +``` +在文件末尾(同步方法之后)加 9 个异步实现,例如: +```cpp +NavRequest* ApiProjectRepository::listWorkspacesAsync() { + auto* call = api_.getAsync(QStringLiteral("/business/system/tenant/enterprise/joined/list")); + return new ApiNavRequest(call, [](const net::ApiResponse& r) { + return QVariant::fromValue(dto::parseWorkspaces(r.data.value(QStringLiteral("value")).toArray())); + }, &isFailureA); +} + +NavRequest* ApiProjectRepository::switchWorkspaceAsync(const std::string& tenantId) { + const QString path = + QStringLiteral("/business/system/tenant/enterprise/switch/%1").arg(enc(tenantId)); + auto* call = api_.postJsonAsync(path, QJsonObject{}); + // 解析器内执行 setToken 副作用(与同步版一致):切空间返回新 accessToken 必须重注入。 + return new ApiNavRequest(call, [this](const net::ApiResponse& r) { + const QString token = r.data.value(QStringLiteral("accessToken")).toString(); + if (!token.isEmpty()) api_.setToken(token); + return QVariant::fromValue(true); + }, &isFailureA); +} + +NavRequest* ApiProjectRepository::pageProjectsAsync(const std::string& nameFilter, + const std::string& typeId, int pageNo, int pageSize) { + QJsonObject body{{QStringLiteral("projectName"), QString::fromStdString(nameFilter)}, + {QStringLiteral("pageNo"), pageNo}, + {QStringLiteral("pageSize"), pageSize}}; + if (!typeId.empty()) body[QStringLiteral("projectTypeId")] = QString::fromStdString(typeId); + auto* call = api_.postJsonAsync(QStringLiteral("/business/my/profile/project/page"), body); + return new ApiNavRequest(call, [](const net::ApiResponse& r) { + return QVariant::fromValue(dto::parseProjectPage(r.data)); + }, &isFailureA); +} + +NavRequest* ApiProjectRepository::loadStructureAsync(const std::string& projectId) { + const QString path = + QStringLiteral("/business/projectStruct/queryProjectStruct/%1").arg(enc(projectId)); + auto* call = api_.getAsync(path); + return new ApiNavRequest(call, [](const net::ApiResponse& r) { + return QVariant::fromValue(dto::parseStructNodes(r.data.value(QStringLiteral("value")).toArray())); + }, &isFailureA); +} + +NavRequest* ApiProjectRepository::loadRowsAsync(const std::string& projectId, const std::string& parentId, + int parentConfType, int classifyType, int pageNo) { + const QString path = (classifyType == 1) ? QStringLiteral("/business/dsObject/file/page") + : QStringLiteral("/business/dsObject/data/page"); + const QJsonObject body{ + {QStringLiteral("projectId"), QString::fromStdString(projectId)}, + {QStringLiteral("structParentId"), QString::fromStdString(parentId)}, + {QStringLiteral("structParentConfType"), parentConfType}, + {QStringLiteral("classifyTypeList"), QJsonArray{classifyType}}, + {QStringLiteral("pageNo"), pageNo}, + {QStringLiteral("pageSize"), 5}}; + auto* call = api_.postJsonAsync(path, body); + return new ApiNavRequest(call, [](const net::ApiResponse& r) { + return QVariant::fromValue(dto::parseDsPage(r.data)); + }, &isFailureA); +} + +NavRequest* ApiProjectRepository::loadObjectDetailAsync(const std::string& objectId, int confType) { + const QString path = + (confType == 1) ? QStringLiteral("/business/gsObject/getGsObjectDetail/%1").arg(enc(objectId)) + : QStringLiteral("/business/tmObject/getDetail/%1").arg(enc(objectId)); + auto* call = api_.getAsync(path); + return new ApiNavRequest(call, [](const net::ApiResponse& r) { + return QVariant::fromValue(dto::parseDynamicForm(r.data)); + }, &isFailureA); +} + +NavRequest* ApiProjectRepository::loadDatasetFormAsync(const std::string& dsObjectId) { + const QString path = QStringLiteral("/business/dsObject/dynamicForm/%1").arg(enc(dsObjectId)); + auto* call = api_.getAsync(path); + return new ApiNavRequest(call, [](const net::ApiResponse& r) { + return QVariant::fromValue(dto::parseDynamicForm(r.data)); + }, &isFailureA); +} + +NavRequest* ApiProjectRepository::listProjectTypesAsync() { + auto* call = api_.getAsync(QStringLiteral("/business/project/type/list")); + return new ApiNavRequest(call, [](const net::ApiResponse& r) { + return QVariant::fromValue(dto::parseProjectTypes(r.data.value(QStringLiteral("value")).toArray())); + }, &isFailureA); +} + +NavRequest* ApiProjectRepository::loadExceptionsByTmAsync(const std::string& tmObjectId) { + const QString path = + QStringLiteral("/business/exception/queryExceptionByTmObjectId/%1").arg(enc(tmObjectId)); + auto* call = api_.getAsync(path); + return new ApiNavRequest(call, [](const net::ApiResponse& r) { + return QVariant::fromValue(dto::parseExceptions(r.data.value(QStringLiteral("value")).toArray())); + }, &isFailureA); +} +``` +> 解析函数名与现同步实现逐一对齐:`parseWorkspaces`/`parseProjectPage`/`parseProjectTypes`/`parseStructNodes`/`parseDsPage`/`parseDynamicForm`/`parseExceptions`(见现 `ApiProjectRepository.cpp`)。URL/body 构造原样搬,不重写。 + +- [ ] **Step 4: 构建(异步方法离线不可单测真实端点,验证编译/链接/moc)** + +Run: `powershell.exe -ExecutionPolicy Bypass -File scripts/dev-build.ps1` +Expected: 链接通过。 +> 真实端点行为由 Task A6 手动验证覆盖(与现 ApiProjectRepository 同:无离线单测)。 + +- [ ] **Step 5: Commit** + +```bash +git add src/data/repo/IAsyncProjectRepository.hpp src/data/api/ApiProjectRepository.hpp src/data/api/ApiProjectRepository.cpp +git commit -m "feat(data): ApiProjectRepository 实现 IAsyncProjectRepository(9方法,Async后缀,薄封装,新旧并存)" +``` + +--- + +## Task A4: controller — `WorkbenchNavController` 异步化(abort-and-replace + ApiChain/ApiBatch 编排 + 删 busy_/drain)+ 单测(TDD) + +> 本 Task 体量大(控制器是难点)。**Step 1(写控制器单测,先确立目标行为)→ Step 2/3(改控制器头/源)→ Step 4(跑测试)必须连续完成、同一提交**(接口消费切换是原子改动,见决策 5)。同步 `IProjectRepository` 路径在本 Task 后无消费者(A6 删除)。 + +**Files:** +- Create: `tests/controller/test_workbench_nav_controller.cpp` +- Modify: `src/controller/WorkbenchNavController.hpp`, `src/controller/WorkbenchNavController.cpp`, `tests/CMakeLists.txt` + +- [ ] **Step 1: 写控制器单测 `tests/controller/test_workbench_nav_controller.cpp`**(Stub 异步 repo + QSignalSpy) + +桩策略:定义一个 `StubNavRequest`(不声明 Q_OBJECT,发射继承自 `NavRequest` 的 `done/failed`、override `abort` 记录),`StubAsyncRepo` 实现 `IAsyncProjectRepository`,每方法 `new StubNavRequest` 并记录最近一个(按方法名分桶),测试可手动 `fireDone(QVariant)`/`fireFailed()`/查 `aborted`。 + +```cpp +#include +#include +#include +#include "WorkbenchNavController.hpp" +#include "repo/IAsyncProjectRepository.hpp" +#include "api/NavRequest.hpp" +#include "api/NavLoads.hpp" +using namespace geopro; + +namespace { +struct StubNavRequest : data::NavRequest { + bool aborted = false; + void abort() override { aborted = true; } + void fireDone(const QVariant& v) { emit done(v); } + void fireFailed() { emit failed(QStringLiteral("x")); } +}; +struct StubAsyncRepo : data::IAsyncProjectRepository { + StubNavRequest* lastWorkspaces = nullptr; + StubNavRequest* lastProjects = nullptr; + StubNavRequest* lastStructure = nullptr; + StubNavRequest* lastExceptions = nullptr; + // ...其余按需 + data::NavRequest* listWorkspacesAsync() override { return lastWorkspaces = new StubNavRequest; } + data::NavRequest* pageProjectsAsync(const std::string&, const std::string&, int, int) override { + return lastProjects = new StubNavRequest; } + data::NavRequest* loadStructureAsync(const std::string&) override { + return lastStructure = new StubNavRequest; } + data::NavRequest* loadExceptionsByTmAsync(const std::string&) override { + return lastExceptions = new StubNavRequest; } + data::NavRequest* switchWorkspaceAsync(const std::string&) override { return new StubNavRequest; } + data::NavRequest* listProjectTypesAsync() override { return new StubNavRequest; } + data::NavRequest* loadRowsAsync(const std::string&, const std::string&, int, int, int) override { + return new StubNavRequest; } + data::NavRequest* loadObjectDetailAsync(const std::string&, int) override { return new StubNavRequest; } + data::NavRequest* loadDatasetFormAsync(const std::string&) override { return new StubNavRequest; } +}; +} + +// start() 依赖链:workspaces → projects → structure,逐级 emit 既有信号。 +TEST(WorkbenchNavController, StartChainEmitsWorkspacesThenProjectsThenStructure) { + StubAsyncRepo repo; + controller::WorkbenchNavController c(repo); + QSignalSpy wsSpy(&c, &controller::WorkbenchNavController::workspacesLoaded); + QSignalSpy psSpy(&c, &controller::WorkbenchNavController::projectsLoaded); + QSignalSpy stSpy(&c, &controller::WorkbenchNavController::structureLoaded); + c.start(); + repo.lastWorkspaces->fireDone(QVariant::fromValue(std::vector{{"w1","WS",2,true}})); + EXPECT_EQ(wsSpy.count(), 1); + repo.lastProjects->fireDone(QVariant::fromValue(data::ProjectListPage{{{"p1","P1"}}, 1})); + EXPECT_EQ(psSpy.count(), 1); + repo.lastStructure->fireDone(QVariant::fromValue(std::vector{})); + EXPECT_EQ(stSpy.count(), 1); +} + +// busyChanged 反映在飞:发起→true,最后完成→false。 +TEST(WorkbenchNavController, BusyChangedReflectsInflight) { + StubAsyncRepo repo; + controller::WorkbenchNavController c(repo); + QSignalSpy busySpy(&c, &controller::WorkbenchNavController::busyChanged); + c.start(); + ASSERT_GE(busySpy.count(), 1); + EXPECT_TRUE(busySpy.takeFirst().at(0).toBool()); // 首次 true + repo.lastWorkspaces->fireDone(QVariant::fromValue(std::vector{{"w1","WS",2,true}})); + repo.lastProjects->fireDone(QVariant::fromValue(data::ProjectListPage{{{"p1","P1"}}, 1})); + repo.lastStructure->fireDone(QVariant::fromValue(std::vector{})); + EXPECT_FALSE(busySpy.last().at(0).toBool()); // 末尾 false +} + +// setCheckedTms:新勾选 abort 旧异常批(以最后一次为准)。 +TEST(WorkbenchNavController, SetCheckedTmsAbortsPreviousBatch) { + StubAsyncRepo repo; + controller::WorkbenchNavController c(repo); + c.setCheckedTms({"tmA"}); + StubNavRequest* a = repo.lastExceptions; + c.setCheckedTms({"tmB"}); // 覆盖 + EXPECT_TRUE(a->aborted); +} + +// 回灌防护:abort 后旧句柄迟到 done 被身份比对丢弃。 +TEST(WorkbenchNavController, DropsLateStructureAfterProjectSwitch) { + StubAsyncRepo repo; + controller::WorkbenchNavController c(repo); + QSignalSpy stSpy(&c, &controller::WorkbenchNavController::structureLoaded); + c.switchProject("pA"); + StubNavRequest* a = repo.lastStructure; + c.switchProject("pB"); + StubNavRequest* b = repo.lastStructure; + a->fireDone(QVariant::fromValue(std::vector{})); // 旧 → 丢弃 + EXPECT_EQ(stSpy.count(), 0); + b->fireDone(QVariant::fromValue(std::vector{})); // 新 → 正常 + EXPECT_EQ(stSpy.count(), 1); +} +``` +> 实现者注:测试里 `Workspace{"w1","WS",2,true}` 字段顺序须对齐 `RepoTypes.hpp`(`id,name,ownerType,isCurrent`);`ProjectSummary`/`StructNode` 等聚合初始化按其声明顺序。`switchProject` 单请求路径用 `loadStructureAsync`。selectObject 的并发(ApiBatch)测试可加 `SelectObjectConcurrentBatch`(三 stub 全 done 后一次性 emit datasets/files/objectDetail)——列为补充用例。 + +- [ ] **Step 2: 改 `WorkbenchNavController.hpp`** + +要点:① 改依赖 `IAsyncProjectRepository&`;② 删 `busy_`/`checkedTmsPending_`/`pendingCheckedTms_`/`drainPendingCheckedTms`/`friend struct BusyGuard`;③ 加每路径 `QPointer` 在飞句柄成员 + `ApiChain`/`ApiBatch` 的 `QPointer`;④ 加 `emitBusyIfChanged()` 私有辅助 + `bool lastBusy_`;⑤ 信号面不变(`busyChanged` 语义重定义为「有在飞」)。 + +```cpp +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include "repo/RepoTypes.hpp" + +namespace geopro::net { class ApiChain; class ApiBatch; } +namespace geopro::data { class IAsyncProjectRepository; class NavRequest; } + +namespace geopro::controller { + +class WorkbenchNavController : public QObject { + Q_OBJECT +public: + explicit WorkbenchNavController(data::IAsyncProjectRepository& repo, QObject* parent = nullptr); + void start(); + QString currentCrsCode() const { return QString::fromStdString(currentCrsCode_); } + +public slots: + void switchWorkspace(const QString& tenantId); + void switchProject(const QString& projectId); + void selectObject(const QString& objectId, int confType); + void setCheckedTms(const QStringList& tmObjectIds); + void selectDataset(const QString& dsObjectId); + void loadMoreData(); + void loadMoreFiles(); + +signals: + void busyChanged(bool busy); + void workspacesLoaded(const std::vector& list, const QString& currentId); + void projectsLoaded(const std::vector& list, const QString& currentId, int total); + void structureLoaded(const QString& projectName, const std::vector& nodes); + void datasetsLoaded(const QString& tmObjectId, const std::vector& rows, int total, bool append); + void filesLoaded(const QString& tmObjectId, const std::vector& rows, int total, bool append); + void objectDetailLoaded(const QString& title, const geopro::data::DynamicForm& form); + void exceptionTreeLoaded(const std::vector& groups, int exceptionCount); + void datasetDetailLoaded(const geopro::data::DynamicForm& form); + void loadFailed(const QString& stage, const QString& message); + +private: + void emitBusyIfChanged(); // 据「是否存在任一在飞句柄」去抖发 busyChanged + bool anyInflight() const; // OR 所有在飞 QPointer + + data::IAsyncProjectRepository& repo_; + bool lastBusy_ = false; + + // 在飞句柄(QPointer 防悬垂;身份比对用): + QPointer startChain_; // start / switchWorkspace 依赖链 + QPointer structReq_; // switchProject + QPointer selectBatch_; // selectObject 三并发 + QPointer moreDataReq_; + QPointer moreFilesReq_; + QPointer datasetReq_; + QPointer exceptionsBatch_; // setCheckedTms + + std::vector lastProjects_; + std::string currentWorkspaceId_, currentProjectId_, currentProjectName_, currentCrsCode_; + std::string currentParentId_; + int currentParentConfType_ = 0; + std::vector lastStructNodes_; + std::map> tmExceptionCache_; + int dataPageNo_ = 0, filePageNo_ = 0, dataTotal_ = 0, fileTotal_ = 0; +}; + +} // namespace geopro::controller +``` + +- [ ] **Step 3: 改 `WorkbenchNavController.cpp`**(核心编排,分块写) + +设计要点(每块对照现同步实现 1:1 搬状态更新逻辑,只把「同步取值」换成「句柄 done lambda」): + +1. **`start()` / `switchWorkspace()` 用 `ApiChain`:** + - `start()`: abort 旧 `startChain_`;建 chain `{ listWorkspacesAsync 工厂, pageProjectsAsync 工厂, loadStructureAsync 工厂 }`。但 repo 是「一方法一请求」的 `NavRequest`,而 `ApiChain` 步骤要的是 `IApiCall*`。**桥接:** `ApiChain` 步骤工厂层级要 `IApiCall`,但导航链的「依赖」体现在「用上一步解析后的业务值(如 currentWorkspaceId、首个 projectId)构造下一请求」,而非透传 `ApiResponse`。两种实现路线: + - **路线 X(推荐,KISS):不用 `ApiChain` 串 repo 句柄,改用「NavRequest done lambda 里发起下一 NavRequest」的续延**——即在控制器内手写链:`listWorkspacesAsync().done → 算 currentId → pageProjectsAsync().done → 算首个 project → loadStructureAsync().done → emit`。每级 abort-and-replace 由 `startChain_` 改为一个「当前在飞 NavRequest」的 QPointer 链(用单一 `QPointer startStepReq_` 跟踪当前级,abort 它即 abort 整条链的当前级)。`aborted_` 闸门由 NavRequest 自身 + 控制器在每级 done 里比对 `sender()/捕获指针 == 当前级` 提供。 + - 路线 Y:`ApiChain`(透传 ApiResponse),但导航链需要业务解析后的值(不只是 raw response)构造下一请求——`ApiChain` 工厂入参是 `QList`,可在工厂内重新解析(DTO 解析幂等、廉价),可行但重复解析。 + - **采用路线 X**:`ApiChain` 原语**保留供登录用**(登录链透传 response 直接喂下一请求,天然契合);导航依赖链用「NavRequest 续延」在控制器内编排(导航需要业务值串联,续延更直观)。**因此 `startChain_` 成员改为 `QPointer startStepReq_`**(跟踪 start/switchWorkspace 链当前在飞级)。 + > 决策修订记录(self-review 复核):决策 1 说「导航依赖链用 ApiChain」,实测代码后修订为「导航依赖链用 NavRequest 续延(因需业务值串联)、ApiChain 专供登录(透传 response 串联)」。`ApiChain` 仍在 A0 落地并测试,Part B 登录使用。 + +2. **`switchProject()` 用单 `NavRequest`(`loadStructureAsync`):** abort 旧 `structReq_`;存新;done lambda 内身份比对 + 1:1 搬现状态更新(`currentProjectId_`/`currentProjectName_`/`currentCrsCode_` from `lastProjects_`、`tmExceptionCache_.clear()`、重置选中态)+ emit `structureLoaded`。 + +3. **`selectObject()` 用 `ApiBatch`(三并发:loadRows data + loadRows file + loadObjectDetail):** + - abort 旧 `selectBatch_`;用 `api`?——控制器没有 `ApiClient`,只有 repo。**桥接 ApiBatch 需要 `IApiCall*`,而 repo 给的是 `NavRequest`。** 解决:`selectObject` 不用 `ApiBatch`(它要 IApiCall);改用「三个独立 `NavRequest` + 控制器内计数汇聚」或顺序续延。**KISS 选择:** 保持现「顺序」语义但异步化——data done → file done → detail done → 全到齐前不算完成;或真并发——三 `NavRequest` 并行、各自 done,控制器维护 `remaining` 计数,全到齐后一次性 emit(与现 emit 三信号一致)。**推荐并发计数**(三请求独立,无依赖;缩短等待):成员加 `QPointer selDataReq_/selFileReq_/selDetailReq_` + `int selRemaining_`,三者 done 各自比对身份、填暂存、`--selRemaining_==0` 后 emit `datasetsLoaded`+`filesLoaded`+`objectDetailLoaded`。失败任一 → emit `loadFailed` + abort 其余。 + > 这复刻了 `ApiBatch` 的能力但作用于 `NavRequest`——是否值得为此再造?**判断:** `ApiBatch` 作用于 `IApiCall`,`selectObject` 需要的是「三个 typed NavRequest 的汇聚」。可以加一个轻量 `NavBatch`(汇聚 N 个 `NavRequest`,全 done → succeeded(QList);任一 failed → fail + abort)。**但 selectObject 是唯一的导航并发汇聚点**(setCheckedTms 见下,其实是「N 个相同类型请求 + 缓存」),YAGNI 下先用控制器内计数(~15 行);若 setCheckedTms 也要并发汇聚,则提炼 `NavBatch`。**本计划:selectObject 用控制器内三句柄计数(不新增原语)。** + +4. **`loadMoreData()` / `loadMoreFiles()` / `selectDataset()` 用单 `NavRequest`:** abort 旧、存新、done 身份比对 + emit(append=true / datasetDetailLoaded)。删 `currentParentId_.empty()` 之外的 busy 守卫。 + +5. **`setCheckedTms()` 异步化(去 busy/pending/drain):** + - abort 旧 `exceptionsBatch_`(或续延句柄)。 + - 命中缓存的 TM 直接组装;未命中的 N 个 `loadExceptionsByTmAsync` **并发**、计数汇聚(同 selectObject 计数模式),全到齐后写缓存 + `groupExceptionsByConsortium` 组装 + emit `exceptionTreeLoaded`。 + - **以最后一次勾选为准**:新 `setCheckedTms` abort 旧的在飞集(abort-and-replace),无需 pending 重放。 + - 若全部命中缓存(无在飞请求):同步组装后直接 emit(busyChanged 不抖动)。 + +6. **`busyChanged`:** 每次「发起请求后」与「句柄 done/failed 清空后」调 `emitBusyIfChanged()`: +```cpp +bool WorkbenchNavController::anyInflight() const { + return startStepReq_ || structReq_ || selDataReq_ || selFileReq_ || selDetailReq_ || + moreDataReq_ || moreFilesReq_ || datasetReq_ || !checkedInflight_.isEmpty(); +} +void WorkbenchNavController::emitBusyIfChanged() { + const bool now = anyInflight(); + if (now != lastBusy_) { lastBusy_ = now; emit busyChanged(now); } +} +``` + +> 实现者:本 Step 是全计划最大改动。建议在 subagent 内分 6 个子提交(start链 / switchProject / selectObject / loadMore+selectDataset / setCheckedTms / busyChanged 收口),但**首个能编译通过的提交必须已切换 ctor 到 `IAsyncProjectRepository`**(否则与 main.cpp 装配不一致)。若分子提交导致中间不可编译,则合并为单提交(接口消费切换原子性,决策 5)。 + +- [ ] **Step 4: 改 `WorkbenchNavController.cpp` include + 注册测试 + 跑** + +cpp 顶部 include 改为: +```cpp +#include "WorkbenchNavController.hpp" +#include "repo/IAsyncProjectRepository.hpp" +#include "api/NavRequest.hpp" +#include "api/NavLoads.hpp" +#include "ApiBatch.hpp" // 若 selectObject 最终用 NavBatch/ApiBatch;本计划用计数则不需要 +#include "dto/NavDto.hpp" +``` +`tests/CMakeLists.txt` controller 段(`controller/test_dataset_detail_controller.cpp` 后)加: +```cmake +target_sources(geopro_tests PRIVATE controller/test_workbench_nav_controller.cpp) +``` +Run: `powershell.exe -ExecutionPolicy Bypass -File scripts/dev-test.ps1` +Expected: `WorkbenchNavController.*` PASS(≥4 个);总 ~100/100。 +> ⚠️ 此时 main.cpp 仍把 `ApiProjectRepository`(实现两接口)作 `IProjectRepository&` 传给 `buildWorkbench`/`nav`——但 `nav` ctor 已改 `IAsyncProjectRepository&`。**Step 4 跑测试前不构建 app 也可**(dev-build 构建全部);若 app 编译失败,须同批做 A5(main.cpp 装配)。**建议 A4 与 A5 连续执行、A5 完成后再要求全绿。** + +- [ ] **Step 5: Commit(A4)** + +```bash +git add src/controller/WorkbenchNavController.hpp src/controller/WorkbenchNavController.cpp tests/controller/test_workbench_nav_controller.cpp tests/CMakeLists.txt +git commit -m "feat(controller): WorkbenchNavController 异步化(NavRequest续延+并发计数, abort-and-replace+身份比对, 删busy_/drain/BusyGuard, busyChanged=在飞存在性) + 单测" +``` + +--- + +## Task A5: app — 装配切换到异步导航仓储(与 A4 连续) + +**Files:** +- Modify: `src/app/main.cpp`(必要时 `ProjectListDialog` 的 repo 形参类型) + +- [ ] **Step 1: 核对 `nav` 构造与 `buildWorkbench` 形参** + +`main.cpp:858-859`:`ApiProjectRepository projectRepo(api); WorkbenchNavController nav(projectRepo);`——`projectRepo` 现同时是 `IProjectRepository` 与 `IAsyncProjectRepository`,`nav` ctor 取 `IAsyncProjectRepository&`,**引用绑定自动选对基类,无需改**。 +`buildWorkbench(...)` 形参 `geopro::data::IProjectRepository& projectRepo`(:195)仍用于 `ProjectListDialog`(:655 `new ProjectListDialog(projectRepo, ...)`)——`ProjectListDialog` 仍用同步接口(它自己分页查询)。**本阶段不动 ProjectListDialog**(同步接口 A6 才删;ProjectListDialog 是同步接口最后消费者之一,A6 一并迁移或保留同步)。 + +- [ ] **Step 2: 确认 nav 信号接线不变** + +`main.cpp:637-751` 所有 `nav` 信号接线(workspacesLoaded/projectsLoaded/structureLoaded/datasetsLoaded/filesLoaded/objectDetailLoaded/exceptionTreeLoaded/datasetDetailLoaded/loadFailed/busyChanged)**信号面未变,全部不动**。`nav.start()`(:885)不变。 + +- [ ] **Step 3: 构建 + 全量测试** + +Run: `powershell.exe -ExecutionPolicy Bypass -File scripts/dev-build.ps1` 然后 `... dev-test.ps1` +Expected: 全绿 ~100/100;构建出 `geopro_desktop.exe`。 + +- [ ] **Step 4: 手动验证(核心收益)** + +启动 app 登录后: +- 切换工作空间/项目 → 结构树加载期 **UI 不冻**。 +- 单击对象 → 数据/文件/详情**并发**加载,不冻。 +- 快速连点不同对象 → 旧请求 abort、无错位回灌、无崩溃。 +- 快速改勾选 TM → 异常树以最后一次勾选为准(旧批 abort)。 +- 加载期等待光标(busyChanged)正常出现/消失。 + +- [ ] **Step 5: Commit** + +```bash +git add src/app/main.cpp +git commit -m "feat(app): 装配 WorkbenchNavController 异步导航仓储(引用绑定切换 IAsyncProjectRepository)" +``` + +--- + +## Task A6: cleanup — 删除导航同步接口/方法(若无其他消费者) + +> 仅当 `ProjectListDialog` 等同步 `IProjectRepository` 消费者已迁移或确认保留时执行。**先 grep 确认消费者**。 + +**Files:** +- Modify: `src/data/repo/IProjectRepository.hpp`, `src/data/api/ApiProjectRepository.{hpp,cpp}`, `src/app/...`(ProjectListDialog 若迁移) + +- [ ] **Step 1: 审计同步 `IProjectRepository` 消费者** + +```bash +grep -rn "IProjectRepository\|RepoResult\|pageProjects\b\|listProjectTypes\b" src/ tests/ +``` +确认除 `ApiProjectRepository` 自身外的消费者(预期:`ProjectListDialog`、`buildWorkbench` 形参、可能的测试)。 + +- [ ] **Step 2: 决策保留 or 迁移 ProjectListDialog** + +若 `ProjectListDialog` 是唯一剩余同步消费者:评估迁移到 `pageProjectsAsync`/`listProjectTypesAsync`(它有自己的分页/类型查询 UI)。若改造成本高、收益低(弹窗短时阻塞可接受),**保留同步接口**并在本 Task 仅删「导航控制器已不用的同步方法」中确无消费者的——**KISS:若 ProjectListDialog 仍用同步,则不删 IProjectRepository,本 Task 跳过删除、仅记录技术债。** + +- [ ] **Step 3(仅当可删): 删同步接口 + 实现 + 继承** + +`ApiProjectRepository` 去掉 `public IProjectRepository`,删 9 个同步方法实现;删 `IProjectRepository.hpp`(若全无消费者);`buildWorkbench` 形参与 ProjectListDialog 改异步。 + +- [ ] **Step 4: 构建 + 全量测试 + Commit** + +```bash +git add -A +git commit -m "refactor(data): 删除导航同步 IProjectRepository(无消费者) / 或记录 ProjectListDialog 技术债" +``` + +--- + +# Part B — 登录(高层任务骨架) + +> 在 Part A 落地、`ApiChain` 经导航/单测验证稳定后展开。登录是**串行依赖链 + 共享会话 + 模态 `exec()`**,风险集中。建议另起 plan `2026-06-11-apiclient-async-login.md` 细化为 bite-sized,或在本文件续写。以下为高层任务骨架与关键设计。 + +## Task B0: net — `AuthService` 异步改形设计确认(无代码,确认信号面) + +- `fetchCaptcha()` → 异步:返回 `NavRequest*`(payload = `Captcha{codeId,code}` via QVariant,需 `Q_DECLARE_METATYPE(AuthService::Captcha)`)或专用 `CaptchaLoad` 句柄。单请求,用 `NavRequest`/单句柄即可。 +- `login(user,pwd,code,codeId)` → 异步:返回 `LoginLoad*` 句柄(done(token) / failed(msg)),内部用 **`ApiChain`** 编排:step1 = `verifyCodeCheck`(POST,工厂用入参 code/codeId),step2 = `login2`(POST,工厂内先 **RSA 加密**(可抛 → ApiChain 转 failed)再构造 body)。末步 succeeded → 取 `accessToken` → done(token)。 +- **共享会话不变量**:verify 与 login2 经同一 `ApiClient`(同一 NAM),JSESSIONID 串联——`ApiChain` 各 step 工厂调 `api_.postJsonAsync`,与现同步链一致(`ApiClient.hpp:24-27`)。 +- 失败谓词 = `code != 200 || !rawError.isEmpty()`(同 §7,复用)。错误文案:`msg` 空回退 `rawError`,RSA 失败给专门文案(在 login2 工厂 catch 内 throw 带文案的 exception,ApiChain 工厂 catch 写入 rawError)。 + +## Task B1: net — `AuthService` 异步实现 + 离线单测(TDD) + +- 新句柄 `CaptchaLoad`/`LoginLoad`(或复用 `NavRequest`);`AuthService` 改用 `getAsync/postJsonAsync` + `ApiChain`。 +- 离线单测:用 FakeApiCall 注入 ApiChain(B 需要把 ApiChain 暴露为可注入或测 AuthService 经 stub ApiClient——评估:AuthService 直接持 `ApiClient&`,离线测较难;可加 `IApiClient` 缝或测 ApiChain 层已覆盖链逻辑,AuthService 仅薄编排,留 live 测)。**推荐**:AuthService 薄编排不强求离线单测,链逻辑由 `test_api_chain.cpp` 覆盖,端到端由改造后的 live 测覆盖。 +- 删 `AuthService` 同步 `fetchCaptcha`/`login`(或保留过渡,B3 删)。 + +## Task B2: app — `LoginWindow` 接异步信号(不冻 + 可取消) + +- `refreshCaptcha()`:调异步 `fetchCaptcha`,连 done → 重绘图、failed → showError;发起期禁用刷新按钮。 +- `attemptLogin()`:调异步 `login`,连 done(token) → `token_=...; accept()`;failed → showError + refreshCaptcha;发起期禁用登录按钮 + 文案「登录中…」(**去掉 `repaint()` 同步 hack**,异步天然不冻)。 +- **取消语义**:窗口关闭/`reject()` 时 abort 在飞 captcha/login 句柄(退出契约);持 `QPointer<...Load>` 成员。 +- 模态 `exec()` 仍可用:异步信号在 `exec()` 的事件循环内正常派发(QDialog::exec 跑嵌套事件循环,句柄 finished 经其泵出)。`accept()` 退出循环。 + +## Task B3: net/app — 删除 `ApiClient` 同步 `get/postJson`(最终清理) + +- 登录是同步方法最后消费者(导航 Part A 已异步,详情试点已异步)。grep 确认无剩余 `\.get(\|\.postJson(` 同步调用。 +- 删 `ApiClient::get/postJson` + `Impl::await` + `QEventLoop` include。`ApiResponseParse::buildResponse` 仅异步用,保留。 + +## Task B4: tests — `test_auth.cpp` live 改造为异步等待 + +- 现 `test_auth.cpp` 用同步 `fetchCaptcha()`/`login()`。改为:连句柄信号 + `QSignalSpy spy; spy.wait(10000)` 等异步完成;断言 token。仍标记 live(联网)。 +- `QCoreApplication` 事件循环已有(现测试已构造)。 + +## Task B5: app — 手动验证 + 收口 + +- 登录期 UI 不冻、可关窗取消;验证码异步刷新;登录失败异步报错 + 刷新验证码。 +- 全量 dev-test 绿;记忆登录路径(recallValidToken)不受影响(不经登录窗)。 + +--- + +## Self-Review(覆盖核对 + 类型一致性) + +### 覆盖核对(对照 prompt 规划要点) + +- **要点 1(登录串行链原语)** → `ApiChain`(Task A0,离线单测 4 例);登录 verify→RSA→login2 用之(Task B0/B1);RSA 在 login2 工厂内同步完成、工厂可抛转 failed;共享会话经同一 ApiClient/NAM(B0 不变量)。✓ +- **要点 2(导航分类)** → 决策 2 表:start/switchWorkspace=依赖链、switchProject/loadMore*/selectDataset=单请求、selectObject/setCheckedTms=可并发。Task A4 分别落地。✓ +- **要点 3(busy_/drain 演化)** → 决策 4:删 busy_/BusyGuard/drain/pending/friend;busyChanged 重定义为「在飞存在性」;setCheckedTms「最后一次为准」由 abort-and-replace 承接(非 TBD)。验证用例 BusyChangedReflectsInflight / SetCheckedTmsAbortsPreviousBatch。✓ +- **要点 4(RepoResult 异步化)** → 决策 3:单非模板 `NavRequest`(QVariant payload)+ `ApiNavRequest`,控制类爆炸;抽象基+Api实现+stub 可测(Task A1 单测)。与详情 ChartLoad/GridLoad 风格差异已说明理由。✓ +- **要点 5(测试)** → 复用 `tests/net/FakeApiCall.hpp`;新增 test_api_chain / test_nav_request / test_workbench_nav_controller(导航控制器原无单测);test_auth.cpp live 改造(Task B4)。✓ +- **要点 6(迁移顺序/可构建性)** → 决策 5 + Task A3 出路 2(Async 后缀,新旧并存,每步绿)+ A4/A5 连续/原子提交标注。✓ +- **要点 7(构建/测试命令)** → header 给出 dev-build/dev-test、LNK1104 处理、dev-test 不构建须先 dev-build、基线 89/89。✓ +- **要点 8(阶段/拆 plan)** → 顶部「阶段划分」:Part A 全 bite-sized、Part B 骨架;推荐拆两份 plan/两 PR;先导航(风险可分散)后登录(风险集中)。✓ + +### 关键修订(实测代码后对初始决策的修正,已在正文标注) + +- **决策 1 vs Task A4 路线 X:** 导航依赖链最终用「NavRequest 续延」而非 `ApiChain`(导航需业务解析值串联,续延更直观;ApiChain 工厂只透传 ApiResponse)。`ApiChain` 仍在 A0 落地+测试,**专供登录**(登录链透传 response 直接喂下一请求,天然契合)。此修订在 A4 Step 3 块 1 显式记录。 +- **selectObject/setCheckedTms 并发:** 不强行复用 `ApiBatch`(它作用于 IApiCall,而控制器只有 NavRequest),用控制器内「多 NavRequest + remaining 计数」(YAGNI,~15 行/路径);若未来更多并发汇聚点再提炼 `NavBatch`。在 A4 Step 3 块 3 记录判断。 + +### 类型一致性核对 + +- `ApiChain::StepFactory = function&)>`、`succeeded(QList)`/`failed(int,ApiResponse)` — 与 `ApiBatch` 信号面对称。✓ +- `NavRequest::done(QVariant)`/`failed(QString)`、`ApiNavRequest(IApiCall*, Parser=function, Predicate)` — 控制器 `qvariant_cast`。✓ +- `IAsyncProjectRepository` 9 方法均返回 `NavRequest*`,名带 `Async` 后缀(与同步 `IProjectRepository` 共存)。✓ +- 各 payload 类型 `Q_DECLARE_METATYPE`(NavLoads.hpp):`vector`/`ProjectListPage`/`vector`/`vector`/`DsPage`/`DynamicForm`/`vector`;`bool` 内置。同线程直连无需 `qRegisterMetaType`。✓ +- 控制器对外信号面(workspacesLoaded/.../loadFailed/busyChanged)**全程不变**,main.cpp 接线零改动(A5 Step 2)。✓ +- 解析函数名对齐现 `ApiProjectRepository.cpp`:parseWorkspaces/parseProjectPage/parseProjectTypes/parseStructNodes/parseDsPage/parseDynamicForm/parseExceptions。✓ +- 安全不变量(spec §5.0):ApiChain/ApiNavRequest 各持 `aborted_` 入口守卫 + 控制器句柄身份比对 + 一律 deleteLater。回归用例:ApiChain.AbortGate / NavRequest.AbortSuppressesLateDone / WorkbenchNavController.DropsLateStructureAfterProjectSwitch。✓ + +### 文件 < 800 行 / 函数 < 50 行核对 + +- `ApiChain.cpp`/`NavRequest.cpp` 均 < 60 行。`WorkbenchNavController.cpp` 改后预计 ~250 行(< 800);其最大函数 `setCheckedTms` 异步版需拆 helper(缓存命中组装 / 并发汇聚)保持 < 50 行——在 A4 Step 3 块 5 注明提炼 `assembleExceptionTree()` helper。✓ + +--- + +## Execution Handoff + +计划已保存 `docs/superpowers/plans/2026-06-11-apiclient-async-rollout.md`。 + +- **Part A(导航)** 全 bite-sized 任务 A0–A6 可直接执行(推荐 subagent-driven,每 Task 一 subagent,A4/A5 连续)。 +- **Part B(登录)** 骨架 B0–B5——执行前先据骨架细化为 bite-sized(或另起 `2026-06-11-apiclient-async-login.md`)。 + +建议:先完整落地 Part A(一个 PR),验证导航全异步收益与 `ApiChain` 稳定,再启动 Part B。 diff --git a/docs/superpowers/plans/2026-06-11-dataset-detail-chart-v2-qwt.md b/docs/superpowers/plans/2026-06-11-dataset-detail-chart-v2-qwt.md new file mode 100644 index 0000000..3a39bf4 --- /dev/null +++ b/docs/superpowers/plans/2026-06-11-dataset-detail-chart-v2-qwt.md @@ -0,0 +1,205 @@ +# 数据集详情「二维图表」返工实现计划 v2(QwtPlot) + +> **For agentic workers:** REQUIRED SUB-SKILL: 用 superpowers:subagent-driven-development 或 executing-plans 执行。步骤用 `- [ ]`。 +> **铁律(来自返工方案):** 每一步做完**必须**与原版对照验收,通过再进下一步——不许一次性做完才看。任何对原版的不确定,**必须用 Playwright 实地学习原版**,禁止联想猜测。 + +**Goal:** 推翻当前 QGraphicsView 二维图表,按《Geopro3.0_二维图表返工技术方案.md》用 **QwtPlot(坐标系/交互/图例三层分离)+ VTK 算法(等值线多边形)+ 连续对数色阶** 重做「原数据散点图」与「网格数据等值线图」,修复用户列出的 9 个问题,达到**视觉等价 + 交互一致**(非字节级像素)。 + +**Architecture:** 三层分离——工具条(属于面板)/ QwtPlot(数据区 + 固定坐标轴 + Magnifier/Panner 交互)/ 独立 ColorBarWidget(不入数据坐标系)。等值线多边形复用已有 `ContourBands`(VTK 算法);色阶用 `QwtLinearColorMap` 连续插值 + 对数标度,图与图例同源(ColorMapService)。 + +**Tech Stack:** C++17、Qt6 Widgets、**Qwt 6.2(已集成,见 cmake/qwt.cmake,目标名 `qwt`,头在 external/qwt-src/src)**、VTK 9.6(仅算法)、GoogleTest/CTest。 + +**依据:** `docs/Geopro3.0_二维图表返工技术方案.md`(架构权威)、`docs/superpowers/specs/2026-06-11-dataset-detail-view-design.md`(数据/接口)。 + +--- + +## 0. 复用与替换盘点(不要重造已有的) + +**复用(不动):** +- `src/data/api/ApiDatasetRepository.*`、`src/data/dto/DatasetChartDto.*`、`src/controller/DatasetDetailController.*`(数据层+编排,`ChartData` 已带 scatter/grid/colorScale×2/anomalies)。 +- `src/render/ContourBands.*`(VTK 算分层多边形+等值线几何,作为等值线图的几何来源 = 方案的 ContourEngine)。 +- `src/core/model/{Field,ColorScale,Anomaly}.hpp`、`src/app/panels/AnomalyTablePanel.*`(异常表,列对,复用)。 +- 既有下划线页签组件 `buildTabbedPanel`(main.cpp 中「异常/对象属性」面板用的同款,§步骤1 复用)。 + +**替换(删除/重写):** +- ❌ `src/app/panels/chart/DatasetChartView.*`(裸 QGraphicsView)—— 删除。 +- ❌ `src/app/panels/DatasetDetailPage.*`(旧两按钮+图表)—— 重写为基于 QwtPlot 的视图。 +- `src/app/panels/DatasetDetailPanel.*`、`main.cpp` 接线 —— 按新组件调整。 + +--- + +## 文件结构(新建/修改) + +| 文件 | 职责 | +|---|---| +| `src/app/panels/chart/ColorMapService.hpp/.cpp`(建) | 色阶服务:断点+颜色、连续对数映射、产出 `QwtLinearColorMap` 与离散色带;图/图例同源 | +| `src/app/panels/chart/ColorBarWidget.hpp/.cpp`(建) | 独立色阶条 QWidget(固定,不入数据坐标系) | +| `src/app/panels/chart/ScatterPlotItem.hpp/.cpp`(建) | QwtPlotItem:散点方块按连续色阶着色 | +| `src/app/panels/chart/ContourPlotItem.hpp/.cpp`(建) | QwtPlotItem:画 ContourBands 分层填充多边形 + 等值线 + 标注 + 异常叠加 | +| `src/app/panels/chart/RawDataChartView.hpp/.cpp`(建) | 原数据页:工具条 + QwtPlot + ScatterPlotItem + ColorBarWidget | +| `src/app/panels/chart/GridDataChartView.hpp/.cpp`(建) | 网格数据页:工具条 + QwtPlot + ContourPlotItem + ColorBarWidget | +| `src/app/panels/DatasetDetailPage.hpp/.cpp`(重写) | 单 ds 页:下划线页签(原数据/网格数据) + 两视图 + 下方(异常列表/描述) | +| `src/app/panels/DatasetDetailPanel.hpp/.cpp`(改) | 多 Tab 壳(保留 dsId 去重/反向联动) | +| `src/app/panels/DescriptionPanel.hpp/.cpp`(建) | 下方「描述」页(纯文本/只读富文本占位) | +| `src/app/panels/chart/DatasetChartView.*`(删) | 旧 QGraphicsView 渲染器 | +| `src/app/CMakeLists.txt`(改) | 注册新文件 + 链接 `qwt`;移除 DatasetChartView | +| 测试 | ColorMapService 对数映射 / ContourBands 既有 / 散点着色 单测 | + +**构建/测试命令(本机工具链):** +- 构建:`powershell.exe -ExecutionPolicy Bypass -File scripts/dev-build.ps1` +- 测试:`powershell.exe -ExecutionPolicy Bypass -File scripts/dev-test.ps1` +- 运行(视觉验收):`build/release/src/app/geopro_desktop.exe`(需登录) + +**Qwt 关键类参考**(实现时可查 `external/qwt-src/src/` 头文件确认签名):`QwtPlot`、`QwtPlotItem`、`QwtPlotMagnifier`、`QwtPlotPanner`、`QwtPlotZoomer`、`QwtLinearColorMap`、`QwtScaleEngine`/`QwtLogScaleEngine`、`QwtText`、`QwtPlotCanvas`。 + +--- + +## 步骤 1:三层骨架 + UI 结构(治 #6 #7 #8) + +**目标**:先把布局结构做对,不画图。下划线页签、工具条归位、下方双页签。 + +**研究原版(Playwright)**:对照 `assets/web-datasetinfo-*.png` 与实测——确认 (a) 原数据/网格数据 = 下划线页签(非按钮);(b) 两个页签各自的工具条项(§方案7);(c) 下方 = 异常列表 + 描述 双页签。 + +**Files:** 重写 `DatasetDetailPage.*`;建 `DescriptionPanel.*`;改 `DatasetDetailPanel.*`、`src/app/CMakeLists.txt`。 + +- [ ] **Step 1.1** 先研究:用 Playwright 打开 `datasetInfo?id=1439812897218560&ddCode=dd_inversion_data`,截图原数据/网格两页签的工具条与下方区域,确认结构(不靠记忆)。 +- [ ] **Step 1.2** 复用 `buildTabbedPanel`(main.cpp 中「异常/对象属性」用的下划线页签)做 `DatasetDetailPage` 顶部「原数据/网格数据」切换;内容区先放两个空 `QWidget` 占位(RawDataChartView/GridDataChartView 后续填)。 +- [ ] **Step 1.3** 下方用同款 `buildTabbedPanel` 做「异常列表(现有 `AnomalyTablePanel`)/ 描述(新 `DescriptionPanel`,先放只读文本占位)」。 +- [ ] **Step 1.4** 工具条:原数据页工具条 = [网格][色阶配置][当前图形▼][另存为];网格数据页工具条 = [网格][色阶配置][白化][滤波处理][☑显示异常][☑显示等值线标注][☐显示等值线提示信息][简化容差滑块][异常标注][自动标注][另存为]。**先只放按钮/控件占位(无功能)**,位置在各自页签视图内部顶部。 +- [ ] **Step 1.5** 构建通过;启动 app 双击 ds,**对照原版验收**:页签是下划线样式、工具条在各自页签内、下方有两个页签。截图发用户确认后再进步骤 2。 + +--- + +## 步骤 2:QwtPlot 接入 + 交互(治 #3 #4 #9) + +**研究原版(Playwright,关键——消除「四象限」歧义)**: +- [ ] **Step 2.1** 在原版散点页拖动/缩放,观察:坐标轴是否固定、刻度值是否跟随、能否平移到数据范围外(负区)。抓 Plotly `layout.xaxis/yaxis`(range/scaleanchor/autorange/side)。**判定原数据到底是「真四象限」还是「单象限+自由平移」**,网格数据同样观察。把结论记入本步骤再实现,不靠猜。 + +**Files:** `RawDataChartView.*`、`GridDataChartView.*`(先只含 QwtPlot 空图 + 交互);`src/app/CMakeLists.txt` 链 `qwt`。 + +- [ ] **Step 2.2** 两视图各内嵌一个 `QwtPlot`。按步骤2.1结论设轴:网格数据 = 单象限(x/y 从数据 min,原点左下);原数据 = 按结论(四象限则轴范围含负、十字交叉;单象限则同网格)。 +- [ ] **Step 2.3** 加 `QwtPlotMagnifier(plot->canvas())`(滚轮缩放)+ `QwtPlotPanner(plot->canvas())`(拖动平移)。确认:**轴位置不动、只有数据区平移缩放、刻度数值自动跟随**(QwtPlot 默认行为)。 +- [ ] **Step 2.4** 纵横比(#9):原版剖面"宽扁"。先用 `plot->setAxisScale` 固定 y 数据范围、x 自适应填充宽度;若需等比例尺,在 resize/replot 时按数据 x/y 跨度调 canvas 宽高(方案 §4.4)。 +- [ ] **Step 2.5** 构建+运行,**对照原版验收**:拖动只动数据、不花屏、轴值跟随、宽扁比例接近。截图确认后进步骤 3。 + +--- + +## 步骤 3:独立色阶条 ColorBarWidget(治 #1) + +**Files:** `ColorBarWidget.*`;放进 Raw/GridDataChartView 布局中 QwtPlot **下方**(兄弟 widget)。 + +- [ ] **Step 3.1** `ColorBarWidget : QWidget`,`paintEvent` 自绘一条水平色带(暂用占位色阶)+ 刻度数值。固定高度(~36px:色条+文字)。 +- [ ] **Step 3.2** 布局:`QVBoxLayout`= [工具条][QwtPlot stretch][ColorBarWidget 固定高]。**ColorBar 不入 QwtPlot 坐标系**。 +- [ ] **Step 3.3** 运行验收:缩放/拖动主图时色阶条**不动、不与横轴重叠**(治 #1)。截图确认进步骤 4。 + +--- + +## 步骤 4:ColorMapService + 抓原版真实色值(治 #2 基础) + +**研究原版(Playwright,关键)**: +- [ ] **Step 4.1** 抓原版色值与映射方式(**严禁凭记忆配色**): + - 散点页:抓 Plotly `data[0].marker.colorscale`([[pos,color],…])、`cmin/cmax`、是否连续;判断标度(线性 vs 对数)——对色阶条断点 `17.105,25.937,…` 做比值检验确认对数/等比。 + - 网格页:色阶来自 `colorGradation/getDetail`(已知 `properties.colorBar=[[值,rgba],…]`、type1 原数据/type2 网格)。 + - 取色兜底:对色阶条截图每格中心取 RGB。 + +**Files:** `ColorMapService.hpp/.cpp`;测试 `tests/app/test_colormap_service.cpp`。 + +- [ ] **Step 4.2 写失败测试**(对数映射): +```cpp +#include +#include "panels/chart/ColorMapService.hpp" +using geopro::app::ColorMapService; +TEST(ColorMapService, LogMapsValueToUnitInterval) { + ColorMapService s; + s.setRange(10.0, 1000.0, /*logScale=*/true); + EXPECT_NEAR(s.normalized(10.0), 0.0, 1e-9); + EXPECT_NEAR(s.normalized(1000.0), 1.0, 1e-9); + EXPECT_NEAR(s.normalized(100.0), 0.5, 1e-9); // log10 中点 +} +``` +- [ ] **Step 4.3** 注册测试到 `tests/CMakeLists.txt`(app 段,已 `target_include_directories ... src/app`);跑确认失败(编译失败)。 +- [ ] **Step 4.4 实现** `ColorMapService`: + - 持 `core::ColorScale`(离散断点+颜色,来自 colorBar)+ `vmin/vmax/logScale`。 + - `double normalized(double v)`:log 时 `(log(v)-log(vmin))/(log(vmax)-log(vmin))`,否则线性;clamp [0,1]。 + - `QwtLinearColorMap* makeQwtColorMap()`:用断点的归一化位置 + 颜色 `addColorStop`,连续插值(默认 ScaledColors)。 + - `core::Rgba colorAt(double v)`:连续插值取色(散点用);以及离散 `bandColor`(等值线用)。 + - 图与图例(ColorBarWidget)+ 等值线图都用**同一个 ColorMapService 实例**。 +- [ ] **Step 4.5** 跑测试通过;提交。 + +--- + +## 步骤 5:原数据散点图(治 #2) + +**研究原版(Playwright)**:确认散点 = 方块 symbol、白描边、按 vlist 连续着色、x:y 关系(步骤2结论)。 + +**Files:** `ScatterPlotItem.*`;接入 `RawDataChartView`。 + +- [ ] **Step 5.1** `ScatterPlotItem : public QwtPlotItem`,重写 `draw(QPainter*, xMap, yMap, canvasRect)`:对每个点 `(xlist[i],ylist[i])` 用 `xMap.transform/yMap.transform` 映射到像素,画固定像素边长的方块(白描边 1px),填色 = `ColorMapService::colorAt(vlist[i])`。`rtti()` 返回自定义。 +- [ ] **Step 5.2** `RawDataChartView::setData(scatter, colorMapService)`:建 ScatterPlotItem attach 到 QwtPlot,设轴范围为数据包围盒,`replot()`;ColorBarWidget 用同一 service 刷新。 +- [ ] **Step 5.3** 运行,**与原版散点并排对照配色**(#2):颜色应连续插值、与原版一致。截图确认进步骤 6。 + +--- + +## 步骤 6:网格数据等值线图(治 #2 #5) + +**研究原版(Playwright)**:确认填充色带(离散 banded)+ 黑色等值线 + **线上数值标注**(贴线方向)+ 数据凸包白边。 + +**Files:** `ContourPlotItem.*`(复用 `ContourBands`);接入 `GridDataChartView`。 + +- [ ] **Step 6.1** `ContourPlotItem : QwtPlotItem`,持 `ContourBandsResult`(来自 `geopro::render::buildContourBands(grid, colorScale, opt)`)。`draw()`: + - 填充:每个 `BandPolygon` 用 xMap/yMap 把局部坐标映射到像素,`painter->drawPolygon`,填 `band.color`(离散)。 + - 等值线:每条 `ContourLine` `drawPolyline`(黑、细);受「显示等值线标注」控制在线中段绘制旋转贴线的数值文本(#5)。 +- [ ] **Step 6.2** 异常叠加:把 ds 异常(`core::Anomaly`,局部坐标)按 markType 画在同一 item 之上(虚线多段线/多边形/点),受「显示异常」控制。 +- [ ] **Step 6.3** `GridDataChartView::setData(grid, gridColorMapService, anomalies)`:构建 ContourPlotItem attach;轴=数据范围;ColorBar 用网格色阶(type2)。 +- [ ] **Step 6.4** 运行,**与原版网格图并排对照**(#2 配色、#5 标注、凸包白边、宽扁比例)。截图确认进步骤 7。 + +--- + +## 步骤 7:工具条联动(显示开关 + 简化容差) + +- [ ] **Step 7.1** 「显示异常 / 显示等值线标注 / 显示等值线提示信息」复选框 → 切 ContourPlotItem 对应开关 + replot。 +- [ ] **Step 7.2** 「简化容差」滑块(0–1)→ 改 `ContourOptions::simplifyTol` 重算 ContourBands + replot(对应原版滑块)。 +- [ ] **Step 7.3** 「白化 / 滤波处理 / 网格 / 色阶配置 / 异常标注 / 自动标注 / 另存为」**先做占位按钮**(弹「待实现」或留空槽),功能后续单独立项。运行验收开关行为与原版一致。 + +--- + +## 步骤 8:接线 DatasetDetailController + main.cpp(替换旧渲染) + +- [ ] **Step 8.1** `DatasetDetailPage::setData(ChartData)`:分别喂 `RawDataChartView`(scatter + scatterColorScale)与 `GridDataChartView`(grid + gridColorScale + anomalies);异常列表喂 `AnomalyTablePanel`。 +- [ ] **Step 8.2** `main.cpp`:删除对旧 `DatasetChartView`/`DatasetDetailPage` 的引用;`DatasetDetailPanel` 改用新 `DatasetDetailPage`。`chartReady`→`openOrUpdate` 链路不变。删除 `src/app/panels/chart/DatasetChartView.*` 及其 CMake 注册。 +- [ ] **Step 8.3** 全量构建 + `dev-test.ps1` 不回归;启动 app 双击 ds **整体对照验收**。 + +--- + +## 步骤 9:整体对照验收(治全部 9 项) + +- [ ] **Step 9.1** 原数据、网格数据分别与原版并排截图,逐条核对方案 §10 验收清单:配色/轴固定/色阶条固定/标注/坐标系象限/宽扁比例/拖动流畅/页签样式/工具条归位/下方双页签。 +- [ ] **Step 9.2** cpp-reviewer 审查本次改动;处理 CRITICAL/HIGH。 +- [ ] **Step 9.3** 收尾提交。 + +--- + +## 验收标准(方案 §10,视觉等价 + 交互一致,非字节级) + +- [ ] 配色与原版视觉一致(连续插值、对数分布、同色阶) +- [ ] 轴固定、拖动/缩放只动数据区、刻度值跟随、不花屏 +- [ ] 色阶条固定轴下方、不随图缩放、不与轴重叠 +- [ ] 等值线数值标注显示、贴线方向 +- [ ] 原数据/网格数据坐标系象限与缩放行为分别与原版一致 +- [ ] 宽扁比例与原版一致 +- [ ] 页签下划线样式、工具条各自页签内、下方异常列表+描述双页签 +- [ ] 全量测试不回归 +- ✗(明确不作为标准):与 web 逐像素 diff 相同 + +--- + +## 自审(Spec/方案 覆盖核对) + +- 9 个问题逐条 → 步骤 1(#6/#7/#8)、2(#3/#4/#9)、3(#1)、4-5(#2)、6(#2/#5) 全覆盖。✓ +- 复用项(ContourBands/数据层/AnomalyTablePanel/buildTabbedPanel)已标注,不重造。✓ +- 替换项(DatasetChartView/旧 DatasetDetailPage)明确删除。✓ +- 每步含「Playwright 研究原版 + 对照验收」检查点(铁律)。✓ +- 可测部分(ColorMapService 对数映射)有 TDD;视觉部分以对照验收为准(方案明确视觉等价非像素级)。✓ +- 待澄清/研究项(步骤 2.1 四象限歧义、步骤 4.1 真实色值与标度)已显式列为开工先研究项,不靠猜。✓ + +> 工具功能(白化/滤波/自动标注/网格化/色阶配置/另存为/导出)本计划只做**占位归位**(治 #7),功能实现后续单独立项。 diff --git a/docs/superpowers/plans/2026-06-11-dataset-detail-chart.md b/docs/superpowers/plans/2026-06-11-dataset-detail-chart.md new file mode 100644 index 0000000..de020db --- /dev/null +++ b/docs/superpowers/plans/2026-06-11-dataset-detail-chart.md @@ -0,0 +1,1628 @@ +# 数据集详情视图(平面图表)实现计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 把客户端「数据详情」dock 从 VTK 渲染重建为本地面板 + 平面图表(QGraphicsView),接真实 API,真正接上数据集选择链路,100% 复刻 web datasetInfo 的展示(ERT 反演首版)。 + +**Architecture:** VTK 仅当几何算法库(`vtkBandedPolyDataContourFilter` 出色带/等值线几何,无 render window);QGraphicsView 画 2D 场景。新增 `ContourBuilder`(render 层)、`ApiDatasetRepository` + DTO 解析、`DatasetChartView`、dd 策略注册表、`AnomalyTablePanel`/`DatasetDetailPage`/`DatasetDetailPanel`、`DatasetDetailController`,并在 `main.cpp` 换掉旧 VTK 详情 dock。复用 `core::Grid/ScatterField/ColorScale/Anomaly`。 + +**Tech Stack:** C++17、Qt6 Widgets(QGraphicsView/Scene/Item)、VTK 9.6(仅几何)、GoogleTest/CTest、CMake、`net::ApiClient`(同步 HTTP)。 + +**设计依据:** `docs/superpowers/specs/2026-06-11-dataset-detail-view-design.md` + +--- + +## 文件结构(创建/修改) + +| 文件 | 职责 | +|---|---| +| `src/core/model/Field.hpp`(改) | `Grid` 增 NaN 约定 + `hasValue(i,j)` 辅助 | +| `src/render/ContourBands.hpp/.cpp`(建) | `core::Grid + ColorScale → 色带多边形 + 等值线`(VTK 几何提取 + 上采样/平滑/裁剪/简化) | +| `src/render/CMakeLists.txt`(改) | 注册 ContourBands | +| `src/data/dto/DatasetChartDto.hpp/.cpp`(建) | `QJsonObject → Grid/ScatterField/ColorScale/Anomaly[]` 解析 | +| `src/data/repo/IDatasetRepository.hpp`(改) | 增 `loadScatterColorScale` | +| `src/data/api/ApiDatasetRepository.hpp/.cpp`(建) | 实现 `IDatasetRepository` 接真实 API | +| `src/data/repo/LocalSampleRepository.hpp`(改) | `loadScatterColorScale` 标 `override` | +| `src/data/CMakeLists.txt`(改) | 注册 DatasetChartDto + ApiDatasetRepository | +| `src/app/panels/chart/DatasetChartView.hpp/.cpp`(建) | QGraphicsView 2D 渲染器 | +| `src/app/panels/chart/IDatasetChartStrategy.hpp`(建) | dd 策略接口 + 注册表 | +| `src/app/panels/chart/ErtInversionStrategy.hpp/.cpp`(建) | dd_inversion_data 策略 | +| `src/app/panels/AnomalyTablePanel.hpp/.cpp`(建) | ds 级异常表 | +| `src/app/panels/DatasetDetailPage.hpp/.cpp`(建) | 单数据集详情页 | +| `src/app/panels/DatasetDetailPanel.hpp/.cpp`(建) | 多 Tab 壳 | +| `src/controller/DatasetDetailController.hpp/.cpp`(建) | 编排:选策略→拉数据→发信号 | +| `src/app/CMakeLists.txt` / `src/controller/CMakeLists.txt`(改) | 注册新文件 | +| `src/app/main.cpp`(改) | 移除旧 VTK 详情 dock,接上新面板 + 控制器 + 单击/双击 | +| `tests/render/test_contour_bands.cpp`(建) | ContourBands 单测 | +| `tests/data/test_dataset_chart_dto.cpp`(建) | DTO 解析单测 | +| `tests/app/test_chart_strategy_registry.cpp`(建) | 注册表单测 | +| `tests/CMakeLists.txt`(改) | 注册新测试源 | + +**构建/测试命令(全程统一):** +- 配置:`cmake --preset default`(若首次);构建:`cmake --build build` +- 跑全部测试:`ctest --test-dir build --output-on-failure` +- 跑单个:`ctest --test-dir build -R <用例名> --output-on-failure` + +--- + +## Phase 1:core::Grid NaN + ContourBands(VTK 几何提取) + +### Task 1.1:Grid 增 NaN 约定 + hasValue + +**Files:** +- Modify: `src/core/model/Field.hpp`(`class Grid`,在 `valueAt` 附近) + +- [ ] **Step 1: 写失败测试** + +Create `tests/core/test_model_data.cpp` 末尾追加(该文件已在 CMake 中): +```cpp +#include +#include "model/Field.hpp" +TEST(GridNaN, HasValueReflectsNaN) { + geopro::core::Grid g(2, 2); + g.valueAt(0, 0) = 3.0; + g.valueAt(1, 0) = std::nan(""); + EXPECT_TRUE(g.hasValue(0, 0)); + EXPECT_FALSE(g.hasValue(1, 0)); +} +``` + +- [ ] **Step 2: 运行确认失败** + +Run: `cmake --build build && ctest --test-dir build -R GridNaN --output-on-failure` +Expected: 编译失败「no member named 'hasValue'」 + +- [ ] **Step 3: 实现** + +在 `Field.hpp` 的 `Grid` 类 public 区(`valueAt` 之后)加: +```cpp + bool hasValue(int i, int j) const { return !std::isnan(valueAt(i, j)); } +``` +并在文件顶部 include 区确保有 `#include `。在 `Grid` 类注释补一行:`// NaN 值=无效/测区外(凸包裁剪据此)。` + +- [ ] **Step 4: 运行确认通过** + +Run: `ctest --test-dir build -R GridNaN --output-on-failure` +Expected: PASS + +- [ ] **Step 5: 提交** + +```bash +git add src/core/model/Field.hpp tests/core/test_model_data.cpp +git commit -m "feat(core): Grid 增 NaN 约定 + hasValue(凸包裁剪用)" +``` + +--- + +### Task 1.2:ContourBands 骨架 + 色带提取(不含预处理) + +**Files:** +- Create: `src/render/ContourBands.hpp`, `src/render/ContourBands.cpp` +- Modify: `src/render/CMakeLists.txt` +- Test: `tests/render/test_contour_bands.cpp`, `tests/CMakeLists.txt` + +- [ ] **Step 1: 写头文件(类型 + 接口)** + +Create `src/render/ContourBands.hpp`: +```cpp +#pragma once +#include +#include "model/Grid.hpp" +#include "model/ColorScale.hpp" +namespace geopro::render { + +struct BandPolygon { + geopro::core::Rgba color; + std::vector ring; // 单环多边形(局部坐标,y=高程向上为正) +}; +struct ContourLine { + double level; + std::vector pts; +}; +struct ContourBandsResult { + std::vector bands; + std::vector lines; +}; +struct ContourOptions { + int upsample = 2; // 双线性上采样倍数(1=不采样) + double smooth = 0.3; // 平滑强度 0..1(0=不平滑) + double simplifyTol = 0.5; // 等值线简化容差(数据单位,0=不简化) + bool makeLines = true; +}; + +// core::Grid(NaN=无效)+ ColorScale 分段 → 色带多边形 + 等值线几何。 +// VTK 仅作算法(vtkBandedPolyDataContourFilter),无 render window。 +ContourBandsResult buildContourBands(const geopro::core::Grid& g, + const geopro::core::ColorScale& cs, + const ContourOptions& opt = {}); +} // namespace geopro::render +``` +> 注:`core::Vec2{double x,y}` 已定义于 `Anomaly.hpp`;`ContourBands.hpp` 经 `Grid.hpp`/`ColorScale.hpp` 间接需要它,显式 `#include "model/Anomaly.hpp"` 以引入 `Vec2`。在上面 include 区加 `#include "model/Anomaly.hpp"`。 + +- [ ] **Step 2: 写失败测试** + +Create `tests/render/test_contour_bands.cpp`: +```cpp +#include +#include "ContourBands.hpp" +using namespace geopro::core; +using namespace geopro::render; + +// 2x2 平滑梯度网格 + 2 段色阶 → 至少 1 个色带多边形,多边形顶点 >=3。 +TEST(ContourBands, ProducesNonEmptyBands) { + Grid g(3, 3); + g.x = {0, 1, 2}; g.y = {0, 1, 2}; + for (int j = 0; j < 3; ++j) + for (int i = 0; i < 3; ++i) g.valueAt(i, j) = static_cast(i + j); // 0..4 + g.vmin = 0; g.vmax = 4; + ColorScale cs; + cs.addStop(0.0, Rgba{0, 0, 255, 255}); + cs.addStop(2.0, Rgba{0, 255, 0, 255}); + cs.addStop(4.0, Rgba{255, 0, 0, 255}); + + ContourOptions opt; opt.upsample = 1; opt.smooth = 0; opt.simplifyTol = 0; + auto r = buildContourBands(g, cs, opt); + ASSERT_FALSE(r.bands.empty()); + for (const auto& b : r.bands) EXPECT_GE(b.ring.size(), 3u); +} +``` + +- [ ] **Step 3: 注册 CMake + 跑确认失败** + +`src/render/CMakeLists.txt`:在现有 `target_sources(geopro_render PRIVATE ...)` 列表加一行 `ContourBands.cpp`(与 `ColorLutBuilder.cpp` 同处)。 +`tests/CMakeLists.txt`:在 render 测试段(`render/test_color_lut.cpp` 之后)加 `target_sources(geopro_tests PRIVATE render/test_contour_bands.cpp)`。 +Run: `cmake --build build && ctest --test-dir build -R ContourBands --output-on-failure` +Expected: 链接失败「buildContourBands 未定义」 + +- [ ] **Step 4: 实现色带提取(先不做预处理)** + +Create `src/render/ContourBands.cpp`: +```cpp +#include "ContourBands.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace geopro::render { +using geopro::core::Grid; +using geopro::core::ColorScale; +using geopro::core::Vec2; +using geopro::core::Rgba; + +namespace { +// 用 Grid 构 vtkStructuredGrid(点 (x[i], y[j], 0);NaN 值置 0 但记录于掩膜—— +// 首版裁剪在 Task 1.4 接入,这里先全量)。 +vtkSmartPointer toStructuredGrid(const Grid& g) { + const int nx = g.nx(), ny = g.ny(); + auto sg = vtkSmartPointer::New(); + sg->SetDimensions(nx, ny, 1); + vtkNew pts; pts->SetNumberOfPoints(static_cast(nx) * ny); + vtkNew sc; sc->SetName("v"); + sc->SetNumberOfTuples(static_cast(nx) * ny); + for (int j = 0; j < ny; ++j) + for (int i = 0; i < nx; ++i) { + const vtkIdType id = static_cast(j) * nx + i; + pts->SetPoint(id, g.x[i], g.y[j], 0.0); + const double v = g.valueAt(i, j); + sc->SetValue(id, std::isnan(v) ? 0.0 : v); + } + sg->SetPoints(pts); + sg->GetPointData()->SetScalars(sc); + return sg; +} +} // namespace + +ContourBandsResult buildContourBands(const Grid& g, const ColorScale& cs, const ContourOptions&) { + ContourBandsResult out; + const int nx = g.nx(), ny = g.ny(); + if (nx < 2 || ny < 2 || g.x.size() < 2 || g.y.size() < 2) return out; + + const std::vector stops = cs.stopValues(); + if (stops.size() < 2) return out; + + auto sg = toStructuredGrid(g); + vtkNew surf; surf->SetInputData(sg); + vtkNew banded; + banded->SetInputConnection(surf->GetOutputPort()); + banded->SetNumberOfContours(static_cast(stops.size())); + for (int i = 0; i < static_cast(stops.size()); ++i) banded->SetValue(i, stops[i]); + banded->GenerateContourEdgesOn(); + banded->SetScalarModeToValue(); + banded->Update(); + + // port0:色带多边形(cell 标量=带的代表值)→ 按 ColorScale 上色。 + vtkPolyData* poly = banded->GetOutput(); + vtkDataArray* cellScalars = poly->GetCellData()->GetScalars(); + const vtkIdType nCells = poly->GetNumberOfCells(); + for (vtkIdType c = 0; c < nCells; ++c) { + vtkCell* cell = poly->GetCell(c); + vtkPoints* cp = cell->GetPoints(); + if (cp->GetNumberOfPoints() < 3) continue; + BandPolygon bp; + const double val = cellScalars ? cellScalars->GetTuple1(c) : 0.0; + bp.color = cs.colorAt(val); + for (vtkIdType p = 0; p < cp->GetNumberOfPoints(); ++p) { + double xyz[3]; cp->GetPoint(p, xyz); + bp.ring.push_back(Vec2{xyz[0], xyz[1]}); + } + out.bands.push_back(std::move(bp)); + } + + // port1:等值线(polylines)。 + vtkPolyData* edges = banded->GetContourEdgesOutput(); + if (edges) { + edges->BuildCells(); + const vtkIdType nLines = edges->GetNumberOfCells(); + for (vtkIdType c = 0; c < nLines; ++c) { + vtkCell* cell = edges->GetCell(c); + vtkPoints* cp = cell->GetPoints(); + ContourLine cl; cl.level = 0.0; + for (vtkIdType p = 0; p < cp->GetNumberOfPoints(); ++p) { + double xyz[3]; cp->GetPoint(p, xyz); + cl.pts.push_back(Vec2{xyz[0], xyz[1]}); + } + if (cl.pts.size() >= 2) out.lines.push_back(std::move(cl)); + } + } + return out; +} +} // namespace geopro::render +``` + +- [ ] **Step 5: 跑确认通过 + 提交** + +Run: `ctest --test-dir build -R ContourBands --output-on-failure` +Expected: PASS +```bash +git add src/render/ContourBands.hpp src/render/ContourBands.cpp src/render/CMakeLists.txt \ + tests/render/test_contour_bands.cpp tests/CMakeLists.txt +git commit -m "feat(render): ContourBands 从 VTK banded 提取色带多边形+等值线几何" +``` + +--- + +### Task 1.3:双线性上采样 + 平滑预处理 + +**Files:** +- Modify: `src/render/ContourBands.cpp`(在 `buildContourBands` 开头插入预处理) + +- [ ] **Step 1: 写失败测试** + +在 `tests/render/test_contour_bands.cpp` 追加: +```cpp +// 上采样 2x:色带边界更密 → 多边形数应多于不上采样。 +TEST(ContourBands, UpsampleIncreasesDetail) { + Grid g(3, 3); + g.x = {0, 1, 2}; g.y = {0, 1, 2}; + for (int j = 0; j < 3; ++j) + for (int i = 0; i < 3; ++i) g.valueAt(i, j) = static_cast(i * i + j); + g.vmin = 0; g.vmax = 6; + ColorScale cs; + cs.addStop(0, Rgba{0,0,255,255}); cs.addStop(3, Rgba{0,255,0,255}); cs.addStop(6, Rgba{255,0,0,255}); + ContourOptions a; a.upsample = 1; a.smooth = 0; a.simplifyTol = 0; + ContourOptions b; b.upsample = 2; b.smooth = 0; b.simplifyTol = 0; + auto ra = buildContourBands(g, cs, a); + auto rb = buildContourBands(g, cs, b); + EXPECT_GT(rb.bands.size(), ra.bands.size()); +} +``` + +- [ ] **Step 2: 跑确认失败** + +Run: `cmake --build build && ctest --test-dir build -R ContourBands.UpsampleIncreasesDetail --output-on-failure` +Expected: FAIL(当前忽略 opt,两者相等) + +- [ ] **Step 3: 实现上采样 + 平滑** + +在 `ContourBands.cpp` 的匿名命名空间加: +```cpp +// 双线性上采样:nx×ny → ((nx-1)*k+1)×((ny-1)*k+1)。NaN 单元参与的目标点置 NaN。 +Grid upsampleBilinear(const Grid& g, int k) { + if (k <= 1) return g; + const int nx = g.nx(), ny = g.ny(); + const int Nx = (nx - 1) * k + 1, Ny = (ny - 1) * k + 1; + Grid out(Nx, Ny); + out.x.resize(Nx); out.y.resize(Ny); + for (int I = 0; I < Nx; ++I) { double fi = double(I) / k; int i0 = std::min(int(fi), nx - 2); + double t = fi - i0; out.x[I] = g.x[i0] * (1 - t) + g.x[i0 + 1] * t; } + for (int J = 0; J < Ny; ++J) { double fj = double(J) / k; int j0 = std::min(int(fj), ny - 2); + double t = fj - j0; out.y[J] = g.y[j0] * (1 - t) + g.y[j0 + 1] * t; } + for (int J = 0; J < Ny; ++J) { + double fj = double(J) / k; int j0 = std::min(int(fj), ny - 2); double tj = fj - j0; + for (int I = 0; I < Nx; ++I) { + double fi = double(I) / k; int i0 = std::min(int(fi), nx - 2); double ti = fi - i0; + double v00 = g.valueAt(i0, j0), v10 = g.valueAt(i0 + 1, j0); + double v01 = g.valueAt(i0, j0 + 1), v11 = g.valueAt(i0 + 1, j0 + 1); + if (std::isnan(v00) || std::isnan(v10) || std::isnan(v01) || std::isnan(v11)) + out.valueAt(I, J) = std::nan(""); + else + out.valueAt(I, J) = (v00 * (1 - ti) + v10 * ti) * (1 - tj) + + (v01 * (1 - ti) + v11 * ti) * tj; + } + } + out.vmin = g.vmin; out.vmax = g.vmax; + return out; +} +// 3x3 盒式平滑(NaN 跳过);strength 0..1 线性混合原值。 +Grid smoothGrid(const Grid& g, double strength) { + if (strength <= 0) return g; + const int nx = g.nx(), ny = g.ny(); + Grid out = g; + for (int j = 0; j < ny; ++j) + for (int i = 0; i < nx; ++i) { + double v = g.valueAt(i, j); + if (std::isnan(v)) continue; + double sum = 0; int n = 0; + for (int dj = -1; dj <= 1; ++dj) for (int di = -1; di <= 1; ++di) { + int ii = i + di, jj = j + dj; + if (ii < 0 || ii >= nx || jj < 0 || jj >= ny) continue; + double w = g.valueAt(ii, jj); if (std::isnan(w)) continue; sum += w; ++n; + } + if (n) out.valueAt(i, j) = v * (1 - strength) + (sum / n) * strength; + } + return out; +} +``` +在 `buildContourBands` 函数体最前面(`if (nx<2...)` 之后)插入: +```cpp + Grid work = smoothGrid(upsampleBilinear(g, std::max(1, opt.upsample)), opt.smooth); +``` +并把后续 `toStructuredGrid(g)`、`nx/ny` 改用 `work`(即 `const int nx=work.nx()...`、`toStructuredGrid(work)`)。注意把函数签名第三参恢复命名:`const ContourOptions& opt`。 + +- [ ] **Step 4: 跑确认通过** + +Run: `ctest --test-dir build -R ContourBands --output-on-failure` +Expected: 两个用例均 PASS + +- [ ] **Step 5: 提交** + +```bash +git add src/render/ContourBands.cpp tests/render/test_contour_bands.cpp +git commit -m "feat(render): ContourBands 双线性上采样+盒式平滑预处理(对齐 web 2x+smooth)" +``` + +--- + +### Task 1.4:NaN 凸包裁剪 + 等值线简化 + +**Files:** +- Modify: `src/render/ContourBands.cpp` + +- [ ] **Step 1: 写失败测试** + +追加: +```cpp +// 含 NaN 无效角的网格:裁剪后该角不应被任何色带覆盖(所有多边形顶点远离该角)。 +TEST(ContourBands, ClipsNaNRegion) { + Grid g(3, 3); + g.x = {0, 1, 2}; g.y = {0, 1, 2}; + for (int j = 0; j < 3; ++j) for (int i = 0; i < 3; ++i) g.valueAt(i, j) = 5.0; + g.valueAt(2, 2) = std::nan(""); // 右上角无效 + g.vmin = 0; g.vmax = 10; + ColorScale cs; cs.addStop(0, Rgba{0,0,255,255}); cs.addStop(10, Rgba{255,0,0,255}); + ContourOptions opt; opt.upsample = 1; opt.smooth = 0; opt.simplifyTol = 0; + auto r = buildContourBands(g, cs, opt); + bool coversCorner = false; + for (const auto& b : r.bands) + for (const auto& p : b.ring) + if (p.x > 1.5 && p.y > 1.5) coversCorner = true; + EXPECT_FALSE(coversCorner); +} +``` + +- [ ] **Step 2: 跑确认失败** + +Run: `cmake --build build && ctest --test-dir build -R ContourBands.ClipsNaNRegion --output-on-failure` +Expected: FAIL(当前 NaN 置 0,整片覆盖) + +- [ ] **Step 3: 实现裁剪 + 简化** + +裁剪策略(首版务实):把含任一 NaN 顶点的网格单元(quad)从 `vtkStructuredGrid` 中**剔除**——改 `toStructuredGrid` 为 `vtkUnstructuredGrid`,逐 quad 仅当 4 顶点都 `hasValue` 才 `InsertNextCell(VTK_QUAD,...)`。在 `ContourBands.cpp` 顶部加 `#include `、`#include `,并把 `toStructuredGrid` 替换为: +```cpp +vtkSmartPointer toCellGrid(const Grid& g) { + const int nx = g.nx(), ny = g.ny(); + auto ug = vtkSmartPointer::New(); + vtkNew pts; pts->SetNumberOfPoints(static_cast(nx) * ny); + vtkNew sc; sc->SetName("v"); + sc->SetNumberOfTuples(static_cast(nx) * ny); + for (int j = 0; j < ny; ++j) for (int i = 0; i < nx; ++i) { + const vtkIdType id = static_cast(j) * nx + i; + pts->SetPoint(id, g.x[i], g.y[j], 0.0); + const double v = g.valueAt(i, j); + sc->SetValue(id, std::isnan(v) ? 0.0 : v); + } + ug->SetPoints(pts); + ug->GetPointData()->SetScalars(sc); + auto idAt = [nx](int i, int j) { return static_cast(j) * nx + i; }; + for (int j = 0; j < ny - 1; ++j) for (int i = 0; i < nx - 1; ++i) { + if (!g.hasValue(i, j) || !g.hasValue(i+1, j) || !g.hasValue(i, j+1) || !g.hasValue(i+1, j+1)) + continue; // 含 NaN 的 quad 不入网格 → 凸包裁剪 + vtkIdType quad[4] = { idAt(i,j), idAt(i+1,j), idAt(i+1,j+1), idAt(i,j+1) }; + ug->InsertNextCell(VTK_QUAD, 4, quad); + } + return ug; +} +``` +`vtkDataSetSurfaceFilter` 对 `vtkUnstructuredGrid` 同样适用:把 `surf->SetInputData(sg)` 改为 `surf->SetInputData(toCellGrid(work))`(删除原 `toStructuredGrid(work)` 调用与 `sg` 变量)。 +等值线简化:在收集 `cl.pts` 后调用 Douglas-Peucker。加匿名函数: +```cpp +void simplifyInPlace(std::vector& pts, double tol) { + if (tol <= 0 || pts.size() < 3) return; + std::vector keep(pts.size(), 0); keep.front() = keep.back() = 1; + std::vector> st{{0, int(pts.size()) - 1}}; + auto dist = [](const Vec2& p, const Vec2& a, const Vec2& b) { + double dx = b.x - a.x, dy = b.y - a.y, L = std::hypot(dx, dy); + if (L < 1e-12) return std::hypot(p.x - a.x, p.y - a.y); + return std::fabs((p.x - a.x) * dy - (p.y - a.y) * dx) / L; }; + while (!st.empty()) { auto [s, e] = st.back(); st.pop_back(); + double dmax = 0; int idx = -1; + for (int k = s + 1; k < e; ++k) { double d = dist(pts[k], pts[s], pts[e]); + if (d > dmax) { dmax = d; idx = k; } } + if (idx >= 0 && dmax > tol) { keep[idx] = 1; st.push_back({s, idx}); st.push_back({idx, e}); } } + std::vector r; for (size_t k = 0; k < pts.size(); ++k) if (keep[k]) r.push_back(pts[k]); + pts.swap(r); +} +``` +在 `if (cl.pts.size() >= 2)` 之前调用 `simplifyInPlace(cl.pts, opt.simplifyTol);`。文件顶部加 `#include `(已含)与 ``(经头文件已含)。 + +- [ ] **Step 4: 跑确认通过** + +Run: `ctest --test-dir build -R ContourBands --output-on-failure` +Expected: 全部 PASS + +- [ ] **Step 5: 提交** + +```bash +git add src/render/ContourBands.cpp tests/render/test_contour_bands.cpp +git commit -m "feat(render): ContourBands NaN 凸包裁剪(剔除无效quad)+等值线DP简化" +``` + +--- + +## Phase 2:DTO 解析 + ApiDatasetRepository + +### Task 2.1:DatasetChartDto 解析(Grid/Scatter/ColorBar/Anomaly) + +**Files:** +- Create: `src/data/dto/DatasetChartDto.hpp`, `src/data/dto/DatasetChartDto.cpp` +- Modify: `src/data/CMakeLists.txt` +- Test: `tests/data/test_dataset_chart_dto.cpp`, `tests/CMakeLists.txt` + +- [ ] **Step 1: 写头文件** + +Create `src/data/dto/DatasetChartDto.hpp`: +```cpp +#pragma once +#include +#include +#include +#include "model/Field.hpp" +#include "model/ColorScale.hpp" +#include "model/Anomaly.hpp" +namespace geopro::data::dto { + +// inversion/rows 的 data{ x,y,v[ny][nx],z,elevation,vmin,vmax } → Grid(v 缺省/非数→NaN)。 +geopro::core::Grid parseInversionGrid(const QJsonObject& data); +// getErtRawDataScatterGraph 的 data{ xlist,ylist,hlist,vlist,projectXList,projectYList } → ScatterField。 +geopro::core::ScatterField parseScatterGraph(const QJsonObject& data); +// colorGradation/getDetail 的 data.properties.colorBar [[值,"rgba()"],…] → ColorScale。 +geopro::core::ColorScale parseColorBar(const QJsonObject& data); +// queryException 的 data 数组 → Anomaly[](location.coordinate / legend)。 +std::vector parseDatasetAnomalies(const QJsonArray& arr); + +} // namespace geopro::data::dto +``` + +- [ ] **Step 2: 写失败测试** + +Create `tests/data/test_dataset_chart_dto.cpp`: +```cpp +#include +#include +#include +#include +#include "dto/DatasetChartDto.hpp" +using namespace geopro::data::dto; + +static QJsonObject obj(const char* json) { + return QJsonDocument::fromJson(json).object(); +} + +TEST(DatasetChartDto, ParsesInversionGrid) { + auto d = obj(R"({"x":[0,1],"y":[0,1,2],"v":[[1,2],[3,4],[5,6]],"vmin":1,"vmax":6})"); + auto g = parseInversionGrid(d); + EXPECT_EQ(g.nx(), 2); EXPECT_EQ(g.ny(), 3); + EXPECT_DOUBLE_EQ(g.valueAt(1, 2), 6.0); + EXPECT_DOUBLE_EQ(g.vmax, 6.0); +} +TEST(DatasetChartDto, ParsesColorBar) { + auto d = obj(R"({"properties":{"colorBar":[["10","rgba(0,0,255,255)"],["20","rgba(255,0,0,255)"]]}})"); + auto cs = parseColorBar(d); + auto stops = cs.stopValues(); + ASSERT_EQ(stops.size(), 2u); + EXPECT_DOUBLE_EQ(stops[0], 10.0); + auto c = cs.colorAt(12.0); // [10,20) → 蓝 + EXPECT_GT(c.b, c.r); +} +TEST(DatasetChartDto, ParsesAnomalyPolyline) { + auto arr = QJsonDocument::fromJson( + R"([{"exceptionName":"A1","exceptionTypeName":"异常区","exceptionMarkType":2, + "legend":{"polylineColor":"#0D0101","polylineWidth":4,"polylineShape":"dash"}, + "location":{"coordinate":[{"x":1,"y":2},{"x":3,"y":4}]}}])").array(); + auto v = parseDatasetAnomalies(arr); + ASSERT_EQ(v.size(), 1u); + EXPECT_EQ(v[0].name, "A1"); + EXPECT_EQ(static_cast(v[0].markType), 2); + ASSERT_EQ(v[0].localPts.size(), 2u); + EXPECT_DOUBLE_EQ(v[0].localPts[1].x, 3.0); + EXPECT_TRUE(v[0].dashed); +} +``` + +- [ ] **Step 3: 注册 CMake + 跑确认失败** + +`src/data/CMakeLists.txt`:在 `target_sources(geopro_data PRIVATE ...)` 加 `dto/DatasetChartDto.cpp`。 +`tests/CMakeLists.txt`:data 段加 `target_sources(geopro_tests PRIVATE data/test_dataset_chart_dto.cpp)`。 +Run: `cmake --build build && ctest --test-dir build -R DatasetChartDto --output-on-failure` +Expected: 链接失败 + +- [ ] **Step 4: 实现** + +Create `src/data/dto/DatasetChartDto.cpp`: +```cpp +#include "dto/DatasetChartDto.hpp" +#include +#include +namespace geopro::data::dto { +using namespace geopro::core; + +static double num(const QJsonValue& v, double def = 0.0) { + if (v.isDouble()) return v.toDouble(); + if (v.isString()) { bool ok = false; double d = v.toString().toDouble(&ok); return ok ? d : def; } + return def; +} + +Grid parseInversionGrid(const QJsonObject& data) { + const QJsonArray x = data.value("x").toArray(); + const QJsonArray y = data.value("y").toArray(); + const QJsonArray v = data.value("v").toArray(); // [ny][nx] + const int nx = x.size(), ny = y.size(); + Grid g(nx < 1 ? 1 : nx, ny < 1 ? 1 : ny); + g.x.clear(); for (auto e : x) g.x.push_back(num(e)); + g.y.clear(); for (auto e : y) g.y.push_back(num(e)); + for (int j = 0; j < ny; ++j) { + const QJsonArray row = v.at(j).toArray(); + for (int i = 0; i < nx; ++i) { + const QJsonValue cell = row.at(i); + g.valueAt(i, j) = (cell.isNull() || cell.isUndefined()) ? std::nan("") : num(cell, std::nan("")); + } + } + g.vmin = num(data.value("vmin")); g.vmax = num(data.value("vmax")); + return g; +} + +ScatterField parseScatterGraph(const QJsonObject& data) { + ScatterField s; + auto fill = [&](const char* key, std::vector& dst) { + for (auto e : data.value(key).toArray()) dst.push_back(num(e)); }; + fill("xlist", s.x); fill("ylist", s.y); fill("hlist", s.z); fill("vlist", s.v); + fill("projectXList", s.projX); fill("projectYList", s.projY); + return s; +} + +ColorScale parseColorBar(const QJsonObject& data) { + ColorScale cs; + const QJsonArray bar = data.value("properties").toObject().value("colorBar").toArray(); + for (auto e : bar) { + const QJsonArray pair = e.toArray(); + if (pair.size() < 2) continue; + const double val = num(pair.at(0)); + const std::string rgba = pair.at(1).toString().toStdString(); + cs.addStop(val, parseColor(rgba, AlphaScale::Bit255)); + } + return cs; +} + +std::vector parseDatasetAnomalies(const QJsonArray& arr) { + std::vector out; + for (auto e : arr) { + const QJsonObject o = e.toObject(); + Anomaly a; + a.name = o.value("exceptionName").toString().toStdString(); + a.typeName = o.value("exceptionTypeName").toString().toStdString(); + a.markType = static_cast(o.value("exceptionMarkType").toInt(2)); + const QJsonObject lg = o.value("legend").toObject(); + a.lineColor = lg.value("polylineColor").toString("#000000").toStdString(); + a.lineWidth = lg.value("polylineWidth").toDouble(1.0); + a.dashed = lg.value("polylineShape").toString() == "dash"; + for (auto c : o.value("location").toObject().value("coordinate").toArray()) { + const QJsonObject p = c.toObject(); + a.localPts.push_back(Vec2{p.value("x").toDouble(), p.value("y").toDouble()}); + } + out.push_back(std::move(a)); + } + return out; +} +} // namespace geopro::data::dto +``` + +- [ ] **Step 5: 跑确认通过 + 提交** + +Run: `ctest --test-dir build -R DatasetChartDto --output-on-failure` +Expected: PASS +```bash +git add src/data/dto/DatasetChartDto.hpp src/data/dto/DatasetChartDto.cpp \ + src/data/CMakeLists.txt tests/data/test_dataset_chart_dto.cpp tests/CMakeLists.txt +git commit -m "feat(data): DatasetChartDto 解析 inversion网格/散点/colorBar/异常" +``` + +--- + +### Task 2.2:IDatasetRepository 增 loadScatterColorScale + ApiDatasetRepository + +**Files:** +- Modify: `src/data/repo/IDatasetRepository.hpp`, `src/data/repo/LocalSampleRepository.hpp` +- Create: `src/data/api/ApiDatasetRepository.hpp`, `src/data/api/ApiDatasetRepository.cpp` +- Modify: `src/data/CMakeLists.txt` + +- [ ] **Step 1: 接口增方法** + +`src/data/repo/IDatasetRepository.hpp`:在 `loadColorScale` 后加纯虚: +```cpp + virtual geopro::core::ColorScale loadScatterColorScale(const std::string& dsId) = 0; +``` +`src/data/repo/LocalSampleRepository.hpp`:给已有的 `loadScatterColorScale` 声明加 `override`(若未标)。 + +- [ ] **Step 2: 写头文件** + +Create `src/data/api/ApiDatasetRepository.hpp`: +```cpp +#pragma once +#include "repo/IDatasetRepository.hpp" +namespace geopro::net { class ApiClient; } +namespace geopro::data { + +// 真实 API 实现 IDatasetRepository(ERT 反演相关)。失败抛 std::runtime_error。 +class ApiDatasetRepository : public IDatasetRepository { +public: + explicit ApiDatasetRepository(net::ApiClient& api); + std::vector loadStructure() override { return {}; } // 不经此仓储取结构 + geopro::core::Grid loadGrid(const std::string& dsId) override; + geopro::core::ScatterField loadScatter(const std::string& dsId) override; + geopro::core::ColorScale loadColorScale(const std::string& dsId) override; // type2 网格 + geopro::core::ColorScale loadScatterColorScale(const std::string& dsId) override; // type1 原数据 + std::vector loadAnomalies(const std::string& dsId) override; +private: + net::ApiClient& api_; +}; +} // namespace geopro::data +``` + +- [ ] **Step 3: 实现** + +Create `src/data/api/ApiDatasetRepository.cpp`: +```cpp +#include "api/ApiDatasetRepository.hpp" +#include +#include +#include +#include +#include "ApiClient.hpp" +#include "dto/DatasetChartDto.hpp" +namespace geopro::data { +namespace { +QString enc(const std::string& s) { + return QString::fromUtf8(QUrl::toPercentEncoding(QString::fromStdString(s))); +} +void must(const net::ApiResponse& r, const char* what) { + if (r.code != 200) throw std::runtime_error(std::string(what) + " failed: " + + (r.msg.isEmpty() ? r.rawError.toStdString() : r.msg.toStdString())); +} +geopro::core::ColorScale colorScale(net::ApiClient& api, const std::string& dsId, int type) { + QJsonObject body{{"dsObjectId", QString::fromStdString(dsId)}, {"businessCode", ""}, {"type", type}}; + const net::ApiResponse r = api.postJson(QStringLiteral("/business/lvl/colorGradation/getDetail"), body); + must(r, "colorGradation"); + return dto::parseColorBar(r.data); +} +} // namespace + +ApiDatasetRepository::ApiDatasetRepository(net::ApiClient& api) : api_(api) {} + +geopro::core::Grid ApiDatasetRepository::loadGrid(const std::string& dsId) { + const net::ApiResponse r = api_.get( + QStringLiteral("/business/dd/ert/inversion/rows/%1").arg(enc(dsId))); + must(r, "inversion/rows"); + return dto::parseInversionGrid(r.data); +} +geopro::core::ScatterField ApiDatasetRepository::loadScatter(const std::string& dsId) { + const net::ApiResponse r = api_.get( + QStringLiteral("/business/dd/ert/inversion/getErtRawDataScatterGraph/%1").arg(enc(dsId))); + must(r, "scatterGraph"); + return dto::parseScatterGraph(r.data); +} +geopro::core::ColorScale ApiDatasetRepository::loadColorScale(const std::string& dsId) { + return colorScale(api_, dsId, 2); +} +geopro::core::ColorScale ApiDatasetRepository::loadScatterColorScale(const std::string& dsId) { + return colorScale(api_, dsId, 1); +} +std::vector ApiDatasetRepository::loadAnomalies(const std::string& dsId) { + const net::ApiResponse r = api_.get( + QStringLiteral("/business/exception/queryException/%1").arg(enc(dsId))); + must(r, "queryException"); + return dto::parseDatasetAnomalies(r.data.value(QStringLiteral("value")).toArray()); +} +} // namespace geopro::data +``` +> 注:`queryException` 的 `data` 是否含 `value` 包裹按现网验证调整——若 `r.data` 直接是数组语义,改为对应取法;本仓返回信封 `data` 为对象,数组在 `value` 字段(同 `loadExceptionsByTm`)。 +`src/data/CMakeLists.txt`:`target_sources(geopro_data PRIVATE api/ApiDatasetRepository.cpp)`。确认 `geopro_data` 链接了 `geopro_net`(`ApiClient`)——`ApiProjectRepository` 已用,故已链。 + +- [ ] **Step 4: 构建确认通过** + +Run: `cmake --build build` +Expected: 编译链接通过(无新测试,编译即验证签名一致)。 + +- [ ] **Step 5: 提交** + +```bash +git add src/data/repo/IDatasetRepository.hpp src/data/repo/LocalSampleRepository.hpp \ + src/data/api/ApiDatasetRepository.hpp src/data/api/ApiDatasetRepository.cpp src/data/CMakeLists.txt +git commit -m "feat(data): ApiDatasetRepository 接真实 API + loadScatterColorScale 提到接口" +``` + +--- + +## Phase 3:DatasetChartView(QGraphicsView 渲染器) + +### Task 3.1:DatasetChartView 散点/等值面/异常渲染 + +**Files:** +- Create: `src/app/panels/chart/DatasetChartView.hpp`, `src/app/panels/chart/DatasetChartView.cpp` +- Modify: `src/app/CMakeLists.txt` + +> 说明:UI 渲染类以集成/视觉验证为主,不做像素级单测。本任务给出可编译运行的骨架与渲染逻辑;坐标轴钉边重算在 Task 3.2。 + +- [ ] **Step 1: 写头文件** + +Create `src/app/panels/chart/DatasetChartView.hpp`: +```cpp +#pragma once +#include +#include +#include +#include "model/Field.hpp" +#include "model/ColorScale.hpp" +#include "model/Anomaly.hpp" +#include "ContourBands.hpp" +namespace geopro::app { + +// 平面图表视图:散点(原数据) / 等值面(网格) + 异常叠加 + 色阶图例。VTK 仅经 ContourBands 算几何。 +class DatasetChartView : public QGraphicsView { + Q_OBJECT +public: + explicit DatasetChartView(QWidget* parent = nullptr); + void showScatter(const geopro::core::ScatterField& f, const geopro::core::ColorScale& cs); + void showContour(const geopro::core::Grid& g, const geopro::core::ColorScale& cs, + const geopro::render::ContourOptions& opt); + void setAnomalies(const std::vector& list); + void setHiddenAnomalies(const std::set& hidden); // 下标=list 序 + void setShowAnomalies(bool on); + void setShowContourLines(bool on); + void clearChart(); +protected: + void wheelEvent(QWheelEvent* e) override; // 滚轮缩放 +private: + void rebuildAnomalyItems(); + QGraphicsScene* scene_; + std::vector anomalies_; + std::set hidden_; + bool showAnomalies_ = true; + bool showContourLines_ = true; + std::vector anomalyItems_; +}; +} // namespace geopro::app +``` + +- [ ] **Step 2: 实现** + +Create `src/app/panels/chart/DatasetChartView.cpp`: +```cpp +#include "panels/chart/DatasetChartView.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +namespace geopro::app { +using geopro::core::Rgba; + +static QColor toQ(const Rgba& c) { return QColor(c.r, c.g, c.b, c.a); } + +DatasetChartView::DatasetChartView(QWidget* parent) + : QGraphicsView(parent), scene_(new QGraphicsScene(this)) { + setScene(scene_); + setRenderHint(QPainter::Antialiasing, true); + setDragMode(QGraphicsView::ScrollHandDrag); + // 场景 y 向上为正:用 y 翻转的变换(QGraphicsView 默认 y 向下)。 + scale(1, -1); +} + +void DatasetChartView::clearChart() { + scene_->clear(); anomalyItems_.clear(); +} + +void DatasetChartView::showScatter(const geopro::core::ScatterField& f, const geopro::core::ColorScale& cs) { + clearChart(); + const double sz = 0.6; // 方块边长(数据单位,近似 web 方点) + for (size_t i = 0; i < f.v.size(); ++i) { + auto* r = scene_->addRect(f.x[i] - sz / 2, f.y[i] - sz / 2, sz, sz, + QPen(Qt::white, 0), QBrush(toQ(cs.colorAt(f.v[i])))); + r->setPen(QPen(Qt::white, 0)); // 白描边 + } + rebuildAnomalyItems(); + fitInView(scene_->itemsBoundingRect(), Qt::KeepAspectRatio); // x:y 等比 +} + +void DatasetChartView::showContour(const geopro::core::Grid& g, const geopro::core::ColorScale& cs, + const geopro::render::ContourOptions& opt) { + clearChart(); + const auto r = geopro::render::buildContourBands(g, cs, opt); + for (const auto& b : r.bands) { + QPainterPath path; if (b.ring.empty()) continue; + path.moveTo(b.ring[0].x, b.ring[0].y); + for (size_t k = 1; k < b.ring.size(); ++k) path.lineTo(b.ring[k].x, b.ring[k].y); + path.closeSubpath(); + auto* it = scene_->addPath(path, QPen(Qt::NoPen), QBrush(toQ(b.color))); + it->setZValue(0); + } + if (showContourLines_) + for (const auto& l : r.lines) { + QPainterPath path; path.moveTo(l.pts[0].x, l.pts[0].y); + for (size_t k = 1; k < l.pts.size(); ++k) path.lineTo(l.pts[k].x, l.pts[k].y); + auto* it = scene_->addPath(path, QPen(QColor(0, 0, 0), 0)); + it->setZValue(1); + } + rebuildAnomalyItems(); + fitInView(scene_->itemsBoundingRect(), Qt::IgnoreAspectRatio); // 剖面 X/Y 各自铺满 +} + +void DatasetChartView::setAnomalies(const std::vector& list) { + anomalies_ = list; rebuildAnomalyItems(); +} +void DatasetChartView::setHiddenAnomalies(const std::set& hidden) { hidden_ = hidden; rebuildAnomalyItems(); } +void DatasetChartView::setShowAnomalies(bool on) { showAnomalies_ = on; rebuildAnomalyItems(); } +void DatasetChartView::setShowContourLines(bool on) { showContourLines_ = on; } + +void DatasetChartView::rebuildAnomalyItems() { + for (auto* it : anomalyItems_) scene_->removeItem(it), delete it; + anomalyItems_.clear(); + if (!showAnomalies_) return; + for (int i = 0; i < static_cast(anomalies_.size()); ++i) { + if (hidden_.count(i)) continue; + const auto& a = anomalies_[i]; + if (a.localPts.size() < 2) continue; + QPainterPath path; path.moveTo(a.localPts[0].x, a.localPts[0].y); + for (size_t k = 1; k < a.localPts.size(); ++k) path.lineTo(a.localPts[k].x, a.localPts[k].y); + if (static_cast(a.markType) == 3) path.closeSubpath(); + QPen pen(QColor(QString::fromStdString(a.lineColor)), 0); + if (a.dashed) pen.setStyle(Qt::DashLine); + auto* it = scene_->addPath(path, pen); + it->setZValue(2); + anomalyItems_.push_back(it); + } +} + +void DatasetChartView::wheelEvent(QWheelEvent* e) { + const double f = e->angleDelta().y() > 0 ? 1.15 : 1 / 1.15; + scale(f, f); +} +} // namespace geopro::app +``` +`src/app/CMakeLists.txt`:把 `panels/chart/DatasetChartView.cpp` 加入 app 目标的 `target_sources`(与其它 panel 同处);确认 app 目标 include 了 `src/render` 头路径(已链 geopro_render 则传递;若无,`target_link_libraries(... geopro_render)` 已存在即可)。 + +- [ ] **Step 3: 构建确认通过** + +Run: `cmake --build build` +Expected: 编译通过(moc 处理 Q_OBJECT)。 + +- [ ] **Step 4: 提交** + +```bash +git add src/app/panels/chart/DatasetChartView.hpp src/app/panels/chart/DatasetChartView.cpp src/app/CMakeLists.txt +git commit -m "feat(ui): DatasetChartView 散点/等值面/异常叠加(QGraphicsView)" +``` + +--- + +### Task 3.2:坐标轴 overlay + 色阶图例 + 等比 + +**Files:** +- Modify: `src/app/panels/chart/DatasetChartView.hpp/.cpp` + +- [ ] **Step 1: 加坐标轴与图例绘制** + +在 `DatasetChartView` 重写 `drawForeground(QPainter*, const QRectF&)`:用 `mapFromScene` 把场景刻度投影到视口画轴线/刻度文字(轴钉视口边缘,缩放/平移随 `drawForeground` 自动重绘);底部画色阶图例(离散色带 + 分段值)。 +头文件加: +```cpp +protected: + void drawForeground(QPainter* p, const QRectF& rect) override; +private: + geopro::core::ColorScale legendScale_; + void setLegend(const geopro::core::ColorScale& cs) { legendScale_ = cs; } +``` +在 `showScatter`/`showContour` 末尾调用 `setLegend(cs);` 并 `viewport()->update();`。 +`drawForeground` 实现:以 `viewport()->rect()` 为基;遍历场景包围盒 x/y 范围取若干刻度,`mapFromScene(QPointF(sx, sy))` 得像素位置画 `p->drawLine`/`p->drawText`;底部按 `legendScale_.stopValues()` 平均分段画矩形 + 文字。 +(完整像素代码在实现时据视觉对照微调,逻辑:轴线沿 viewport 左/下边,刻度 6~8 个,图例条高约 16px 居底。) + +- [ ] **Step 2: 构建 + 手动运行核对** + +Run: `cmake --build build`,启动 app(`build/src/app/geopro_app` 或既有启动脚本),选中一个反演数据集,肉眼对照 `docs/superpowers/specs/assets/web-datasetinfo-*.png`:散点等比、等值面带边、异常虚线、底部色阶图例、坐标轴刻度。 +Expected: 视觉与 web 接近(~95%)。 + +- [ ] **Step 3: 提交** + +```bash +git add src/app/panels/chart/DatasetChartView.hpp src/app/panels/chart/DatasetChartView.cpp +git commit -m "feat(ui): DatasetChartView 坐标轴 overlay(钉边重算)+底部色阶图例" +``` + +--- + +## Phase 4:dd 策略框架 + DatasetDetailController + +### Task 4.1:策略接口 + 注册表(含降级) + +**Files:** +- Create: `src/app/panels/chart/IDatasetChartStrategy.hpp` +- Test: `tests/app/test_chart_strategy_registry.cpp`, `tests/CMakeLists.txt` + +- [ ] **Step 1: 写头文件** + +Create `src/app/panels/chart/IDatasetChartStrategy.hpp`: +```cpp +#pragma once +#include +#include +#include +namespace geopro::app { + +class DatasetDetailPage; // 前置 +namespace geopro::data { } + +// dd 类型驱动的图表策略:决定某 ddCode 的详情页如何加载/渲染。 +struct IDatasetChartStrategy { + virtual ~IDatasetChartStrategy() = default; + virtual std::string ddCode() const = 0; +}; + +class ChartStrategyRegistry { +public: + void add(std::unique_ptr s) { + const std::string code = s->ddCode(); + map_[code] = std::move(s); + } + IDatasetChartStrategy* find(const std::string& ddCode) const { + auto it = map_.find(ddCode); + return it == map_.end() ? nullptr : it->second.get(); + } + bool supports(const std::string& ddCode) const { return map_.count(ddCode) > 0; } +private: + std::map> map_; +}; +} // namespace geopro::app +``` + +- [ ] **Step 2: 写失败测试** + +Create `tests/app/test_chart_strategy_registry.cpp`: +```cpp +#include +#include "panels/chart/IDatasetChartStrategy.hpp" +using namespace geopro::app; +namespace { +struct Fake : IDatasetChartStrategy { std::string ddCode() const override { return "dd_inversion_data"; } }; +} +TEST(ChartStrategyRegistry, FindsRegisteredAndDegradesUnknown) { + ChartStrategyRegistry reg; + reg.add(std::make_unique()); + EXPECT_TRUE(reg.supports("dd_inversion_data")); + EXPECT_NE(reg.find("dd_inversion_data"), nullptr); + EXPECT_FALSE(reg.supports("dd_unknown")); + EXPECT_EQ(reg.find("dd_unknown"), nullptr); +} +``` + +- [ ] **Step 3: 注册 CMake + 跑确认失败 → 通过** + +`tests/CMakeLists.txt`:新增一段把 `app/test_chart_strategy_registry.cpp` 加入 `geopro_tests` 并 `target_link_libraries(geopro_tests PRIVATE geopro_app)`(若 `geopro_app` 是可执行而非库,则改为仅 `target_include_directories(geopro_tests PRIVATE ${CMAKE_SOURCE_DIR}/src/app)`,因该测试只用头文件 `IDatasetChartStrategy.hpp`,无需链 app)。优先后者(纯头测试): +```cmake +target_include_directories(geopro_tests PRIVATE ${CMAKE_SOURCE_DIR}/src/app) +target_sources(geopro_tests PRIVATE app/test_chart_strategy_registry.cpp) +``` +Run: `cmake --build build && ctest --test-dir build -R ChartStrategyRegistry --output-on-failure` +Expected: PASS + +- [ ] **Step 4: 提交** + +```bash +git add src/app/panels/chart/IDatasetChartStrategy.hpp tests/app/test_chart_strategy_registry.cpp tests/CMakeLists.txt +git commit -m "feat(ui): dd 图表策略接口 + 注册表(未注册降级)" +``` + +--- + +### Task 4.2:DatasetDetailController + ErtInversionStrategy + +**Files:** +- Create: `src/controller/DatasetDetailController.hpp/.cpp`, `src/app/panels/chart/ErtInversionStrategy.hpp/.cpp` +- Modify: `src/controller/CMakeLists.txt`, `src/app/CMakeLists.txt` + +- [ ] **Step 1: 写控制器头** + +Create `src/controller/DatasetDetailController.hpp`: +```cpp +#pragma once +#include +#include +#include +#include "model/Field.hpp" +#include "model/ColorScale.hpp" +#include "model/Anomaly.hpp" +namespace geopro::data { class IDatasetRepository; } +namespace geopro::controller { + +// 数据详情编排:单击/双击数据集 → 拉 散点/网格/色阶/异常 → 发信号给详情面板。被动视图。 +// 仅服务图表,不与 WorkbenchNavController(项目/结构导航)耦合。 +class DatasetDetailController : public QObject { + Q_OBJECT +public: + struct ChartData { + QString dsId, ddCode; + geopro::core::ScatterField scatter; + geopro::core::ColorScale scatterScale; + geopro::core::Grid grid; + geopro::core::ColorScale gridScale; + std::vector anomalies; + }; + explicit DatasetDetailController(data::IDatasetRepository& repo, QObject* parent = nullptr); +public slots: + void openDataset(const QString& dsId, const QString& ddCode); // 双击=新建/聚焦页 + void focusDataset(const QString& dsId); // 单击=聚焦已开页 +signals: + void chartReady(const ChartData& data); + void focusRequested(const QString& dsId); + void loadFailed(const QString& dsId, const QString& message); +private: + data::IDatasetRepository& repo_; +}; +} // namespace geopro::controller +``` + +- [ ] **Step 2: 实现控制器** + +Create `src/controller/DatasetDetailController.cpp`: +```cpp +#include "DatasetDetailController.hpp" +#include +#include "repo/IDatasetRepository.hpp" +namespace geopro::controller { + +DatasetDetailController::DatasetDetailController(data::IDatasetRepository& repo, QObject* parent) + : QObject(parent), repo_(repo) {} + +void DatasetDetailController::openDataset(const QString& dsId, const QString& ddCode) { + const std::string id = dsId.toStdString(); + ChartData d; d.dsId = dsId; d.ddCode = ddCode; + try { + d.scatter = repo_.loadScatter(id); + d.scatterScale = repo_.loadScatterColorScale(id); + d.grid = repo_.loadGrid(id); + d.gridScale = repo_.loadColorScale(id); + d.anomalies = repo_.loadAnomalies(id); + } catch (const std::exception& e) { + emit loadFailed(dsId, QString::fromStdString(e.what())); + return; + } + emit chartReady(d); +} + +void DatasetDetailController::focusDataset(const QString& dsId) { emit focusRequested(dsId); } +} // namespace geopro::controller +``` +`src/controller/CMakeLists.txt`:把 `DatasetDetailController.cpp` 加入 `geopro_controller` 的 `target_sources`;确认链接 `geopro_data`(接口)+ `Qt6::Core`(已有)。 + +- [ ] **Step 3: ErtInversionStrategy(标识用,渲染由页驱动)** + +Create `src/app/panels/chart/ErtInversionStrategy.hpp`: +```cpp +#pragma once +#include "panels/chart/IDatasetChartStrategy.hpp" +namespace geopro::app { +struct ErtInversionStrategy : IDatasetChartStrategy { + std::string ddCode() const override { return "dd_inversion_data"; } +}; +} // namespace geopro::app +``` +> 首版 ErtInversion 仅作 ddCode 标识/能力声明;具体「散点+等值面」渲染由 `DatasetDetailPage` 消费 `ChartData` 完成(见 Task 5.2)。后续 dd 类型可把渲染细节下沉到策略。无 .cpp。 + +- [ ] **Step 4: 构建确认通过 + 提交** + +Run: `cmake --build build` +Expected: 通过 +```bash +git add src/controller/DatasetDetailController.hpp src/controller/DatasetDetailController.cpp \ + src/controller/CMakeLists.txt src/app/panels/chart/ErtInversionStrategy.hpp +git commit -m "feat(controller): DatasetDetailController 编排 + ErtInversionStrategy 标识" +``` + +--- + +## Phase 5:异常表 + 详情页 + 多 Tab 壳 + +### Task 5.1:AnomalyTablePanel(行显隐→图表) + +**Files:** +- Create: `src/app/panels/AnomalyTablePanel.hpp/.cpp` +- Modify: `src/app/CMakeLists.txt` + +- [ ] **Step 1: 写头文件** + +Create `src/app/panels/AnomalyTablePanel.hpp`: +```cpp +#pragma once +#include +#include +#include +#include "model/Anomaly.hpp" +class QTableWidget; +namespace geopro::app { + +// ds 级异常表:名称/异常类型/几何类型/创建时间/备注/操作(显隐眼睛)。行显隐 → 信号驱动图表叠加。 +class AnomalyTablePanel : public QWidget { + Q_OBJECT +public: + explicit AnomalyTablePanel(QWidget* parent = nullptr); + void setAnomalies(const std::vector& list, + const std::vector& createTimes, + const std::vector& remarks); +signals: + void hiddenChanged(const std::set& hiddenIndices); +private: + QTableWidget* table_; + std::set hidden_; +}; +} // namespace geopro::app +``` + +- [ ] **Step 2: 实现** + +Create `src/app/panels/AnomalyTablePanel.cpp`: +```cpp +#include "panels/AnomalyTablePanel.hpp" +#include +#include +#include +#include +namespace geopro::app { +static QString markName(int t) { return t == 1 ? "点" : t == 3 ? "多边形" : "多段线"; } + +AnomalyTablePanel::AnomalyTablePanel(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); lay->setContentsMargins(0, 0, 0, 0); + table_ = new QTableWidget(this); + table_->setColumnCount(6); + table_->setHorizontalHeaderLabels({"名称", "异常类型", "几何类型", "创建时间", "备注", "操作"}); + table_->horizontalHeader()->setStretchLastSection(true); + table_->setEditTriggers(QAbstractItemView::NoEditTriggers); + lay->addWidget(table_); +} + +void AnomalyTablePanel::setAnomalies(const std::vector& list, + const std::vector& createTimes, + const std::vector& remarks) { + hidden_.clear(); + table_->setRowCount(static_cast(list.size())); + for (int i = 0; i < static_cast(list.size()); ++i) { + const auto& a = list[i]; + table_->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(a.name))); + table_->setItem(i, 1, new QTableWidgetItem(QString::fromStdString(a.typeName))); + table_->setItem(i, 2, new QTableWidgetItem(markName(static_cast(a.markType)))); + table_->setItem(i, 3, new QTableWidgetItem(i < (int)createTimes.size() ? createTimes[i] : "")); + table_->setItem(i, 4, new QTableWidgetItem(i < (int)remarks.size() ? remarks[i] : "")); + auto* eye = new QToolButton(table_); eye->setCheckable(true); eye->setChecked(true); + eye->setText("👁"); + connect(eye, &QToolButton::toggled, this, [this, i](bool on) { + if (on) hidden_.erase(i); else hidden_.insert(i); + emit hiddenChanged(hidden_); + }); + table_->setCellWidget(i, 5, eye); + } +} +} // namespace geopro::app +``` +`src/app/CMakeLists.txt`:加 `panels/AnomalyTablePanel.cpp`。 + +- [ ] **Step 3: 构建 + 提交** + +Run: `cmake --build build` +```bash +git add src/app/panels/AnomalyTablePanel.hpp src/app/panels/AnomalyTablePanel.cpp src/app/CMakeLists.txt +git commit -m "feat(ui): AnomalyTablePanel ds级异常表(行眼睛→隐藏集信号)" +``` + +--- + +### Task 5.2:DatasetDetailPage(标题+切换+图表+异常表) + +**Files:** +- Create: `src/app/panels/DatasetDetailPage.hpp/.cpp` +- Modify: `src/app/CMakeLists.txt` + +- [ ] **Step 1: 写头文件** + +Create `src/app/panels/DatasetDetailPage.hpp`: +```cpp +#pragma once +#include +#include "DatasetDetailController.hpp" // ChartData +namespace geopro::app { +class DatasetChartView; class AnomalyTablePanel; + +// 单个数据集详情页:标题 + 原数据/网格数据 切换 + 叠加开关 + 图表 + 异常表。 +class DatasetDetailPage : public QWidget { + Q_OBJECT +public: + explicit DatasetDetailPage(QWidget* parent = nullptr); + void setData(const geopro::controller::DatasetDetailController::ChartData& d); + QString dsId() const { return dsId_; } +private: + void showScatterMode(); void showGridMode(); + QString dsId_; + geopro::controller::DatasetDetailController::ChartData data_; + DatasetChartView* chart_; + AnomalyTablePanel* anomalyTable_; + bool gridMode_ = true; +}; +} // namespace geopro::app +``` + +- [ ] **Step 2: 实现(接线切换/叠加/异常联动)** + +Create `src/app/panels/DatasetDetailPage.cpp`: +```cpp +#include "panels/DatasetDetailPage.hpp" +#include +#include +#include +#include +#include +#include +#include "panels/chart/DatasetChartView.hpp" +#include "panels/AnomalyTablePanel.hpp" +namespace geopro::app { + +DatasetDetailPage::DatasetDetailPage(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); + auto* bar = new QHBoxLayout(); + auto* origin = new QToolButton(this); origin->setText("原数据"); origin->setCheckable(true); + auto* grid = new QToolButton(this); grid->setText("网格数据"); grid->setCheckable(true); + grid->setChecked(true); + auto* grp = new QButtonGroup(this); grp->setExclusive(true); grp->addButton(origin); grp->addButton(grid); + auto* showAnom = new QCheckBox("显示异常", this); showAnom->setChecked(true); + auto* showLines = new QCheckBox("显示等值线", this); showLines->setChecked(true); + bar->addWidget(origin); bar->addWidget(grid); bar->addStretch(); + bar->addWidget(showAnom); bar->addWidget(showLines); + lay->addLayout(bar); + + chart_ = new DatasetChartView(this); + anomalyTable_ = new AnomalyTablePanel(this); + lay->addWidget(chart_, 3); + lay->addWidget(anomalyTable_, 1); + + connect(grid, &QToolButton::clicked, this, [this] { gridMode_ = true; showGridMode(); }); + connect(origin, &QToolButton::clicked, this, [this] { gridMode_ = false; showScatterMode(); }); + connect(showAnom, &QCheckBox::toggled, chart_, [this](bool on) { chart_->setShowAnomalies(on); }); + connect(showLines, &QCheckBox::toggled, chart_, [this](bool on) { + chart_->setShowContourLines(on); if (gridMode_) showGridMode(); }); + connect(anomalyTable_, &AnomalyTablePanel::hiddenChanged, chart_, + [this](const std::set& h) { chart_->setHiddenAnomalies(h); }); +} + +void DatasetDetailPage::setData(const geopro::controller::DatasetDetailController::ChartData& d) { + dsId_ = d.dsId; data_ = d; + chart_->setAnomalies(d.anomalies); + anomalyTable_->setAnomalies(d.anomalies, {}, {}); // 创建时间/备注可后续从 VO 补 + if (gridMode_) showGridMode(); else showScatterMode(); +} +void DatasetDetailPage::showGridMode() { + geopro::render::ContourOptions opt; // 默认 2x+smooth+simplify0.5 + chart_->showContour(data_.grid, data_.gridScale, opt); +} +void DatasetDetailPage::showScatterMode() { + chart_->showScatter(data_.scatter, data_.scatterScale); +} +} // namespace geopro::app +``` +`src/app/CMakeLists.txt`:加 `panels/DatasetDetailPage.cpp`;确认 app 链接 `geopro_controller`(用 ChartData)——main 已链则已有。 + +- [ ] **Step 3: 构建 + 提交** + +Run: `cmake --build build` +```bash +git add src/app/panels/DatasetDetailPage.hpp src/app/panels/DatasetDetailPage.cpp src/app/CMakeLists.txt +git commit -m "feat(ui): DatasetDetailPage 原数据/网格切换+叠加开关+异常联动" +``` + +--- + +### Task 5.3:DatasetDetailPanel(多 Tab,按 dsId 去重) + +**Files:** +- Create: `src/app/panels/DatasetDetailPanel.hpp/.cpp` +- Modify: `src/app/CMakeLists.txt` + +- [ ] **Step 1: 写头文件** + +Create `src/app/panels/DatasetDetailPanel.hpp`: +```cpp +#pragma once +#include +#include "DatasetDetailController.hpp" +namespace geopro::app { +class DatasetDetailPage; + +// 多 Tab 壳:每数据集一页(按 dsId 去重)。R095。 +class DatasetDetailPanel : public QTabWidget { + Q_OBJECT +public: + explicit DatasetDetailPanel(QWidget* parent = nullptr); + void openOrUpdate(const geopro::controller::DatasetDetailController::ChartData& d); // 双击/数据到达 + void focusDataset(const QString& dsId); // 单击聚焦已开页 +signals: + void activeDatasetChanged(const QString& dsId); // 反向联动数据集列表 +private: + DatasetDetailPage* pageFor(const QString& dsId) const; +}; +} // namespace geopro::app +``` + +- [ ] **Step 2: 实现** + +Create `src/app/panels/DatasetDetailPanel.cpp`: +```cpp +#include "panels/DatasetDetailPanel.hpp" +#include "panels/DatasetDetailPage.hpp" +namespace geopro::app { + +DatasetDetailPanel::DatasetDetailPanel(QWidget* parent) : QTabWidget(parent) { + setTabsClosable(true); + connect(this, &QTabWidget::tabCloseRequested, this, [this](int i) { delete widget(i); }); + connect(this, &QTabWidget::currentChanged, this, [this](int i) { + if (auto* p = qobject_cast(widget(i))) + emit activeDatasetChanged(p->dsId()); + }); +} + +DatasetDetailPage* DatasetDetailPanel::pageFor(const QString& dsId) const { + for (int i = 0; i < count(); ++i) + if (auto* p = qobject_cast(widget(i))) + if (p->dsId() == dsId) return p; + return nullptr; +} + +void DatasetDetailPanel::openOrUpdate(const geopro::controller::DatasetDetailController::ChartData& d) { + auto* p = pageFor(d.dsId); + if (!p) { p = new DatasetDetailPage(this); addTab(p, d.dsId); } // 标题后续可换 ds 名 + p->setData(d); + setCurrentWidget(p); +} +void DatasetDetailPanel::focusDataset(const QString& dsId) { + if (auto* p = pageFor(dsId)) setCurrentWidget(p); +} +} // namespace geopro::app +``` +`src/app/CMakeLists.txt`:加 `panels/DatasetDetailPanel.cpp`。 + +- [ ] **Step 3: 构建 + 提交** + +Run: `cmake --build build` +```bash +git add src/app/panels/DatasetDetailPanel.hpp src/app/panels/DatasetDetailPanel.cpp src/app/CMakeLists.txt +git commit -m "feat(ui): DatasetDetailPanel 多Tab壳(按dsId去重+反向联动信号)" +``` + +--- + +## Phase 6:main.cpp 接线(移除旧 VTK 详情,接上新面板) + +### Task 6.1:替换详情 dock + 接上控制器与单击/双击 + +**Files:** +- Modify: `src/app/main.cpp` + +- [ ] **Step 1: 移除旧 VTK 详情 dock 构建** + +删除 `main.cpp:451-523`(`detailWidget`/`detailRenderWindow`/`detailRenderer`/`detailContainer`/`detailToolBar`/`actScatter`/`actSection`/`actShow*`/`detailDock` 整段)与 `main.cpp:602-702` 的 `currentDsId/detailMode/showAnomalies/...rebuildDetail` lambda 及其在 716-742、795-800 的相关连接。保留 `rebuildCentral`(中央场景)。 +> 注:`rebuildDetail` 在 795-800「VTK 背景随主题」连接中与 `rebuildCentral` 同列,删除其中 `rebuildDetail` 调用即可,保留 `rebuildCentral`。 + +- [ ] **Step 2: 新建详情面板 dock + 控制器** + +在 `buildWorkbench` 内(原 detailDock 位置)替换为: +```cpp + auto* detailPanel = new geopro::app::DatasetDetailPanel(); + auto* detailDock = new ads::CDockWidget(QStringLiteral("数据详情")); + detailDock->setWidget(wrapWithHeader(geopro::app::Glyph::Detail, QStringLiteral("数据详情"), detailPanel)); + dockManager->addDockWidget(ads::BottomDockWidgetArea, detailDock, centerDockArea); +``` +`buildWorkbench` 签名加参 `geopro::controller::DatasetDetailController& detailCtrl`(在 `WorkbenchNavController& nav` 之后)。文件顶部 include 新头:`panels/DatasetDetailPanel.hpp`、`DatasetDetailController.hpp`。 + +- [ ] **Step 3: 接线单击/双击 + 反向联动** + +把 `main.cpp:705-713` 的 datasetList 单击 lambda 改为同时联动控制器: +```cpp + QObject::connect(datasetList, &QListWidget::itemClicked, datasetList, + [&nav, &detailCtrl](QListWidgetItem* item) { + if (item->data(geopro::app::kDsLoadMoreRole).toBool()) { nav.loadMoreData(); return; } + const QString dsId = item->data(geopro::app::kDsIdRole).toString(); + if (dsId.isEmpty()) return; + nav.selectDataset(dsId); // 属性表单(现状) + detailCtrl.focusDataset(dsId); // 单击=聚焦已开页 + }); + QObject::connect(datasetList, &QListWidget::itemDoubleClicked, datasetList, + [&detailCtrl](QListWidgetItem* item) { + const QString dsId = item->data(geopro::app::kDsIdRole).toString(); + const QString ddCode = item->data(geopro::app::kDsDdCodeRole).toString(); + if (!dsId.isEmpty()) detailCtrl.openDataset(dsId, ddCode); // 双击=新建/聚焦页 + }); + QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::chartReady, + detailPanel, [detailPanel](const geopro::controller::DatasetDetailController::ChartData& d) { + detailPanel->openOrUpdate(d); + }); + QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::focusRequested, + detailPanel, [detailPanel](const QString& dsId) { detailPanel->focusDataset(dsId); }); + QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::activeDatasetChanged, + datasetList, [datasetList](const QString& dsId) { + for (int i = 0; i < datasetList->count(); ++i) + if (datasetList->item(i)->data(geopro::app::kDsIdRole).toString() == dsId) + datasetList->setCurrentRow(i); + }); +``` +> 需确认列表项是否存了 `kDsDdCodeRole`(ddCode)。若无:在填充 datasetList 的 `datasetsLoaded` 连接处(`main.cpp:908`)给每项 `setData(kDsDdCodeRole, QString::fromStdString(row.ddCode))`,并在 `Glyphs`/角色定义处加 `constexpr int kDsDdCodeRole = Qt::UserRole + N;`(取未占用值)。 + +- [ ] **Step 4: 构造控制器并传入** + +在 `main()`(`main.cpp:1046-1060` 附近): +```cpp + geopro::data::ApiDatasetRepository datasetRepo(api); // api 为现有 ApiClient + geopro::controller::DatasetDetailController detailCtrl(datasetRepo); +``` +并把 `buildWorkbench(*window, repo, projectRepo, nav);` 改为 `buildWorkbench(*window, repo, projectRepo, nav, detailCtrl);`。include `api/ApiDatasetRepository.hpp`、`DatasetDetailController.hpp`。 +> `repo`(LocalSampleRepository)现已不被详情使用——若中央场景仍用 `repo.loadGrid("grid1")`(`main.cpp:241`)则保留;否则一并清理其未用引用(仅清理本次改动产生的孤儿)。 + +- [ ] **Step 5: 构建 + 手动验证 + 提交** + +Run: `cmake --build build`,启动 app:单击数据集→聚焦/属性;双击→详情页打开真实反演剖面(散点/网格切换、异常虚线叠加、异常表、色阶图例);切换 Tab 反向高亮列表。 +```bash +git add src/app/main.cpp src/app/Glyphs.hpp +git commit -m "feat(ui): 接线数据详情面板(移除旧VTK详情)+单击聚焦/双击打开/反向联动" +``` + +--- + +## Phase 7:集成测试 + 视觉核对 + +### Task 7.1:DatasetDetailController 编排集成测试 + +**Files:** +- Create: `tests/controller/test_dataset_detail_controller.cpp` +- Modify: `tests/CMakeLists.txt` + +- [ ] **Step 1: 写测试(用桩 repo)** + +Create `tests/controller/test_dataset_detail_controller.cpp`: +```cpp +#include +#include +#include "DatasetDetailController.hpp" +#include "repo/IDatasetRepository.hpp" +using namespace geopro; +namespace { +struct StubRepo : data::IDatasetRepository { + bool fail = false; + std::vector loadStructure() override { return {}; } + core::Grid loadGrid(const std::string&) override { if (fail) throw std::runtime_error("x"); core::Grid g(2,2); g.x={0,1}; g.y={0,1}; return g; } + core::ScatterField loadScatter(const std::string&) override { return {}; } + core::ColorScale loadColorScale(const std::string&) override { return {}; } + core::ColorScale loadScatterColorScale(const std::string&) override { return {}; } + std::vector loadAnomalies(const std::string&) override { return {}; } +}; +} +TEST(DatasetDetailController, EmitsChartReadyOnSuccess) { + StubRepo repo; + controller::DatasetDetailController c(repo); + QSignalSpy spy(&c, &controller::DatasetDetailController::chartReady); + c.openDataset("ds1", "dd_inversion_data"); + EXPECT_EQ(spy.count(), 1); +} +TEST(DatasetDetailController, EmitsLoadFailedOnThrow) { + StubRepo repo; repo.fail = true; + controller::DatasetDetailController c(repo); + QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed); + c.openDataset("ds1", "dd_inversion_data"); + EXPECT_EQ(spy.count(), 1); +} +``` + +- [ ] **Step 2: 注册 + 跑确认通过** + +`tests/CMakeLists.txt`:加 `target_sources(geopro_tests PRIVATE controller/test_dataset_detail_controller.cpp)` 并 `target_link_libraries(geopro_tests PRIVATE geopro_controller)`(+ `Qt6::Test` 用于 QSignalSpy:`find_package(Qt6 COMPONENTS Test REQUIRED)` 与 `Qt6::Test` 链接)。 +Run: `cmake --build build && ctest --test-dir build -R DatasetDetailController --output-on-failure` +Expected: 两个 PASS + +- [ ] **Step 3: 提交** + +```bash +git add tests/controller/test_dataset_detail_controller.cpp tests/CMakeLists.txt +git commit -m "test(controller): DatasetDetailController chartReady/loadFailed 编排" +``` + +--- + +### Task 7.2:全量回归 + 视觉核对 + +- [ ] **Step 1: 全量测试** + +Run: `ctest --test-dir build --output-on-failure` +Expected: 全绿(含既有用例不回归)。 + +- [ ] **Step 2: 视觉核对** + +启动 app,选真实反演数据集,逐项对照 `docs/superpowers/specs/assets/web-datasetinfo-scatter.png` 与 `web-datasetinfo-grid-with-anomaly.png`: +- 原数据:方块散点、色阶、等比 +- 网格:色带 + 等值线 + 不规则白边(凸包裁剪)、底部色阶 +- 异常:剖面虚线叠加 + 底部异常表行、眼睛切换隐藏叠加 +- 多 Tab:双击多个数据集分别成页、单击/Tab 切换反向高亮 + +- [ ] **Step 3: code review** + +按 `~/.claude/rules` 用 cpp-reviewer 审查本分支改动;处理 CRITICAL/HIGH。 + +- [ ] **Step 4: 收尾提交(如有修正)** + +```bash +git add -A && git commit -m "fix: 视觉核对与 review 修正" +``` + +--- + +## 自审(Spec 覆盖核对) + +- §2.2 范围内逐项 → Phase 1-6 均有任务覆盖(散点/等值面/色阶/叠加开关/异常叠加+表/多Tab)。✓ +- §3 源码契约(Plotly 方块/marching-squares 引擎/4 接口/colorBar/异常)→ Task 3.1(方块)、Phase1(等值面+预处理)、Task 2.1-2.2(接口+解析)。✓ +- §5 组件(ContourBuilder→ContourBands、DatasetChartView、策略、ApiDatasetRepository、AnomalyTablePanel、DatasetDetailPage/Panel、DatasetDetailController)→ 一一对应任务。✓ +- §6 模型映射 → Task 2.1 DTO 解析 + 单测。✓ +- §7 数据流/Tab/异常联动 → Task 5.2/5.3/6.1。✓ +- §7.3 异常归属(右上不动 + 详情底部表)→ 仅新增详情底部表,右上面板不触及。✓ +- §8 混合渲染(上采样+平滑+banded+凸包裁剪+简化)→ Task 1.2-1.4。✓ +- §9 错误/空态 → Task 4.2 loadFailed、Task 4.1 注册表降级、空数据安全(ContourBands 退化返回空)。✓ +- §10 测试 → Task 1.x/2.1/4.1/7.1 单元+集成。✓ + +**命名一致性核对**:`buildContourBands`/`ContourBandsResult`/`ContourOptions`、`DatasetChartView::{showScatter,showContour,setAnomalies,setHiddenAnomalies,setShowAnomalies,setShowContourLines}`、`DatasetDetailController::{openDataset,focusDataset,chartReady,focusRequested,loadFailed,ChartData}`、`DatasetDetailPanel::{openOrUpdate,focusDataset,activeDatasetChanged}` 跨任务一致。✓ + +**已知留白(实现时据现网/视觉微调,非阻塞)**: +- Task 2.2 `queryException` 的 `data` 包裹层(`value` 数组 vs 直接数组)以现网响应为准。 +- Task 3.2 坐标轴/图例像素布局据视觉对照微调。 +- Task 5.2 异常表「创建时间/备注」列需从 `queryException` VO 透传(首版传空,后续在 ChartData 携带原始 createTime/remark)。 diff --git a/docs/superpowers/plans/2026-06-11-dataset-detail-other-dd-types.md b/docs/superpowers/plans/2026-06-11-dataset-detail-other-dd-types.md new file mode 100644 index 0000000..9b7552f --- /dev/null +++ b/docs/superpowers/plans/2026-06-11-dataset-detail-other-dd-types.md @@ -0,0 +1,235 @@ +# 数据集详情图:扩展到其余 dd 类型 实现计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: 用 `superpowers:subagent-driven-development` 执行(每个 bite-sized 任务派一个 subagent,TDD,频繁提交)。步骤用 `- [ ]`。 +> **铁律(项目记忆,务必遵守):** 任何对原版的不确定,**必须用 Playwright 实地学习原版**,禁止联想猜测;严格 **1:1 复刻**,不加原版没有的特性。**没有活样本的 dd 类型一律 BLOCKED,禁止凭想象写渲染代码。** + +**Goal:** 把「数据集详情图」从只支持 ERT 反演(`dd_inversion_data`)扩展到其余 dd 类型。先打通「控制器按 ddCode 走策略注册表」的分派骨架(未注册类型优雅降级「暂不支持」),再按"样本可得性"逐类落地:有活样本的(ERT 测量类、TEM/timeSensor)做完整复刻,无活样本的(dd_grid / 轨迹 / 测井 / GPR)只列调研占位 + 取样前置条件,BLOCKED。 + +**Architecture:** 沿用现有分层,不重造: +- 编排层 `DatasetDetailController`(现状对非 `dd_inversion_data` 直接拒绝)→ 改为持 `ChartStrategyRegistry`,按 ddCode 选策略;策略决定「拉哪些接口 + 用哪个 View 渲染」。 +- 异步数据层 `IAsyncDatasetRepository` + `ChartLoad`/`GridLoad` 句柄(`ApiBatch` 多请求聚合 + 注入解析函数)。每类新 dd 视形态新增句柄类型(如折线 `LineLoad`、图像 `ImageLoad`)或复用现有句柄。 +- DTO 解析层 `src/data/dto/`(纯函数,单测友好,用抓到的真实响应做夹具)。 +- 渲染层 `src/app/panels/chart/`:复用 QwtPlot(轴/交互/图例)+ VTK 算法(等值线几何)。按形态归类复用或新建 View:等值面类复用 `ContourPlotItem`/`GridDataChartView`;散点类复用 `ScatterPlotItem`/`RawDataChartView`;折线类(测井)新建 `LineChartView`;图像类(GPR)新建 `ImageChartView`。 +- 页面壳 `DatasetDetailPanel`(多 Tab) / `DatasetDetailPage`(单 ds 内部页签)。 + +**Tech Stack:** C++17、Qt6 Widgets、Qwt 6.2(`cmake/qwt.cmake`,目标名 `qwt`,头在 `external/qwt-src/src`)、VTK 9.6(仅算法层)、GoogleTest/CTest、vcpkg(仅非 Qt 依赖)。 + +**依据文档:** +- `docs/superpowers/specs/2026-06-11-dataset-detail-view-design.md`(§2.4 后续 dd 类型、§5.3 策略框架、§3 原 web 分析方法)。 +- `docs/superpowers/HANDOFF-dataset-detail-chart.md`(技术栈/构建/已完成范围)。 +- `docs/apis/business_OpenAPI.json`(各 dd 类型取数接口,下文已核对路径/参数)。 + +**构建/测试命令(本机工具链,固定):** +- 构建:`powershell.exe -ExecutionPolicy Bypass -File scripts/dev-build.ps1` + - ⚠️ 若 `LNK1104 无法打开 geopro_desktop.exe`:先 `Get-Process geopro_desktop | Stop-Process -Force` 再构建。 +- 测试:`powershell.exe -ExecutionPolicy Bypass -File scripts/dev-test.ps1`(先 dev-build 再 dev-test)。**基线 89/89 绿。** + - 单测过滤:`build/release/tests/geopro_tests.exe --gtest_filter=.*` +- 运行(视觉验收):`build/release/src/app/geopro_desktop.exe`(需登录)。 + +--- + +## 0. 现状核实(已读真实代码,勿臆测) + +**关键结论:策略框架已存在但未接入控制器。** 详情: + +- `src/app/panels/chart/IDatasetChartStrategy.hpp`:`IDatasetChartStrategy`(只有纯虚 `std::string ddCode()`)+ `ChartStrategyRegistry`(`add/find/supports`,`std::map`)。**已写好,但全仓只有单测引用它,控制器/main.cpp 从未构造/使用注册表。** +- `src/app/panels/chart/ErtInversionStrategy.hpp`:仅一个 stub —— `ddCode()` 返回 `"dd_inversion_data"`,**无任何 load/render 逻辑**。 +- `src/controller/DatasetDetailController.cpp`: + - `openDataset(dsId, ddCode)`:`if (ddCode != "dd_inversion_data") { emit loadFailed("暂不支持该数据类型的预览"); return; }` —— **硬编码**,未走注册表。 + - `loadGridData(dsId, ddCode)`:同样 `if (ddCode != "dd_inversion_data") return;` 硬编码。 +- `tests/app/test_chart_strategy_registry.cpp`:仅测 `add/find/supports/降级`,**未与控制器联动**。 +- `src/data/repo/IAsyncDatasetRepository.hpp`:`loadChartAsync`(scatter+色阶type1)/ `loadGridAsync`(rows+色阶type2+异常),均**写死 ERT 反演接口**(见 `ApiDatasetRepository.cpp`)。 +- ddCode 来源:`src/data/dto/NavDto.cpp:124` 从数据集列表项 `o["ddCode"]` 解析;`main.cpp:501` 双击时从 `kDsDdCodeRole` 取出传给 `openDataset`。**ddCode 是 API 给的字符串,源码里除 `dd_inversion_data` 外无其它 dd code 常量**(grep 到的 `dd_custom_command` 等是 CMake `add_custom_command` 等构建符号,非数据类型)。 + +**结论:本 plan 的 Phase 1 必须先「打通控制器→注册表分派」,这是所有后续 dd 类型的前置。** 现有 `IDatasetChartStrategy` 接口过窄(只有 ddCode),需扩展为能驱动「加载 + 渲染」。 + +--- + +## 1. 各 dd 类型:实测编目(2026-06-12 全量遍历,已坐实) + +> **本节已由 2026-06-12 全量实测替换原"推断矩阵"。** 用脚本直连 API 遍历**两个账号**(威立雅租户 + 赛盈地空"数据多"账号 20 项目 / 108 TM / 752 DS),逐个 ds 核对 `ddCode` + `dsTypeCode` + 真实渲染。Phase 0 探查目标基本达成。 + +### 1.0 实测方法(脚本,已验证可用,勿臆测照此复刻) + +枚举 DS 的 API 链(**踩过的坑都在这**): +1. 项目列表:`POST /business/my/profile/project/page` body `{pageNo, pageSize}`(注意 **`pageNo` 不是 `pageNum`**)。 +2. 结构树:`GET /business/projectStruct/queryProjectStruct/{projectId}` → **扁平节点数组**,每节点 `{id, parentId, type, name, confCode, typeId}`。**`type=1`=项目根或 GS 节点,`type=2`=TM**。层级靠 `parentId` 链:项目根(parentId="0") → GS(type=1) → TM(type=2) → DS。**TM 可能直挂项目、也可能挂在 GS 下**(如雷达项目 项目→GS"北京"→TM)。 +3. DS 列表:`POST /business/dsObject/data/page` body **`{projectId, structParentId:, structParentConfType:2, classifyTypeList:[3], pageNo, pageSize}`**。 + - ⚠️ **字段是 `structParentId`(=tmId)+ `structParentConfType:2`,不是 `tmObjectId`**; + - ⚠️ **必须带 `classifyTypeList:[3]`**(=数据,缺了/换值后端直接 `code:500 sys.internalServerError`); + - ⚠️ `pageNo` 不是 `pageNum`。 + - 时序类另走 `POST /business/dsObject/timeSensor/data/page`(同 body)。 +4. DS 对象关键字段:`ddCode`(如 `dd_grid`)、`dsTypeCode`(英文,如 `WhitenedData`)、`name`(中文数据类型名)、`dsName`(文件名)、`id`(dsId)。 +5. 后端对该接口**间歇 500**,脚本需重试(4–8 次)。 + +**铁律纠正(重要):详情渲染由数据集的"真实数据类型"决定,URL 详情链接里的 `ddCode` 经常标错。** 实测:「视电阻率数据」「接地电阻」的详情链接都带 `ddCode=dd_inversion_data`,但渲染分别是反演剖面 / 柱状图,完全不同。**客户端策略分派必须用真实数据类型(ds 元数据的 `dsTypeCode`/`ddCode`),绝不能用详情链接的 ddCode。** + +### 1.1 已确认数据类型(10 种 ddCode,均有活样本) + +| ddCode | dsTypeCode | 数据类型(中) | 渲染视图(实测) | 客户端 | 样本 | +|---|---|---|---|---|---| +| `dd_inversion_data` | ERT inversion data | 电阻率(反演)数据;TEM反演剖面;视电阻率数据 | **① 原数据(Plotly散点,cauto) + 网格数据(等值面+等值线)** 2 页签 | ✅ 已做 | 多 | +| `dd_ert_measurement_data` | ERT raw data | ERT原始数据 | **③ 散点伪剖面**(x=斜距/y=伪深度, 视电阻率着色, A/B/M/N 悬浮, 含反演运算/生成视电阻率/色阶工具) + 数据列表 | 可做 | 多 | +| `dd_ert_measurement_gr_data` | earth resistance | ERT接地电阻 | **② 柱状图**(Y=电阻/欧姆, X=电极点) + 列表 | 可做 | 多 | +| `dd_trajectory_data` | Platform trajectory | ERT电极坐标;TEM坐标 | **④ 轨迹**:地图(测线折线) + 列表 + 高程 3 页签 | 可做 | 多 | +| `dd_grid` | WhitenedData | 白化数据 | **⑤ 列表**(序号/x/y 地表点) | 可做 | 有 | +| `dd_gpr_channel_detail` | RADAR_SINGLE_CHANNEL_PROFILE | 雷达单通道剖面数据 | **⑥ 雷达剖面灰度图像(B-scan radargram)** 左 + **单道波形(A-scan wiggle)** 右;工具:对比度滑块/灰度色阶/显示频谱图 | 待需求确认 | 雷达项目 | +| `dd_gpr_channel_image` | RADAR_SINGLE_CHANNEL_IMAGE_LIST | 雷达单通道图片列表 | **⑥ 同上**(B-scan 灰度图像 + A-scan 波形,与 channel_detail 同视图) | 待需求确认 | 雷达项目 | +| `dd_radar_channel_trajectory` | RADAR_SINGLE_CHANNEL_TRAJECTORY | 雷达单通道轨迹坐标 | **⑦ 地图轨迹**:真实地理底图(街道/河流) + GPS 测线路径(黄/绿线) + 起点/终点/轨迹点标记 | 待需求确认 | 雷达项目 | +| `dd_radar_rtk_trajectory` | rtk_trajectory | RTK 轨迹坐标 | **⑦ 地图轨迹 + RTK 坐标点设置面板**(GPS定位/解状态过滤:固定解/浮点解/单点解/差分、最大距离过滤、数据还原) | 待需求确认 | 雷达项目 | +| `dd_radar_preprocess_data` | RADAR_PREPROCESS_DATA | 雷达预处理数据 | **空白**(无标题/内容,仅空 map 容器)——疑数据中间态、无独立可视详情 | 待需求确认 | 雷达项目 | + +> 渲染视图编号①②③④⑤⑥⑦。**全 10 种 ddCode 渲染均已逐个打开实测(看截图)确认。** 归并后**共 7 种渲染视图**(① 散点+网格[已做] / ② 柱状图 / ③ 散点伪剖面 / ④ 轨迹(地图/列表/高程) / ⑤ 列表 / ⑥ 雷达剖面图像(B-scan+A-scan) / ⑦ 地图轨迹(GIS底图+GPS路径)),外加 `dd_radar_preprocess_data` 无独立详情。 +> +> 注:④(ERT/TEM 坐标 `dd_trajectory_data` 的"地图/列表/高程") 与 ⑦(雷达 `dd_radar_*_trajectory` 的"GIS底图+GPS路径") 都是"轨迹/地图"族但组件形态不同——④是平面测线轨迹+高程剖面页签,⑦是真实地理底图叠 GPS 路径(+RTK 解过滤);实现时可能各自一个 View 或共用底图 View 加配置。 + +**5 个 radar/gpr 样本路径**(同一条 GPR 测线):项目 **雷达0331**(projectId=`1454042286333952`) → GS **北京**(`1454042309754880`) → TM **1229cx1-0_160**(`1454042386726912`, confCode=gpr) → DS: +- `dd_gpr_channel_detail` dsId=`1454042504011776`、`dd_gpr_channel_image`=`1454042523484160`、`dd_radar_channel_trajectory`=`1454042504060928`、`dd_radar_rtk_trajectory`=`1454042387619840`、`dd_radar_preprocess_data`=`1454042387505152`。 + +### 1.2 仍无样本(保持 BLOCKED) + +| 类型 | 接口(OpenAPI) | 状态 | +|---|---|---| +| TEM 时序 / timeSensor | `POST dd/ert/timeSensor/rows`、`dsObject/timeSensor/data/page` | 🚫 两账号均无此 ds 类型样本(TEM 数据实为 dd_inversion_data + dd_trajectory_data)。`LineChartView` 折线视图无依据,不实现。 | +| 测井 well logging | OpenAPI 无明确接口 | 🚫 无样本、无接口。 | +| 电流法 currentmethod | `dd/indicator/currentmethod/rows`、`scatter/graph/{dsObjectId}` | 🚫 项目「填埋场监测」有 CurrentMethod 方法类型 TM,但无带数据 DS 样本。 | + +**小结:** 实测确认 **10 种 ddCode 有样本**(ERT/通用 5 种渲染已定 + radar/gpr 5 种渲染待定本轮确认);timeSensor/测井/电流法 仍无样本,保持 BLOCKED。客户端已做 1 种(`dd_inversion_data`)。 + +--- + +## 文件结构(新建/修改总览) + +| 文件 | 动作 | 职责 | +|---|---|---| +| `src/app/panels/chart/IDatasetChartStrategy.hpp` | 改 | 扩展接口:除 `ddCode()` 外,加描述「加载/渲染契约」的虚方法(详见 Task 1.1) | +| `src/app/panels/chart/ErtInversionStrategy.{hpp,cpp}` | 改 | 把 stub 实化为「ERT 反演策略」:声明它需要 chart+grid 加载、用散点/等值面视图 | +| `src/controller/DatasetDetailController.{hpp,cpp}` | 改 | 持 `ChartStrategyRegistry&`;`openDataset`/`loadGridData` 改为走注册表分派,未注册→`loadFailed("暂不支持…")` | +| `src/app/main.cpp` | 改 | 构造 `ChartStrategyRegistry`,注册 `ErtInversionStrategy`,注入控制器 | +| `tests/controller/test_dataset_detail_controller.cpp` | 改 | 加「未注册 ddCode 优雅降级 / 已注册走加载」用例 | +| `tests/app/test_chart_strategy_registry.cpp` | 改 | 已有降级用例;按接口扩展补充 | +| `docs/superpowers/sample-probe-other-dd-types.md` | 建(Phase 0 产出) | 各类 dd 的「样本可得性矩阵 + 渲染规格 + 真实 API 响应夹具索引」 | +| `tests/fixtures/dd/*.json` | 建(Phase 0 产出) | 抓到的真实响应,做 DTO 单测夹具(仅有样本的类型) | +| `src/data/dto/DatasetChartDto.{hpp,cpp}` | 改/拆 | 新增各类 dd 的 parse 函数(如 `parseMeasurementScatter`、`parseTimeSensorSeries`)。若文件超 ~400 行则按类型拆 `MeasurementDto.*` / `TimeSensorDto.*` | +| `src/data/api/ApiDatasetRepository.{hpp,cpp}` | 改 | 新增对应 `load*Async`(仅有样本类型)| +| `src/data/api/DatasetLoads.hpp` | 改 | 新增 `*Parts` 结构(仅有样本类型)| +| `src/data/api/DatasetLoadHandles.hpp` + `.cpp` | 改 | 视形态新增句柄类型(如 `LineLoad`/`ImageLoad`),或复用现有 | +| `src/app/panels/chart/LineChartView.{hpp,cpp}` | 建(仅当 TEM/测井解锁) | 折线视图(QwtPlot + QwtPlotCurve) | +| `src/app/panels/chart/ImageChartView.{hpp,cpp}` | 建(仅当 GPR 解锁) | 图像剖面视图 | +| `src/app/CMakeLists.txt` / `tests/CMakeLists.txt` | 改 | 注册新文件/测试 | + +--- + +## Phase 0:样本探查(必做,先于一切实现) + +> 目的:把矩阵里的「待核对/BLOCKED」逐一坐实。**不写任何渲染代码。**产出是「样本规格 + 真实响应夹具」,作为后续 TDD 的 RED 依据。 + +- [ ] **Task 0.1(核对 ddCode 真值)** 用 Playwright 登录原版 `http://tenant.geomative.cn`,进入 `#/projectSpace/datasetMange/datasetInfo`,遍历可见数据集,抓每个数据集列表项的 `ddCode` 字段(网络响应或前端状态)。把真实 ddCode 回填到本 plan §1 矩阵。→ verify:矩阵「推断 ddCode」列全部替换为实测值,无残留「待核对」。 +- [ ] **Task 0.2(逐类样本探查)** 对 ERT 测量原始 / ERT 测量gr / TEM / dd_grid / 轨迹 / 测井 / GPR 七类,逐一在原版找「有数据的对象」: + - 打开其详情页,截图渲染形态(散点?等值面?折线?图像?); + - 在 Network 抓其取数请求的**完整 URL(含 query/path 参数)+ 完整响应 JSON**; + - 记录坐标轴语义(x/y 单位、方向、是否等比)、色阶/图例有无、是否有异常叠加。 + - → verify:每类得出「有样本 / 无样本」结论,有样本的把响应存到 `tests/fixtures/dd/.json`,写进 `docs/superpowers/sample-probe-other-dd-types.md`。 +- [ ] **Task 0.3(产出规格文档)** 写 `docs/superpowers/sample-probe-other-dd-types.md`:每类一节,含「样本可得性 / 真实接口 / 响应结构 / 渲染规格 / 渲染归类 / 是否解锁」。无样本类明确标 **BLOCKED:待样本**,列「解锁前置条件」(如「需租户导入 GPR 数据 / 需测井样本数据集」)。→ verify:文档七类齐全,与 §1 矩阵一致。 +- [ ] **Task 0.4(提交)** `docs: dd 类型样本探查矩阵 + 真实响应夹具`。提交规格文档与 fixtures。 + +**Phase 0 决策门:** 完成后回到本 plan,把 §1 矩阵中实测有样本的类型从「候选」升为 Phase 2+ 的实现任务;无样本的保持 BLOCKED,不进入实现 Phase。 + +--- + +## Phase 1:打通策略分派骨架(前置,无须样本即可做) + +> 把控制器从硬编码 `dd_inversion_data` 改为走 `ChartStrategyRegistry`。**行为对 `dd_inversion_data` 必须零回归(仍正常出图),对未注册类型仍降级「暂不支持」。** 这是所有后续 dd 类型的地基。 + +- [ ] **Task 1.1(扩展策略接口)** 先读 `IDatasetChartStrategy.hpp`、`DatasetDetailController.{hpp,cpp}` 真实签名。把 `IDatasetChartStrategy` 从「只有 `ddCode()`」扩展为能表达「该类型支持哪些加载阶段」的最小契约,**避免过度设计(YAGNI)**。建议最小形: + - 保留 `std::string ddCode() const`。 + - 加 `bool hasGridPhase() const`(ERT 反演=true;纯散点/折线/图像类=false)—— 让控制器据此决定是否允许 `loadGridData`,替代当前对 `loadGridData` 的硬编码 ddCode 判断。 + - (渲染分派暂不进接口:现阶段 `chartReady`/`gridReady` 信号 + 现有 `DatasetDetailPage` 已驱动渲染;待新 View 真要接入时再在对应 Phase 扩展,不在 Phase 1 预先抽象。) + - 先改 `tests/app/test_chart_strategy_registry.cpp`:加「策略报告 hasGridPhase」的断言(RED)→ 改接口 + `ErtInversionStrategy`(GREEN)。 + - → verify:`dev-test` `ChartStrategyRegistry.*` 绿。 +- [ ] **Task 1.2(控制器走注册表 — openDataset)** 改 `DatasetDetailController`:构造函数加 `app::ChartStrategyRegistry& registry`(与现有 `IAsyncDatasetRepository&` 并列)。`openDataset`:把 `if (ddCode != "dd_inversion_data")` 改为 `if (!registry.supports(ddCode)) { emit loadFailed(dsId, "暂不支持该数据类型的预览"); return; }`,其余加载逻辑暂不变(仍走 `loadChartAsync`)。 + - 先改 `tests/controller/test_dataset_detail_controller.cpp`:用「空注册表 → openDataset 任意 ddCode → 收到 loadFailed」+「注册了 dd_inversion_data → 走加载(mock repo 的 loadChartAsync 被调用)」两个用例(RED)。读现有该测试看 mock repo/句柄如何桩(`tests/data/test_dataset_load_handles.cpp` 有句柄桩可参考)。 + - → verify:`DatasetDetailController.*` 测试绿。 +- [ ] **Task 1.3(控制器走注册表 — loadGridData)** `loadGridData`:把 `if (ddCode != "dd_inversion_data") return;` 改为「查策略,`!strategy || !strategy->hasGridPhase()` → return」。补单测:注册一个 `hasGridPhase()==false` 的 fake 策略 → `loadGridData` 不触发 `loadGridAsync`。→ verify:测试绿。 +- [ ] **Task 1.4(main.cpp 接线)** 在 `main.cpp` 构造 `geopro::app::ChartStrategyRegistry registry;`,`registry.add(std::make_unique());`,把 `registry` 注入 `DatasetDetailController detailCtrl(datasetRepo, registry);`。注意生命周期:registry 须比 detailCtrl 活得久(同作用域、registry 在前声明)。 + - → verify:`dev-build` 通过;启动 app,双击 ERT 反演 ds **零回归**(原数据散点 + 网格等值面 + 异常均正常出图);双击其它类型仍显示「暂不支持」。 +- [ ] **Task 1.5(提交)** `refactor(detail): 控制器按 ddCode 走 ChartStrategyRegistry 分派, 未注册优雅降级 (替代硬编码 dd_inversion_data)`。 + +--- + +## Phase 2:ERT 测量类(散点形态,仅当 Task 0.2 确认有样本才进行) + +> 🔓 解锁条件:Phase 0 确认 ERT 测量(`measurement/scatter/graph` 或 `measurement/rows`)有活样本且抓到真实响应。**未解锁则本 Phase 全部 BLOCKED,跳过。** +> 渲染归类:散点类 → **复用** `ScatterPlotItem`/`RawDataChartView`,不新建 View(DRY)。 +> 接口(已核对):`GET /business/dd/ert/measurement/scatter/graph?dsObjectId=&vFieldCode=`、`GET /business/dd/ert/measurement/rows?dsObjectId=`。注意:**dsObjectId 是 query 参数(≠ 反演的 path 参数)**,且 scatter 需 `vFieldCode`(Phase 0 抓其真实取值)。 + +- [ ] **Task 2.1(DTO 解析 + 单测)** 用 `tests/fixtures/dd/ert-measurement-scatter.json` 真实响应做夹具,先写 `tests/data/test_dataset_chart_dto.cpp` 新用例断言字段映射(RED)→ 在 `DatasetChartDto.cpp`(或新拆 `MeasurementDto.cpp`,若主文件超 ~400 行)实现 `parseMeasurementScatter(QJsonObject)`(GREEN)。映射须严格按真实响应字段,**不臆造字段名**。→ verify:DTO 测试绿。 +- [ ] **Task 2.2(色阶处置)** Phase 0 确认测量散点是否有独立色阶接口/type(反演散点用 `colorGradation/getDetail` type1)。若有,按真实 type 拉;若无色阶(纯散点),ColorBar 隐藏。补单测覆盖色阶解析或缺省。→ verify:测试绿。 +- [ ] **Task 2.3(异步句柄 + 仓储)** 复用 `ChartLoad`/`ChartParts`(结构相同则直接复用;字段不同则在 `DatasetLoads.hpp` 加 `MeasurementParts` + 句柄)。在 `ApiDatasetRepository` 加 `loadMeasurementChartAsync(dsId)`(`ApiBatch` 组 scatter[+色阶] 请求 + 注入解析)。注意 query 参数编码(参考现有 `enc()`)。补 `tests/data/test_dataset_load_handles.cpp` 用例。→ verify:测试绿。 +- [ ] **Task 2.4(策略 + 控制器分派)** 新建 `MeasurementStrategy`(`ddCode()` 用 Task 0.1 实测值,`hasGridPhase()` 按 rows 是否等值面而定)。`DatasetDetailController::openDataset` 据策略选择调 `loadMeasurementChartAsync`(若加载形态与反演不同,控制器按 ddCode/策略分支;保持函数 <50 行,必要时抽私有 helper)。`main.cpp` 注册该策略。补控制器单测。→ verify:测试绿。 +- [ ] **Task 2.5(接入视图 + 视觉验收)** `DatasetDetailPage`/`RawDataChartView` 接收 `chartReady` 渲染散点。启动 app 双击测量类 ds,**对照 Phase 0 截图逐项验收**(点形/色阶/轴/等比),截图发用户确认。→ verify:视觉等价。 +- [ ] **Task 2.6(rows 形态)** 若 Phase 0 显示 `measurement/rows` 是另一种展示(如伪剖面/列表),据规格归类(等值面→复用 `ContourPlotItem`;表格→另议),重复 2.1–2.5 节奏。**形态不明则 BLOCKED 待 Phase 0 规格。** +- [ ] **Task 2.7(提交)** 每 1–2 个 Task 一次原子提交,保持构建绿。`feat(detail): ERT 测量散点详情图 (DTO+仓储+策略+视图)`。 + +--- + +## Phase 3:TEM / 设备时序(折线形态,仅当 Task 0.2 确认有样本才进行) + +> 🔓 解锁条件:Phase 0 确认 TEM/timeSensor 有活样本且抓到响应。**未解锁 BLOCKED,跳过。** +> 渲染归类:时序折线类 → **新建** `LineChartView`(QwtPlot + `QwtPlotCurve`,x=时间,y=数值)。 +> 接口(已核对):`POST /business/dd/ert/timeSensor/rows`(body `DDTimeSensorDataQueryReqVO`,Phase 0 抓真实 body 字段)、`GET dd/ert/timeSensor/page`。 + +- [ ] **Task 3.1(DTO 解析 + 单测)** 用真实响应夹具 `tests/fixtures/dd/tem-timesensor.json`,先写测试(RED)→ 实现 `parseTimeSensorSeries`(→ 时间数组 + 多通道数值序列模型;模型若无现成 core 类型,加最小 `core::TimeSeries`,YAGNI)。→ verify:测试绿。 +- [ ] **Task 3.2(异步句柄 + 仓储)** 因是 POST + 不同返回,加 `SeriesLoad`/`SeriesParts` 句柄(参照 `ApiChartLoad`/`ApiGridLoad` 写 `ApiSeriesLoad`)+ `loadTimeSensorAsync(dsId)`(`postJsonAsync` 组 body)。补句柄桩单测。→ verify:测试绿。 +- [ ] **Task 3.3(LineChartView + 组件测)** 新建 `src/app/panels/chart/LineChartView.{hpp,cpp}`:QwtPlot + 每通道一条 `QwtPlotCurve` + 图例 + 平移/缩放(复用 `LivePanner` 模式)。组件测:给定 series → 断言曲线条数/点数。→ verify:测试绿。 +- [ ] **Task 3.4(策略 + 控制器 + 页面接入)** `TimeSensorStrategy`(实测 ddCode,`hasGridPhase()==false`)。控制器据策略走 `loadTimeSensorAsync` → 新增 `seriesReady` 信号 → `DatasetDetailPage` 用 `LineChartView` 渲染(页内页签按类型选 View)。main.cpp 注册。补控制器单测。→ verify:测试绿 + 启动 app 双击 TEM ds,对照 Phase 0 截图验收。 +- [ ] **Task 3.5(提交)** `feat(detail): TEM 时序折线详情图 (LineChartView+DTO+仓储+策略)`。 + +--- + +## Phase 4(BLOCKED:待样本)—— dd_grid / 轨迹 / 测井 / GPR + +> 以下类型当前租户**无活样本**(spec §2.4 + Phase 0 复核)。**绝不规划凭想象的渲染任务**(违反 1:1 复刻原则)。本 Phase 只列「解锁前置条件 + 调研占位」,待样本到位后各自展开为类似 Phase 2/3 的 TDD 任务序列。 + +- [ ] **Task 4.1(dd_grid 网格)** 🚫 BLOCKED。 + - 解锁前置:租户内出现带数据的 dd_grid 对象,Phase 0 抓 `GET dd/ert/grid/rows?dsObjectId&pageNo&pageSize` 真实响应 + 详情页截图。 + - 预归类:等值面类 → 复用 `ContourPlotItem`/`GridDataChartView`(与反演 rows 同构则可大量复用 `parseInversionGrid` 思路;**须实测响应字段是否一致再决定复用/新 parse**)。 + - 注意:grid/rows 带分页参数,须确认是否需多页拼接。 +- [ ] **Task 4.2(轨迹 trajectory)** 🚫 BLOCKED。 + - 解锁前置:带数据的轨迹对象,抓 `GET dd/ert/trajectory/rows?dsObjectId` + `GET dd/ert/trajectory/line?dsObjectId&frontCrsCode` 响应 + 截图。 + - 预归类:路径/折线类 → 待样本定(散点连线 or `LineChartView`)。`frontCrsCode` 取值须 Phase 0 抓。 +- [ ] **Task 4.3(测井 well logging)** 🚫 BLOCKED(双重阻塞:无样本 + 接口未确认)。 + - 解锁前置:(a) 拿到测井数据样本;(b) **OpenAPI 未见明确测井 rows 接口** → Phase 0 须在原版「测井参数表」相关页面抓真实请求确认接口。 + - 预归类:折线类 → 复用 `LineChartView`(y=深度向下 / x=数值;或 x=时间 y=数值曲线,按菜单「测井参数表」两种形态,实测后定)。 +- [ ] **Task 4.4(GPR 雷达剖面)** 🚫 BLOCKED。 + - 解锁前置:GPR 对象有数据(spec 明确当前 GPR 对象无数据),抓 `GET dd/gpr/channel/image/{dsObjectId}` 响应(确认返回是图片 URL / base64 / 像素数组)+ 详情页截图。 + - 预归类:图像类 → 新建 `ImageChartView`(位图剖面 + 坐标轴叠加)。返回形态决定加载方式(图片下载 vs JSON 像素)。 + - 参考相关接口:`dd/gpr/channel/trace/spectrogram`、`dd/gpr/channel/querySegmentation`、`radar/trajectory/ds/{dsObjectId}`。 + +--- + +## 任务顺序与可构建性 + +1. **Phase 0 先行**:无样本不写代码。Phase 0 是纯调研 + 夹具,零代码风险。 +2. **Phase 1 是地基**:策略分派打通后,每个新 dd 类型才能「注册即生效」。Phase 1 必须保证 ERT 反演零回归(已有 89/89 测试 + 视觉验收兜底)。 +3. **接口/控制器改形原子落地**:`DatasetDetailController` 构造函数签名变更(加 registry)+ `main.cpp` 接线**须在同一提交**完成(否则构建红)。参照详情图 v2 「Task5+6 合并」教训:跨文件签名变更不拆提交。 +4. **Phase 2/3 仅在 Phase 0 解锁后展开**;Phase 4 永远 BLOCKED 直到样本到位。 +5. **每 1–2 个 bite-sized Task 一次提交**,每次提交前 `dev-test` 必须绿。 + +## 范围边界 + +- 本 plan 仅「详情图渲染扩展」。**不含**工具条编辑功能(白化/滤波/色阶配置/异常框注/自动标注/网格化/另存为/导出/描述富文本/大视图全屏)—— 另案(spec §2.3)。 +- 不改中央 2D/3D VTK 地图视图。 +- 不改 ApiClient 异步机制(已单独立项,详情详情链路已异步)。 + +--- + +## Self-Review(写完计划的自查结论) + +- **核实优先**:所有接口路径/参数(query vs path、POST body schema)均已对 `docs/apis/business_OpenAPI.json` 核对(见 §1 表),未臆造。ddCode 除 `dd_inversion_data` 外源码无常量,已明确标注「待 Phase 0 实测核对」而非编造。 +- **关键现状坐实**:已读 `DatasetDetailController.cpp` 确认控制器硬编码 `dd_inversion_data`、未用注册表;`ErtInversionStrategy` 是空 stub。Phase 1「打通分派」据此设计,是真实缺口而非假想。 +- **诚实对待样本约束**:唯一确定有样本的是已完成的反演;其余全部置于 Phase 0 探查门之后。无样本类(dd_grid/轨迹/测井/GPR)一律 BLOCKED + 解锁前置条件,**未规划任何凭想象的渲染任务**,符合 1:1 复刻铁律。 +- **DRY/复用**:散点复用 `ScatterPlotItem`、等值面复用 `ContourPlotItem`、异步复用 `ApiBatch`/句柄模式;仅折线(TEM/测井)与图像(GPR)两种新形态新建 View,且各自有样本/解锁条件兜底。 +- **可构建性**:标注了「构造函数签名变更 + main.cpp 接线须同提交」的原子落地约束,避免中间态构建红。 +- **TDD 无占位符**:每个可推进 Task 给出确切文件路径、RED→GREEN 顺序、verify 命令;BLOCKED Task 给出明确解锁前置条件而非空泛描述。 +- **风险点**:(1) Phase 0 可能发现 ERT 测量/TEM 也无具体带数据对象 → 则 Phase 2/3 同样 BLOCKED,plan 仍成立(Phase 1 独立有价值)。(2) `IDatasetChartStrategy` 接口扩展刻意保守(只加 `hasGridPhase()`),避免为未解锁类型过度抽象;待新 View 真接入时再演进。 diff --git a/docs/superpowers/plans/2026-06-12-detail-render-engine.md b/docs/superpowers/plans/2026-06-12-detail-render-engine.md new file mode 100644 index 0000000..5cceb30 --- /dev/null +++ b/docs/superpowers/plans/2026-06-12-detail-render-engine.md @@ -0,0 +1,114 @@ +# 数据详情「按类型渲染」通用引擎 + 迁移 inversion + measurement_data 实现计划 + +> 2026-06-12。承接 `2026-06-11-dataset-detail-other-dd-types.md`(Phase 1 策略分派已 done)。 +> **决策(用户已拍板=选项2)**:建通用渲染引擎,并把已验收的 `dd_inversion_data` 一起迁到新引擎、删旧专用代码,终态只有一套。 +> **铁律**:不确定必用 Playwright 实看原版;严格 1:1;无活样本类型 BLOCKED。 +> **工作方式**:subagent-driven + TDD(真实夹具) + 破坏性接口改形原子落地保持构建绿。 + +## 0. 背景与动机 + +详情视图有多种 dd 类型,每类页签集 + 图形 kind 不同。现状把"两页签(chartReady/gridReady) + 两个专用 Load 句柄 + 写死 inversion 字段"耦合进三层。要支持 measurement_data / gr_data(柱状) / trajectory(轨迹) / grid(白化) 等且可持续扩展,需围绕「页签描述符」统一。 + +## 1. Phase 0 实测成果(2026-06-12,真实响应,已抓夹具) + +API base:`http://tenant.geomative.cn/pop-api/business`。Auth header:`geomativeauthorization: Geomative `。 +同一 TM(project=1438889436225536 / structParentId=1438889842614272)集齐 4 类样本: + +| ddCode | dsId 样本 | 端点 | 数据形状 | 页签 → render kind | +|---|---|---|---|---| +| `dd_ert_measurement_data` 原始 | 1453611522236416 | `GET dd/ert/measurement/scatter/graph?dsObjectId=&vFieldCode=`(图) + `GET dd/ert/measurement/rows?dsObjectId=`(列表) | 图:`{electrodeList[40], scatterGraphConf{x[2],v[6],y[3],vcalculationMethod[3]}, scatterGraphData{hasCoordinate,hasElevation,min[9],max[9],rows[576]位置数组}}`;列表:`{filedList[12], rowList[576]键控对象,含 vmap{R,V,I,R0_RD,SP,R0}}` | 散点伪剖面=Scatter + 列表=Table | +| `dd_ert_measurement_gr_data` 接地电阻 | 1453611522285569 | `GET dd/ert/measurement/gr/rows?dsObjectId=` | `array[40]` of `{electrodeId,testDate,testTime,p1Rg,p1RgStatus,p2Rg,p2RgStatus}` | 柱状图=Bar(X=electrodeId,Y=电阻Ω,系列P1) + 列表=Table | +| `dd_trajectory_data` 坐标 | 1438893961060352 | `GET dd/ert/trajectory/rows?dsObjectId=`(+`/line?…&frontCrsCode` 400 需参数) | `{rowList[40]{electrodeNo,projectX,projectY,longitude,latitude,elevation,lineNo,channelNo,…}, gridHeaderDisplay[14]}` | 地图=PolylineMap(平面折线,非GIS底图) + 列表=Table + 高程=LineProfile | +| `dd_grid` 白化 | 1438944148742207 | `GET dd/ert/grid/rows?dsObjectId=&pageNo=&pageSize=` | `{rowList[N]{x,y,id}, gridHeaderDisplay[2], total}` **分页** | 列表=Table(仅,分页) | + +夹具:`tests/fixtures/dd/ert-measurement-scatter.json`、`ert-measurement-rows.json`(真实响应,rows 裁剪至 20,保 conf/min/max/electrodeList 全量)。 + +**散点位置数组列序(rows[i],与键控 rows 比对得)**:`[0]平距 [1]斜距 [2]layerNo(null) [3]伪深度(负,向下) [4]选中v原值 [5]log10(值) [6]导数态 [7]0 [8]id(str) [9]高程伪深度 [10]a [11]b [12]m [13]n [14]K [15]rowNo [16]选中v值(=色)`。默认 vFieldCode 空 → v=视电阻率 R0;x=平距;y=伪深度。色阶 cauto(同反演原数据,按 v 有限值 min/max 归一化)。 + +**渲染 kind 全集**:Scatter / FilledContour / Bar / LineProfile / PolylineMap / Table /(未来 Image,GPR BLOCKED)。 +**通用 Table**:`{gridHeaderDisplay 或 filedList(列定义) + rowList(对象)}` 形状被 measurement列表/grid白化/trajectory列表共用 → 一个 `DataTableView` + 一个 `parseTable` 通吃(分页与列格式化为 parse-time/decorator,见 §4)。 + +## 2. 目标架构 + +依赖箭头:`controller(ViewKind/TabSpec/Controller) ← app/strategies, app/views ; data(DetailLoad/Repo) 产出 QVariant 载荷`。 + +### 2.1 载荷(payload)——`src/model/detail/DetailPayloads.hpp`(纯数据,无 Qt-widget 依赖,Q_DECLARE_METATYPE) +- `ScatterPayload { core::ScatterField scatter; core::ColorScale scale; }`(反演原数据 + measurement 散点共用;≈现 `ChartParts`) +- `ContourPayload { core::Grid grid; core::ColorScale scale; std::vector anomalies; }`(≈现 `GridParts`) +- `TablePayload { std::vector columns; std::vector> rows; int total = 0; }`;`TableColumn { QString code, title; int width; int sort; }` +- (Bar/LineProfile/Polyline 载荷待对应类型阶段再加,YAGNI) + +### 2.2 页签描述符 + 策略——`src/controller/DatasetDetailTab.hpp` + `IDatasetChartStrategy.hpp` +```cpp +enum class ViewKind { Scatter, FilledContour, Bar, LineProfile, PolylineMap, Table /*,Image*/ }; +struct TabSpec { QString title; ViewKind kind; QString loaderKey; bool lazy=false; bool paginated=false; }; +struct IDatasetChartStrategy { virtual std::string ddCode() const=0; virtual std::vector tabs() const=0; }; // tabs() 取代 hasGridPhase() +``` +`ChartStrategyRegistry` 不变。`ErtInversionStrategy::tabs()` = `{{"原数据",Scatter,"inversion.scatter",false},{"网格数据",FilledContour,"inversion.grid",true}}`。 + +### 2.3 通用异步句柄——`src/data/api/DatasetLoadHandles.*` +合并 `ChartLoad`/`GridLoad` → 单 `DetailLoad{ virtual void abort(); signals: done(QVariant); failed(QString); }` + `ApiDetailLoad`(包 ApiBatch + Parser=`std::function&)>`)。**aborted_ 守卫 + 双 try/catch→failed + deleteLater 三件套从 ApiChartLoad 原样照搬**。删 `ChartParts`/`GridParts`(被 payload 取代)或保留作中间态(由实现者定,优先删以免双份)。 + +### 2.4 仓储——`IAsyncDatasetRepository::loadAsync(loaderKey,dsId)` + `ApiDatasetRepository` 分派表 +唯一端点+解析器集中处。`if (key=="inversion.scatter") return makeInversionScatter(dsId); …`;未知 key 抛。每 make 建 ApiBatch + 注入返回 `QVariant::fromValue(payload)` 的解析器。删 `loadChartAsync/loadGridAsync`。 +> measurement scatter 的 `dsObjectId` 是 **query 参数**(`?dsObjectId=&vFieldCode=`,默认 vFieldCode 空),≠反演 path 参数;用现有 `enc()`。 + +### 2.5 控制器——`DatasetDetailController` 变通用(无 per-ddCode if) +```cpp +slots: openDataset(dsId,ddCode,dsName), loadTab(dsId,ddCode,tabIndex), focusDataset(dsId); +signals: datasetOpened(dsId,dsName, std::vector), tabLoadStarted(dsId,tabIndex), + tabReady(dsId,tabIndex,QVariant), loadFailed(dsId,msg), focusRequested(dsId); +``` +`openDataset`:查 registry,无→`loadFailed("暂不支持…")`(降级不变);`emit datasetOpened(...,strategy->tabs())`;对每个非 lazy 页签 `loadTab`。 +`loadTab`:**唯一保存 §5.0 安全不变量处**,按 tab 槽位:`inflight_`=`QMap>`;abort 旧槽 → 新 load → `tabLoadStarted` → done 回调里 `if (load!=inflight_.value(i)) return;`(身份比对)→ `tabReady`。析构 abort 全部。删 `chartReady/gridReady/ChartData/GridData/loadGridData`。 + +### 2.6 视图 + 工厂 + 壳 +- `src/app/panels/chart/IDetailView.hpp`:`{ QWidget* widget(); void setPayload(const QVariant&); }`。 +- `RawDataChartView`/`GridDataChartView` 实现 IDetailView:`setPayload` 解包 `ScatterPayload`/`ContourPayload` → 调原 `setData(...)`(**渲染代码零改**)。坏/空 variant → 可见"渲染数据格式错误"不崩。 +- `DetailViewFactory.{hpp,cpp}`:`makeDetailView(ViewKind,parent)` switch → 对应视图。 +- `DatasetDetailPage`:`build(tabs)` 按 TabSpec 建页签(`buildTabbedPanel` + 工厂);lazy 页签首次激活发 `tabNeeded(dsId,ddCode,i)`(泛化现 gridDataNeeded);`setTabPayload(i,QVariant)` → `views_[i]->setPayload`。删写死 `rawView_/gridView_/kGridTabIndex`。 +- `DatasetDetailPanel`:`onDatasetOpened(dsId,dsName,tabs)` 建 page→build;按 dsId 转发 tabReady/tabNeeded。dsId 去重壳逻辑不动。 +- `main.cpp`:接线改 datasetOpened/tabReady/tabNeeded/tabLoadStarted;registry 注册不变(后续加 MeasurementStrategy)。**与控制器改形同提交(原子)。** + +## 3. 实施序列(任务) + +### E1a 引擎地基(加性,构建保持绿) [task #1] +新增 payloads / TabSpec/ViewKind / DetailLoad+ApiDetailLoad(与旧句柄并存) / `loadAsync` 分派(inversion.scatter/grid 产 payload),与旧 `loadChartAsync/loadGridAsync` 并存。TDD:DTO→payload、句柄桩(`tests/data/test_dataset_load_handles.cpp` 仿现有桩)、loadAsync 分派。**构建+测试全绿,旧路径不动。** + +### E1b 破坏性改形 + inversion 迁移(原子) [task #2] +控制器/策略接口/视图 IDetailView/工厂/Page/Panel/main.cpp 全部切到 tab 引擎;删旧 `chartReady/gridReady/ChartData/GridData/ChartLoad/GridLoad/loadChartAsync/loadGridAsync/hasGridPhase`。改全部相关测试(controller/strategy_registry/handles)。**一个提交内落地,构建+测试绿 + 跑 app 双击标准 ds `1458990939709440` 目视回归两页签(散点+网格等值面+异常)正常。** + +### E2 measurement_data(③散点伪剖面 + 列表) [task #3] + +**Phase 0 色阶补查(2026-06-12,已坐实,夹具齐)**:measurement 散点**有 colorBar**,来自 `POST lvl/colorGradation/getDetail` body **`{dsObjectId, businessCode:"", type:3}`**(注意:businessCode=v字段码、type=3,≠反演的 type1/2;用 businessCode='' 会返回 null)。响应是**与反演同构的混合格式 colorBar**(hex + rgba alpha 0–1,18 个对数间距 stops `0.00→#00008B … 208.40→#FF0000 … 1323.20→rgba(48,0,48,1)`,`lvlSchemeType:normal`/equalAreaLayerCount/labelConfig/lineConfig)——**现有 `DatasetChartDto` colorBar 解析器(AlphaScale::Unit)直接可用**。截图核对:散点按 v 值**绝对值离散映射**到 colorBar(同反演**网格** `colorAtDiscrete`,**非**反演原数据的连续 cauto),右侧 ColorBarWidget 显示离散级。夹具:`tests/fixtures/dd/ert-measurement-{scatter,rows,colorbar}.json`;原版截图 `meas-scatter-original.jpeg`。 + +**⚠️ 着色模型修正(2026-06-12 读原版 Plotly `_fullData` 实测,推翻"离散"判断)**:原版是 **Plotly scattergl**,散点**连续 cauto 着色**(非离散!):`marker.color`=原始 v 值(col4,**含负值**,本例范围 -1066.59~1232.44);`cauto:true` → `cmin/cmax`=数据 v 的真实 min/max(**含负离群值**);`colorscale`=连续 18 段,**position=colorBar值/maxColorBar值(1323.20)**;上色=`norm(v)=(v-cmin)/(cmax-cmin)` 查 colorscale 连续插值。验证 v=50.18→norm 0.4857→暗紫(stop 0.358 与 0.540 之间)。**这与 inversion 原数据完全同路(交接 §0.2:cauto 数据范围 + colorscale 位置 val/maxVal),measurement 必须用 `discreteColor=false` 走同一条已验收路径**,而非我 E2 误用的 colorAtDiscrete。负离群值撑大数据范围 → R0=25~250 全挤色阶中段暗紫,故原版几乎全暗紫(仅极端值现橙/蓝)。colorBar 图例(竖排右侧离散色块标 0.00~1323.20)独立于 cauto,显示 colorBar 自身 value→color。hover=浮动黑底框 X/Y/Value/a/b/m/n(另有底部 footer A=B=M=N=DataRow=Pseu_Resis=)。 + +**散点渲染规格(截图确认)**:x=**斜距 slopeDistance**(toolbar 默认,col1;≈平距,几乎不可辨,存疑标用户核对)、y=**伪深度 pseudoDepth**(col3,负向下)、色=**视电阻率 R0**(col16,离散 colorBar)、x 轴在**顶部**(同反演原数据)、y=0 处画 **40 个灰色菱形=电极位置**(electrodeList 的 x)、hover 显示 `A= B= M= N= DataRow= Pseu_Resis=`(col10-13=A/B/M/N + 值)。工具条(散点图/数据列表 切换、显示/隐藏、数据过滤、x/y/v/计算方法下拉、色阶配置、生成视电阻率、反演运算、另存为、导出)=**范围外**,MVP 只渲染默认静态视图。 +- 端点:scatter `GET dd/ert/measurement/scatter/graph?dsObjectId=&vFieldCode=`(空=默认R0)+ colorBar `POST lvl/colorGradation/getDetail{dsObjectId,businessCode:"R0",type:3}`;列表 `GET dd/ert/measurement/rows?dsObjectId=`。loaderKey `ert_measurement.scatter` 用 ApiBatch 组 scatter+colorBar 两请求(类比反演 loadChartAsync 的 scatter+type1)。 + +- `src/data/dto/MeasurementDto.{hpp,cpp}`:`parseMeasurementScatter(scatterJson, colorBarJson)→ScatterPayload`(位置数组 rows:x=col1 斜距/y=col3 伪深度/色值=col16;colorBar 复用现有解析器→离散 ColorScale;电极 electrodeList→灰菱形,可放 ScatterPayload 扩展或单列);`parseTable(QJsonObject)→TablePayload`(通用:列来自 `gridHeaderDisplay` 或 `filedList`,值预格式化为 QString;measurement 的 `vmap` 由解析器摊平为列——按 filedList 列码取 vmap[code])。 +- `src/app/panels/chart/DataTableView.{hpp,cpp}`:IDetailView + QTableView + QAbstractTableModel(由 TablePayload 构),列宽/标题/排序来自 TableColumn。 +- `src/app/strategies/MeasurementStrategy.hpp`:ddCode=`dd_ert_measurement_data`,tabs=`{{"散点伪剖面",Scatter,"ert_measurement.scatter",false},{"列表",Table,"ert_measurement.rows",true}}`。 +- `ApiDatasetRepository`:加 `ert_measurement.scatter`(scatter/graph,query 参数 + 默认 vFieldCode 空)、`ert_measurement.rows`(measurement/rows)loaderKey。 +- `DetailViewFactory`:加 `Table` case。`main.cpp`:注册 MeasurementStrategy 一行。 +- TDD 用 `tests/fixtures/dd` 夹具:`tests/data/test_measurement_dto.cpp`(散点位置解码/y 负号/列提取/vmap 摊平) + 控制器 2 页签谱 + loadAsync 分派。 +- 构建+测试绿 + 跑 app 双击「ERT原始数据」对照原版散点伪剖面 + 列表目视核对。 + +## 4. 风险与裁断(architect 评估) + +1. **不要过度泛化控制器信号面**:`tabReady(dsId,i,QVariant)` 单信号已吸收全部近期形状(类型擦除)。激进泛化"句柄/载荷"(合一 DetailLoad/QVariant,是纯去重),但控制器信号保持扁平最小。分页的 `pageNo` 等真做 dd_grid 再加一个默认参,加性无重构。 +2. **QVariant 类型擦除以编译期安全换扩展性**:错的 loaderKey→ViewKind→payload 三元组是运行期 cast miss 而非编译错。缓解:downcast 仅在每视图 `setPayload` 一处;坏 variant 出可见错误不崩(配合现 GuardedApplication::notify);每策略一条 round-trip 测试。接受此权衡(否则 N×3 个 XxxLoad/XxxReady/XxxData 爆炸)。 +3. **不要提前耦合 BLOCKED 的 GPR/radar**:引擎结构上可容纳(加 ViewKind::Image/GisMap + 新视图+解析器+loaderKey,控制器/句柄零改),故不被锁死;但现在不加任何 enum 值/桩(1:1 + 无样本)。trajectory 的 PolylineMap 是金丝雀,证明非图非表视图也能套进引擎。 + +**通用 Table 的边界**:list 形状(measurement/trajectory/grid)共用对;但 ① 分页(dd_grid 独有)= `PaginatedTableView` 装饰器或 spec `paginated` 标志 + page 发 `loadTab(...,pageNo)`,**基类 DataTableView 保持哑**;② 列格式化/值语义(measurement 的 vmap 摊平、gr 的 status 标志着色)= **parse-time** 处理(TablePayload 存预格式化 QString),需要彩色状态格时再加 `TableColumn.kind` 提示(YAGNI)。bar 不是表复用——gr_data 主页签是 Bar,列表页签才复用 parseTable。 + +## 5. 安全不变量(spec §5.0,务必保留) + +"abort 后绝不回灌"三件套:① 每层 `aborted_` 入口守卫;② 控制器句柄身份比对(`if (load!=inflight_.value(i)) return;`);③ 一律 `deleteLater`。错误判定:业务 `code!=200 || !rawError.isEmpty()`(HTTP 200 也可能 code=500)。迁移后这三件套从"按 Chart/Grid 阶段"改"按页签槽位",逐字保留,不得重造削弱。 + +## 6. 构建/测试命令 + +- 构建:`powershell.exe -ExecutionPolicy Bypass -File scripts/dev-build.ps1`(LNK1104 先 `Get-Process geopro_desktop|Stop-Process -Force`)。 +- 测试:`powershell.exe -ExecutionPolicy Bypass -File scripts/dev-test.ps1`(先 build)。基线 **122/122 绿**。 +- 单测过滤:`build/release/tests/geopro_tests.exe --gtest_filter=.*`。 diff --git a/docs/superpowers/specs/2026-06-11-apiclient-async-design.md b/docs/superpowers/specs/2026-06-11-apiclient-async-design.md new file mode 100644 index 0000000..bfc0620 --- /dev/null +++ b/docs/superpowers/specs/2026-06-11-apiclient-async-design.md @@ -0,0 +1,298 @@ +# 设计:ApiClient 异步化(DatasetDetail 路径试点) + +> 日期 2026-06-11。范围:把数据集详情路径从同步阻塞改为异步非阻塞,作为全 App 异步化的模式样板。 +> 后续会按此模式铺开到导航/登录路径(本期不做)。 + +## 状态更新(2026-06-11) + +**DatasetDetail 试点:✅ 已完成并通过评审。** 实现计划 `plans/2026-06-11-apiclient-async-datasetdetail.md`(8 任务,逐任务 spec+质量双评审 + 整体评审)。测试 75 → 89(+14 离线用例)全绿。落地原语:`IApiCall`/`ApiCall`/`ApiBatch`(net)、`ChartLoad`/`GridLoad`/`IAsyncDatasetRepository`(data)、控制器 abort-and-replace + 句柄身份比对 + 退出契约、`LoadingOverlay` 网格懒加载遮罩。核心收益落地:详情路径不冻 UI、慢请求可 abort 不回灌、多请求并发 + fail-fast。 + +**铺开进展(2026-06-12 更新):** +- **导航路径 ✅**(计划 `plans/2026-06-11-apiclient-async-rollout.md` Part A):新增 `ApiChain`(串行依赖链原语)、`NavRequest`(单请求句柄,QVariant payload)、`IAsyncProjectRepository`;`WorkbenchNavController` 全异步(NavRequest 续延 + 并发计数 + abort-and-replace + 身份比对,删 busy_/drain,busyChanged=在飞存在性)。 +- **登录路径 ✅**(同计划 Part B:B1/B2/B4):`AuthService` 异步(`CaptchaLoad`/`LoginLoad` + `ApiChain` 编排 verify→RSA→login2);`LoginWindow` 不冻 + 可取消(析构 abort);`test_auth` live 异步化。 +- 测试 89 → 116。每块逐任务 spec+质量双评审 + 整体评审通过。 + +**技术债清除 ✅(2026-06-12):** `ProjectListDialog` 已迁到 `IAsyncProjectRepository`(NavRequest + abort-and-replace + 身份比对 + 析构 abort),随即删除同步 `IProjectRepository`、`RepoResult`、`ApiProjectRepository` 9 个同步方法、`ApiClient` 同步 `get/postJson`+`await`(A6+B3 解锁完成)。**全 App 网络层现已 100% 异步,无 `QEventLoop` 阻塞、无过渡双接口债。** 测试 116/116。 + +**结论:异步化主题完成。** 数据详情(试点)+ 导航(Part A)+ 登录(Part B)+ 项目列表弹窗全部异步;同步路径彻底移除。 + +可选 follow-up(评审建议,非阻断,纯整洁):删 `DatasetDetailController::ChartData.grid/gridScale` 死字段;如未来引入 cross-thread 再补 `qRegisterMetaType>()`。 + +> 铺开实现计划:`plans/2026-06-11-apiclient-async-rollout.md`(Part A/B + 债务清除均已落地)。 + +## 1. 背景 + +geopro 现网络栈三层全同步阻塞: + +- **`ApiClient`**(`src/net/`):单个 `QNetworkAccessManager`,`get/postJson` 内部用 `QNetworkReply + QEventLoop` 死等响应。共享 cookie / JSESSIONID。 +- **Repository**(`ApiDatasetRepository`、`ApiProjectRepository`):同步调 `api_.get/postJson`,解析后返回 typed 值或抛 `std::runtime_error`。 +- **Controller**(`DatasetDetailController`、`WorkbenchNavController`):在 slot 内同步调 repo,再 `emit` 结果信号。 + +**核心问题**:每次请求都用 `QEventLoop` 阻塞 UI 线程 → 全 App 冻结。其中数据集详情「网格数据」页签的 `dd/ert/inversion/rows/{id}` 请求因**服务端网格化波动 1–4s**,期间界面完全卡死。 + +`QEventLoop` 阻塞的副作用是**可重入**:阻塞期间 Qt 仍泵事件,用户的双击/切换会重入 slot,因此现有控制器到处用 `busy_` 守卫挡重入——这些守卫本身就是同步阻塞架构的产物。 + +`QNetworkAccessManager` 本就是异步设计;当前架构用 `QEventLoop` 把它人为变同步,是反模式。本设计回归 Qt 原生异步。 + +## 2. 目标与非目标 + +### 目标 + +1. 数据集详情路径(原数据 + 网格数据)全程**不阻塞 UI 线程**。 +2. 慢请求在飞时切换/关闭数据集页签可**主动 abort 过期请求**,结果不回灌。 +3. 同一逻辑加载内的多个独立请求**并发**发出(原数据 2 个、网格 3 个),缩短总等待。 +4. 加载期间给用户**轻量"加载中"反馈**(不再因冻结而"看起来在响应")。 +5. 产出**可复用的异步原语**(`ApiCall` / `ApiBatch` / 句柄模式),供后续导航/登录路径照搬。 +6. 现有测试不退化;新增异步路径的单测可离线运行。 + +### 非目标(本期不做) + +- **不**改 `WorkbenchNavController` / `ApiProjectRepository` / `AuthService` / 登录流程——它们继续走同步 `ApiClient.get/postJson`。 +- **不**做后台工作线程:网络是 I/O 密集,`QNetworkAccessManager` 原生异步即可,不引入 `QThread`/`QtConcurrent`。 +- **不**追求与原版 loading 动画像素级一致(轻量提示即可,细抠后续单独做)。 +- **不**改 `LocalSampleRepository`(它实现旧同步 `IDatasetRepository`,但不经详情控制器使用)。 + +## 3. 已定决策(及理由) + +| 决策 | 选择 | 理由 | +|---|---|---| +| 范围 | 仅 DatasetDetail 路径 | 风险小、最痛点(1–4s rows)先解决、产出可复用模式 | +| 取消语义 | 主动 abort 过期请求 | 体验最好;句柄对象使 abort 成为一等公民 | +| 加载反馈 | 轻量"加载中"遮罩 + 禁用交互 | 不阻塞后必须给反馈;先跑通机制 | +| 异步机制 | 方案 A:每请求一个 QObject 句柄(信号 + abort) | 纯 Qt 信号、贴合现有信号驱动代码库;abort 用具体句柄最干净(QFuture 的 cancel 不中断底层 reply,反而要再加句柄);测试用 QSignalSpy 顺手 | +| 多请求汇聚 | **fail-fast with abort**(任一项失败立即回报并 abort 其余在飞 call) | 慢的 rows 1–4s,若 colorScale 200ms 先失败,gather-all 会让用户干等 1–4s 才看到失败(失败比成功还慢,违背"及时反馈")。fail-fast 依赖下方「abort 闸门」机制杜绝迟到回灌,原"避免部分-abort 竞态"的顾虑已被该机制消解。成功路径仍需全部到齐 | +| **abort 闸门(核心安全机制)** | 链路级 `aborted_` 标志 + 句柄身份比对 | 见 §5.1/§5.3:仅靠 disconnect 挡不住「已 emit、正在事件队列等待派发」的迟到信号;必须每层入口判 `aborted_` + 控制器比对句柄身份,才能兑现"abort 后绝不回灌"的核心承诺 | + +## 4. 架构 + +``` +DatasetDetailController (QObject,状态/编排) + │ 调 repo.loadChartAsync(id) / loadGridAsync(id),持有返回的 *Load 句柄 + │ 切换/关页 → 句柄.abort() + ▼ +IAsyncDatasetRepository(纯抽象) ←── ApiDatasetRepository 实现 + │ 每次 loadXxxAsync 创建一个 *Load 句柄,内部用 ApiBatch 并发 N 个 ApiCall + │ batch 全部完成 → 逐项校验+解析 → emit done(Parts) / failed(msg) + ▼ +ApiClient.getAsync(path) → ApiCall(包一个 QNetworkReply,实现 IApiCall) + (ApiClient 保留同步 get/postJson 供 Nav/登录继续使用,共享同一 QNetworkAccessManager) +``` + +**信号面保持不变**:`DatasetDetailController` 对外仍发 `chartReady(ChartData)` / `gridReady(GridData)` / `loadFailed(dsId,msg)` / `focusRequested(dsId)`,新增 `loadStarted(dsId, Phase)`。因此 `main.cpp` 接线与 `DatasetDetailPanel` 几乎不动,仅接 `loadStarted` 显示遮罩。 + +## 5. 组件设计 + +### 5.0 核心安全不变量:abort 后绝不回灌(贯穿全链路) + +> 这是本设计成败的关键。`disconnect` 只能阻止「将来才发」的信号,**挡不住「已经 emit、已转成 `QMetaCallEvent` 在事件队列中等待派发」的迟到信号**(尤其在共享 NAM 同步嵌套 `QEventLoop` 期间,见 5.1)。因此 disconnect 仅作"尽力而为",真正的权威闸门是下面两条: + +1. **每层 `aborted_` 标志 + 入口守卫**:`ApiCall` / `ApiBatch` / `ChartLoad` / `GridLoad` 各持一个 `bool aborted_`。所有 `finished`/`done`/`failed` 槽**入口第一行** `if (aborted_) return;`。`abort()` 先置 `aborted_=true` 再断连/中断。聚合 emit 前同样判 `aborted_`。 +2. **控制器句柄身份比对**:`DatasetDetailController` 在 `done`/`failed` 槽内校验「发信号的句柄 == 当前持有的 `chartLoad_`/`gridLoad_`」(用 `sender()` 或 lambda 捕获句柄指针比对),过期句柄的迟到信号直接丢弃。与 abort-and-replace 形成纵深防御。 +3. **销毁一律 `deleteLater`**:`ApiCall`/`ApiBatch`/`*Load`/`QNetworkReply` 在 abort 或完成后**一律 `deleteLater`,禁止任何同步 `delete`**——避免「abort 一个正在自己回调栈中的对象」导致 use-after-free。 + +### 5.1 net 层 + +#### `IApiCall`(新增,抽象,可测试缝) +```cpp +// src/net/IApiCall.hpp +class IApiCall : public QObject { + Q_OBJECT +public: + using QObject::QObject; + virtual void abort() = 0; // 中断;不再 emit finished;deleteLater +signals: + void finished(const geopro::net::ApiResponse& resp); // 成功/错误均经此(错误写 rawError) +}; +``` +存在意义:`ApiBatch` 只依赖 `IApiCall`,单测可注入假 call(不碰真实网络),离线测 gather/abort。 + +#### `ApiCall`(新增,实现 `IApiCall`) +- 持 `bool aborted_=false`(见 §5.0)。 +- 构造接管一个 `QNetworkReply*`;连接 `reply->finished` → 槽入口判 `if (aborted_) return;` → 解析为 `ApiResponse`(复用现 `parseBody`)→ emit `finished` → `deleteLater()` 自删。 +- `abort()`:`aborted_=true` → `disconnect` 自身与 reply 的连接 → `reply->abort()` → `reply->deleteLater()` → `this->deleteLater()`;**禁止同步 delete**。`reply->abort()` 本身会再触发一次 `finished`(OperationCanceledError),已被 disconnect + `aborted_` 双重挡掉。 +- 跨 `QueuedConnection` 传 `ApiResponse` 需 `qRegisterMetaType()`(应用启动时注册一次)。 + +#### `ApiClient`(扩展,不破坏现有) +```cpp +net::IApiCall* getAsync(const QString& path); // 立即返回,不阻塞 +net::IApiCall* postJsonAsync(const QString& path, const QJsonObject& body); +// 保留:ApiResponse get(path); ApiResponse postJson(path, body); // Nav/登录继续用 +``` +- `getAsync/postJsonAsync` 用同一 `nam_` 发起,返回 `new ApiCall(reply)`。token 注入、`buildRequest` 复用。 +- **已知限制(非"无害",受 §5.0 三条约束兜底)**:Nav 的同步嵌套 `QEventLoop`(`ApiClient::get` 的 `loop.exec()`)期间,详情的异步 reply 若完成会被一并泵出,其整条槽链(ApiCall→ApiBatch→*Load→Controller→`chartReady`→`detailPanel->openOrUpdate`)会**在 Nav 同步调用栈、嵌套循环里重入执行**,即详情 UI 重建可能发生在 Nav 网络调用返回前。这不是"互不写状态"能涵盖的,但有 §5.0 兜底:(a) 一律 `deleteLater` 杜绝栈内同步释放;(b) 各层 `aborted_` 入口守卫;(c) 控制器句柄身份比对。本期 Nav 仍同步、窗口期有限;**全异步后该同步路径与本限制一并消失**。可选缓解(YAGNI,暂不做):详情完成回调改 `QueuedConnection` 投递,避免在嵌套栈内驱动 UI。 + +#### `ApiBatch`(新增,汇聚原语,fail-fast) +```cpp +// src/net/ApiBatch.hpp +class ApiBatch : public QObject { + Q_OBJECT +public: + // calls 接管所有权;predicate 由 repo 注入,判定单个 ApiResponse 是否业务失败 + ApiBatch(QList calls, + std::function isFailure, + QObject* parent=nullptr); + void abort(); // aborted_=true,abort 全部未完成子 call,self deleteLater,不 emit +signals: + void succeeded(const QList& responses); // 全部成功,按下标对齐 + void failed(int index, const geopro::net::ApiResponse& resp); // 首个失败项 +}; +``` +- **所有权契约**:内部用 `QList>` 持有子 call;子 call 正常 `finished` 后自删(`QPointer` 自动置空)。`abort()` 遍历时跳过空指针。子 call parent 不设为 batch(各自 `deleteLater` 自管),靠 `QPointer` + `aborted_` 防悬垂。 +- 持 `bool aborted_`(§5.0)。每个子 call `finished` 槽入口判 `aborted_`。 +- **fail-fast**:每个子 call `finished` 到达时立即用 `isFailure(resp)` 判定——失败则 `emit failed(i, resp)` + `aborted_=true` + abort 其余在飞子 call + `deleteLater`;成功则记入 `responses_[i]`,全部到齐 → `emit succeeded(responses)` + `deleteLater`。 +- 业务/传输/HTTP 三类失败统一由 `isFailure` 判定(见 §7)。 + +### 5.2 data 层 + +#### data 层结果载体(中性,避免 data→controller 反向依赖) +```cpp +// src/data/api/DatasetLoads.hpp +struct ChartParts { core::ScatterField scatter; core::ColorScale scatterScale; }; +struct GridParts { core::Grid grid{1,1}; core::ColorScale gridScale; std::vector anomalies; }; +``` + +#### `*Load` 句柄(每次加载一个,承载 typed 结果 + abort) +```cpp +class ChartLoad : public QObject { + Q_OBJECT +public: + void abort(); // aborted_=true,转发给内部 ApiBatch +signals: + void done(const geopro::data::ChartParts& parts); + void failed(const QString& message); +}; +class GridLoad : public QObject { /* done(GridParts) / failed(QString) / abort() */ }; +``` +- 持 `bool aborted_`(§5.0);连 `ApiBatch::succeeded` 槽入口判 `aborted_` 后解析→ emit `done`→ `deleteLater`;连 `ApiBatch::failed` → 取 `msg`(见 §7)→ emit `failed`→ `deleteLater`。`abort()` 置 `aborted_` 并转发给内部 `QPointer`。 +- 用句柄对象做"每次逻辑加载"的天然关联点 + abort 目标(避免请求 id 簿记)。 + +#### 旧同步方法去向(接口改形审计) +`ApiDatasetRepository` 不再继承 `IDatasetRepository`。旧 6 个同步方法去向: + +| 旧方法 | 去向 | +|---|---| +| `loadStructure()` | **丢弃**(详情路径不用;现实现即返回 `{}` 占位;结构树走 `LocalSampleRepository`/Nav) | +| `loadScatter` + `loadScatterColorScale` | 合并进 `loadChartAsync` | +| `loadGrid` + `loadColorScale` + `loadAnomalies` | 合并进 `loadGridAsync` | + +`LocalSampleRepository` **继续实现同步 `IDatasetRepository`**,二接口并存;`buildWorkbench` 的 `repo` 形参类型不变。已核对:无任何处把 `ApiDatasetRepository` 当 `IDatasetRepository*` 用于 `loadStructure`,丢弃安全。 + +#### `IAsyncDatasetRepository`(新增抽象,详情控制器依赖它) +```cpp +// src/data/repo/IAsyncDatasetRepository.hpp +class IAsyncDatasetRepository { +public: + virtual ~IAsyncDatasetRepository() = default; + virtual data::ChartLoad* loadChartAsync(const std::string& dsId) = 0; // scatter + scatterScale(type1) + virtual data::GridLoad* loadGridAsync(const std::string& dsId) = 0; // grid(rows) + scale(type2) + anomalies +}; +``` +- 复合方法(而非旧 6 个原子 `loadXxx`):原子方法把"该发哪几个请求"泄漏给了控制器;汇聚+abort 是核心复杂度,集中放 repo(它本就知道请求集)是正确归属,控制器保持轻薄。 + +#### `ApiDatasetRepository`(改造) +- 不再继承同步 `IDatasetRepository`;改实现 `IAsyncDatasetRepository`。 +- `loadChartAsync`:建 `ApiBatch{ getScatter, getScatterScale }`,注入 `isFailure`(见 §7)→ `succeeded` 时复用现有 DTO 解析(`DatasetChartDto` 等)填 `ChartParts` 并 `done`;`failed` → 取错误原因并 `failed`。 +- `loadGridAsync`:`ApiBatch{ getRows, getColorScale, getException }` → 同构填 `GridParts`。 +- 现有同步解析/校验逻辑(DTO、v 行数校验、markType 钳制)与 `must()` 的判定(`code==200`)**原样搬入**,不重写——`must()` 的判定提炼为 batch 的 `isFailure` 谓词。 +- 空数据非失败:异常列表可能为空——业务 `code==200` 且 data 为空走成功(空 `GridParts.anomalies`),与现状一致。 + +### 5.3 controller 层 + +#### `DatasetDetailController`(改造) +```cpp +enum class Phase { Chart, Grid }; +signals: + void loadStarted(const QString& dsId, Phase phase); // 新增:驱动加载遮罩 + // 不变:chartReady(ChartData)/gridReady(GridData)/loadFailed(dsId,msg)/focusRequested(dsId) +private: + IAsyncDatasetRepository& repo_; + QPointer chartLoad_; // 当前在飞(QPointer 防悬垂) + QPointer gridLoad_; +``` +- `openDataset(dsId, ddCode)`:ddCode 非 `dd_inversion_data` → `loadFailed`;否则 **若 `chartLoad_` 在飞先 `abort()`**,`repo_.loadChartAsync` 取新句柄存入 `chartLoad_`,emit `loadStarted(dsId, Chart)`,连: + - `done` → **句柄身份比对**(捕获的句柄指针 == 当前 `chartLoad_`?否则丢弃迟到信号,见 §5.0)→ 组 `ChartData` → `chartReady` → 清空 `chartLoad_`。 + - `failed` → 同样身份比对 → `loadFailed` → 清空 `chartLoad_`。 +- `loadGridData(dsId, ddCode)`:同构,针对 `gridLoad_` 与 `gridReady`。 +- **移除 `busy_` 守卫**:由 abort-and-replace + 句柄身份比对取代(新请求来→abort 旧→发新;旧句柄的迟到信号被身份比对丢弃)。 +- 句柄完成/失败后清空对应 `QPointer`。**控制器析构时 abort 所有在飞句柄**(见 §7 退出契约)。 +- 现有 `catch(...)` 兜底不再需要(无同步抛出路径);错误统一经 `failed` 信号传递。 + +### 5.4 UI 层(加载反馈) + +- 新增 `LoadingOverlay`(`QWidget`,半透明 + "加载中…" + 可选 busy 指示),可贴在任一视图区上层。 +- `DatasetDetailPanel`: + - 接 `loadStarted(dsId, Grid)` → 在对应 ds 页的网格视图上显示遮罩 + 禁用交互;`gridReady`/`loadFailed` → 隐藏。 + - 原数据初次加载(~0.8s):页面仍于 `chartReady` 创建;加载期用 busy 光标 + 状态栏"加载中…"(最小改动)。网格遮罩是高价值项(页已存在、等待 1–4s)。 +- 像素级对齐原版 loading 留后续。 + +## 6. 数据流(时序) + +**原数据(双击数据集)** +1. `detailCtrl.openDataset(id, "dd_inversion_data")` → emit `loadStarted(id,Chart)`(UI 置 busy)。 +2. abort 旧 `chartLoad_`(若有)→ `repo.loadChartAsync(id)` 返回 `ChartLoad*`。 +3. repo 内 `ApiBatch{getScatter, getScatterScale}` 并发;UI 线程**保持响应**。 +4. 两 reply 全成功到齐 → `batch.succeeded` → repo 解析 → `ChartLoad::done(ChartParts)`。 +5. controller 身份比对通过 → 组 `ChartData` → `chartReady` → `detailPanel->openOrUpdate`(清 busy)。 + - **任一项失败(fail-fast)**:首个失败 → `batch.failed` + abort 其余在飞 → `ChartLoad::failed` → `loadFailed` → 状态栏提示(清 busy),不等其余请求。 + +**网格数据(首次切到网格页签)** +1. `gridDataNeeded` → `loadGridData(id, ...)` → emit `loadStarted(id,Grid)`(网格视图遮罩)。 +2. abort 旧 `gridLoad_` → `repo.loadGridAsync(id)`。 +3. `ApiBatch{getRows(慢1–4s), getColorScale, getException}` 并发。 +4. 全成功到齐 → 解析 → `done(GridParts)` → `gridReady` → `setGridData`(隐遮罩)。若 colorScale/exception 先失败 → fail-fast 立即 `failed` + abort rows,不干等 1–4s。 +5. 期间用户切走/关页 → controller abort `gridLoad_` → 子 reply 全 abort + `aborted_` 闸门 → 即便有迟到信号也被丢弃,无回灌。 + +## 7. 错误处理与边界 + +- **失败判定三要素(`isFailure` 谓词,由现 `must()` 提炼)**:一个 `ApiResponse` 视为失败当:(1) 传输错误 `!rawError.isEmpty()`;或 (2) HTTP 异常(必要时查 `httpStatus`);或 (3) **业务码 `code != 200`**(服务端常返回 HTTP 200 但 `code=500`,这是现 `must()` 的判定口径,**不能只看 httpStatus**)。失败原因取 `msg`(空则 `rawError`)。 +- **abort 竞态(核心,见 §5.0)**:单靠 disconnect 不足以挡「已入队的迟到信号」。权威闸门 = 各层 `aborted_` 入口守卫 + 控制器句柄身份比对 + 一律 `deleteLater`。`ApiCall::abort` 置 `aborted_` 后 disconnect+`reply->abort()`。 +- **重复触发**:abort-and-replace + 身份比对天然幂等(新覆旧,旧迟到信号丢弃)。 +- **控制器析构(退出契约)**:abort 所有在飞句柄。`ApiCall`/`ApiBatch`/`*Load` **不得在析构或 `deleteLater` 回调里访问 `nam`/`reply`**(reply 在 abort 时已 `deleteLater`)。main.cpp 栈对象析构逆序为 `detailCtrl` → `datasetRepo` → `api`,确保控制器先 abort、ApiClient 最后毁;句柄不持有对 `nam` 的长期引用。 +- **共享 NAM 同步/异步并存**:见 §5.1,**已知限制**(非"无害"),受 §5.0 三约束兜底,全异步后消除。 +- **空/缺数据**:异常列表可能为空——业务 `code==200` 走 `done`(空),非 `failed`,与现状一致。 + +## 8. 测试策略 + +- **`ApiBatch`(离线单测)**:注入假 `IApiCall`(受测可控 emit `finished`/记录 `abort`)。 + - 全成功 → `succeeded` 一次、下标对齐。 + - **fail-fast**:某子 call 先返回失败 → `failed(i,resp)` 一次 + 其余在飞子 call 被 `abort`(且不再等待)。 + - **abort 闸门**:batch `abort()` 后,未完成子 call 均被 abort;之后即便假 call 延迟 emit `finished`,batch 也不 emit(`aborted_` 守卫)。 +- **`DatasetDetailController`(离线单测,QSignalSpy + 事件循环)**:用 `StubAsyncRepo` 返回假 `*Load`,可控 emit `done`/`failed`(即时或 `QMetaObject::invokeMethod(..., QueuedConnection)` 模拟迟到)。异步桩非即时返回,断言需 `spy.wait()` / `QTest::qWait` spin 事件循环。 + - `done` → 一次 `chartReady`/`gridReady`;`failed` → 一次 `loadFailed`;非 `dd_inversion_data` → 直接 `loadFailed`。 + - 在飞时再 `openDataset` → 旧句柄被 `abort`(stub 记录)。 + - **回灌防护(回归原 bug 的核心用例)**:① 句柄 abort 后延迟 emit `done` → 控制器**零** `chartReady`(身份比对 + aborted_ 生效);② `openDataset(A)` 在飞 → `openDataset(B)` → A 句柄迟到 emit `done` → **仅 B 的数据被 `chartReady`,A 丢弃**。 +- **`ApiCall`/`getAsync`(可选 live test)**:依现有 `AuthLiveTest` 先例,标记为 live,不计入离线覆盖门槛。 +- 现有 75 个测试中,`DatasetDetailController.*` 两个改为异步桩版本(加事件循环 spin);其余不动。`LocalRepo.*` 不受影响(同步接口保留)。 +- 目标:异步新增逻辑(batch/controller)离线单测覆盖 ≥ 80%。 + +## 9. 迁移步骤(供 writing-plans 细化) + +1. net:`IApiCall` + `ApiCall` + `ApiClient::getAsync/postJsonAsync`(保留同步)。`parseBody` 抽为可复用。 +2. net:`ApiBatch` + 离线单测。 +3. data:`DatasetLoads.hpp`(ChartParts/GridParts)+ `ChartLoad`/`GridLoad` + `IAsyncDatasetRepository`。 +4. data:`ApiDatasetRepository` 改实现异步接口(搬入现有 DTO 解析)。 +5. controller:`DatasetDetailController` 改异步 + abort-and-replace + `loadStarted`;改其单测。 +6. `main.cpp` 装配换 `IAsyncDatasetRepository` + 启动注册 `qRegisterMetaType()`。 +7. UI:`LoadingOverlay` + `DatasetDetailPanel` 接 `loadStarted`。**此步与异步内核无依赖,可在步骤 1–6 通过测试后再做/并行**,以减小试点 PR 体积(surgical)。 +8. 全量构建 + 测试(dev-build / dev-test);手动验证:切换/关页 abort、网格遮罩、并发加载、UI 不冻、失败 fail-fast 即时报错。 + +## 10. 风险 + +| 风险 | 缓解 | +|---|---| +| **已入队的迟到信号回灌(最危险)** | §5.0 闸门:各层 `aborted_` 入口守卫 + 控制器句柄身份比对 + 一律 `deleteLater`;§8 专项回归用例 | +| abort 后回调悬垂/二次释放 | `QPointer` + abort 先置 `aborted_`+disconnect + **禁止同步 delete,仅 `deleteLater`** | +| 句柄/batch 生命周期泄漏 | 完成或 abort 后统一 `deleteLater`;`ApiBatch` 用 `QPointer` 持子 call | +| 同步/异步共用 NAM 嵌套重入 | **已知限制**(非无害);§5.0 三约束兜底;全异步后消除 | +| 退出期 UAF(句柄 deleteLater 晚于 ApiClient 析构) | 退出契约:句柄不在析构/回调访问 nam/reply;栈析构逆序保证 ctrl→repo→api | +| 失败比成功慢(gather-all 缺陷) | 改 fail-fast + abort 其余在飞 call | +| DTO 解析逻辑搬迁出错 | 原样搬入、不重写;`must()`→`isFailure` 谓词;保留现有解析单测 | +| 接口改形(原子→复合)波及面 | 仅详情控制器消费该接口;§5.2 旧方法去向表;`LocalSampleRepository` 不碰 | + +## 11. 相关文件 + +- 改:`src/net/ApiClient.{hpp,cpp}`、`src/data/api/ApiDatasetRepository.{hpp,cpp}`、`src/controller/DatasetDetailController.{hpp,cpp}`、`src/app/main.cpp`、`src/app/panels/.../DatasetDetailPanel.*`、`tests/controller/test_dataset_detail_controller.cpp`、各 `CMakeLists.txt`。 +- 新:`src/net/IApiCall.hpp`、`src/net/ApiCall.{hpp,cpp}`、`src/net/ApiBatch.{hpp,cpp}`、`src/data/api/DatasetLoads.hpp`、`src/data/api/DatasetLoadHandles.{hpp,cpp}`(ChartLoad/GridLoad)、`src/data/repo/IAsyncDatasetRepository.hpp`、`src/app/.../LoadingOverlay.{hpp,cpp}`、`tests/net/test_api_batch.cpp`。 +- 不动:`src/data/repo/IDatasetRepository.hpp`(同步,留给 LocalSampleRepository)、Nav/Auth/Project 路径。 + + diff --git a/docs/superpowers/specs/2026-06-11-dataset-detail-view-design.md b/docs/superpowers/specs/2026-06-11-dataset-detail-view-design.md new file mode 100644 index 0000000..148748c --- /dev/null +++ b/docs/superpowers/specs/2026-06-11-dataset-detail-view-design.md @@ -0,0 +1,341 @@ +# 数据集详情视图(平面图表)改造 设计 + +- 日期:2026-06-11 +- 分支建议:`feat/dataset-detail-chart` +- 状态:**ERT 反演(dd_inversion_data)展示功能已落地并经用户验收**;其余 dd 类型 + 工具条编辑功能待后续 + +## 状态更新(2026-06-11) + +**架构偏离(重要):** spec 原定渲染器为 **QGraphicsView**,实际落地改用 **QwtPlot(轴/交互/图例)+ VTK 算法层(等值线几何)+ 连续/离散色阶**(见返工方案 `plans/2026-06-11-dataset-detail-chart-v2-qwt.md`,权威)。展示结果视觉等价,下文 §5.2/§8 的 QGraphicsView 细节已被 QwtPlot 方案取代,保留作背景参考。 + +**已完成(仅 `dd_inversion_data` ERT 反演,§2.2 展示范围内):** 原数据散点(方形点/白描边/连续色阶/x 轴顶部 + **hover 显 X/Y/值**)+ 网格等值面(填充栅格 + 黑色等值线 + 沿线数值标注 + NaN 白边裁剪)+ 色阶图例 + 异常叠加 + 底部异常表/描述 + 多 Tab + 网格数据懒加载 + 页签内滚动/分割条 + 实时平移/滚轮缩放。数据加载已异步化(见 `specs/2026-06-11-apiclient-async-design.md`)。 + +**2026-06-12 渲染保真修复:** colorBar 是混合 hex+CSS-rgba 格式且 rgba 的 **alpha 为 0–1 浮点**;原 `DatasetChartDto` 用 `AlphaScale::Bit255` 解析致 12/18 段近透明(散点/网格/图例全发白)。修为 `AlphaScale::Unit`(一行根因修复,通用生效)。散点 `ColorMapService` 连续插值的归一化位置已证 1:1 等于原版 Plotly colorscale。新增 `ScatterHoverTip` 复刻原版 hovertemplate。详见 `HANDOFF-dataset-detail-chart.md §0.1`。 + +**未完成:** +- **其余 dd 类型的详情图渲染**(§2.4):`dd_ert_measurement_data`、`dd_ert_measurement_gr_data`、`dd_grid`、`dd_trajectory_data`、测井(深度/时序折线)、GPR(`dd/gpr/channel/image`)、TEM 等。控制器目前对非 `dd_inversion_data` 直接「暂不支持该类型预览」。**现实约束:当前租户仅 ERT/TEM/GPR 三类,GPR 对象无数据、无测井数据 → 多数类型无活样本,须先取样本。** 实现计划见 `plans/2026-06-11-dataset-detail-other-dd-types.md`(如已生成)。 +- **工具条编辑功能**(§2.3,范围外/后续单独立项):白化 / 滤波处理 / 色阶配置 / 异常框注 / 自动标注 / 网格化 / 另存为 / 导出 / 描述富文本 / 大视图全屏。当前为占位按钮。 +- 加载态:网格懒加载已有「加载中」遮罩;原数据初次加载仅 busy 光标,未做骨架屏。 +- 参考材料: + - 客户端菜单:`D:\Projects\GEOPRO\Geopro3.0 菜单.xlsx`「客户端」页签(R051–R096)、「测井参数表」「DD类型」 + - 原 web 系统:`http://tenant.geomative.cn/#/projectSpace/datasetMange/datasetInfo`(经 Playwright 操作页面 + 抓取 JS chunk 做源码级分析) + - API:`docs/apis/business_OpenAPI.json` + - 参考截图:`assets/web-datasetinfo-scatter.png`(原数据散点)、`assets/web-datasetinfo-grid-with-anomaly.png`(网格等值面 + 异常叠加 + 异常表) + +--- + +## 1. 背景与问题 + +客户端工作台的「数据详情」dock 当前实现有两个问题: + +1. **用了 VTK 渲染**:`main.cpp` 下方「数据详情」dock 是一个独立的 `QVTKOpenGLStereoWidget` + `vtkRenderer`,`rebuildDetail` lambda 用 `vtkBandedPolyDataContourFilter`/散点/电极 actor 配合 `applyTop2D` 正交相机"平躺"渲染剖面。数据集详情本质是**平面图表**,不该走 3D 渲染管线。 +2. **从未真正接上数据集选择**:`main.cpp:705-713` 数据集列表单击只调 `nav.selectDataset(dsId)`(驱动右下「数据集属性」表单),`main.cpp:604` 创建的 `currentDsId` 此后**再无任何赋值**,`itemClicked` 也不触发 `rebuildDetail`。即详情图表收不到选中的 dsId,点数据集时它是空的。 + +本设计将「数据详情」dock 重建为**本地面板 + 平面图表**(QGraphicsView),接真实 API,并真正接上数据集选择链路,目标 100% 复刻原 web 系统 datasetInfo 页面的展示功能。 + +--- + +## 2. 目标与范围 + +### 2.1 目标 +- 数据详情 dock 改为平面图表(QGraphicsView),**不再使用 VTK 渲染窗口**(VTK 仅作几何算法库)。 +- 100% 复刻原 web datasetInfo 的**展示**:原数据散点视图、网格等值面视图、色阶图例、等值线/标注、异常叠加 + 异常列表。 +- 接真实 API(`ApiDatasetRepository`)。 +- 架构按 **dd 类型驱动的图表策略框架**搭建,首版落地 `dd_inversion_data`(ERT 反演),其余 dd 类型作为框架内后续扩展。 + +### 2.2 范围内(仅展示) +- 原数据/网格数据 视图切换 +- 网格等值面渲染(混合方案,见 §8)+ 等值线 + 标注 +- 原数据散点渲染 +- 色阶图例(原数据 type1 / 网格 type2) +- 显示异常/电极/等值线 开关 +- 异常叠加(剖面上)+ 底部异常列表(数据集级,§7.3 辨析) +- 多 Tab 壳:一个或多个数据集详情页(R095) + +### 2.3 范围外(编辑/工具类,后续单独立项) +网格化参数(GridDialog)、色阶配置(colorEditor)、白化(WhiteningDialog)、滤波/迭代处理、异常框注/自动标注(AutoAnnotationDialog)、另存为(SaveAsDialog)、导出(ExportDialog)、描述富文本编辑、大视图(Esc) 全屏。 + +### 2.4 后续 dd 类型(框架内扩展,非本 spec) +其余 dd 类型详情图见下文 **§2.5(2026-06-12 全量实测编目)** 与实现计划 `plans/2026-06-11-dataset-detail-other-dd-types.md`。客户端按真实数据类型走 `ChartStrategyRegistry` 分派。 + +### 2.5 数据类型全景:设计 taxonomy(Excel)vs 实测运行 ddCode + 渲染映射(2026-06-12) + +> 用脚本直连 API 全量遍历两个账号(威立雅 + 赛盈地空"数据多"账号,20 项目/108 TM/752 DS)+ 逐个打开详情看截图确认渲染。**关键纠正:详情渲染由数据集"真实数据类型"决定,URL 详情链接的 `ddCode` 经常标错,客户端分派须用 ds 元数据的真实类型。** + +#### 2.5.1 实测 10 种 ddCode → 7 种渲染视图(均有活样本、看截图确认) + +| 渲染视图 | ddCode | 数据类型 | 客户端 | +|---|---|---|---| +| **① 原数据(Plotly散点,cauto) + 网格数据(等值面+等值线)** | `dd_inversion_data` | ERT/TEM 反演剖面、视电阻率数据 | ✅ 已做 | +| **② 柱状图(Y=电阻欧姆,X=电极点) + 列表** | `dd_ert_measurement_gr_data` | ERT接地电阻 | ❌ | +| **③ 散点伪剖面(斜距/伪深度,视电阻率着色,反演运算工具) + 数据列表** | `dd_ert_measurement_data` | ERT原始数据 | ❌ | +| **④ 轨迹:地图 + 列表 + 高程 3页签** | `dd_trajectory_data` | ERT电极坐标、TEM坐标 | ❌ | +| **⑤ 列表(序号/x/y)** | `dd_grid` | 白化数据 | ❌ | +| **⑥ 雷达剖面灰度图像(B-scan) + 单道波形(A-scan) + 对比度/灰度色阶/频谱** | `dd_gpr_channel_detail`、`dd_gpr_channel_image` | 雷达单通道剖面/图片列表 | ❌ | +| **⑦ 地图轨迹(真实GIS底图 + GPS路径 + 起点/终点)**(RTK 另带坐标点设置/解状态过滤面板) | `dd_radar_channel_trajectory`、`dd_radar_rtk_trajectory` | 雷达单通道轨迹/RTK轨迹 | ❌ | +| (无独立可视详情,疑数据中间态) | `dd_radar_preprocess_data` | 雷达预处理数据 | — | + +**共 7 种渲染视图,客户端已做 1 种(①);待做 6 种:② 柱状图、③ 散点伪剖面、④ 轨迹、⑤ 列表、⑥ 雷达剖面图像、⑦ 地图轨迹。** + +#### 2.5.2 与 `Geopro3.0 菜单.xlsx`「DD类型」设计 taxonomy 的对应 + +Excel「DD类型」页签是**设计期格式蓝图**,用的是另一套命名(`dd_Section`/`dd_Track`/`dd_ERTRawData`/`dd_GPRSection`/`dd_Images`/`dd_TimeVarious`…)。与实际运行 ddCode **命名对不上、语义对得上**: + +| Excel 设计格式 | 描述(Excel) | 对应实测运行 ddCode | 样本 | +|---|---|---|---| +| `dd_Section`("最重要":水平/垂直剖面) | 反演剖面 | `dd_inversion_data`(①)、`dd_gpr_channel_detail`(⑥) | ✅ | +| `dd_ERTRawData` | ERT原始数据 | `dd_ert_measurement_data`(③)、`dd_ert_measurement_gr_data`(②) | ✅ | +| `dd_GPRSection`(设计注:讨论是否并入 dd_Section) | 雷达剖面 | `dd_gpr_channel_detail`/`image`(⑥) | ✅ | +| `dd_Track`(坐标+轨迹合并) | 电极坐标/采集轨迹 | `dd_trajectory_data`(④)、`dd_radar_*_trajectory`(⑦) | ✅ | +| `dd_Images` | 图片类 | `dd_gpr_channel_image`(⑥) | ✅ | +| `dd_Files` | 文件打包(如三维雷达原始包) | `dd_radar_preprocess_data`(疑) | ✅部分 | +| `dd_TimeVarious` | 时间连续变量 | =设计中的 TEM时序/timeSensor 折线 | ❌ 无样本 | +| `dd_Stratigraphy` | 地层编录/**测井** | =设计中的测井折线/深度 | ❌ 无样本 | +| `dd_Segy` | 地震 SEGY(只展示) | — | ❌ 无地震数据 | +| `dd_Sampling` | 采样/化学(XRF/VOCs) | — | ❌ | +| `dd_LinearVarious` / `dd_Structual3D` / `dd_Property3D` / `dd_Video` | 线性变量/三维结构/三维属性/视频 | — | ❌ | +| `dd_KML`/`dd_GeoJson`/`dd_shp`/`dd_dem`/`dd_bim`/`dd_czml`… | 背景资料**图层**(导入/预览/坐标对齐) | — | 非"数据详情图",是地图图层 | + +**结论:** Excel 是"应有哪些数据格式"的设计蓝图;实测 10 种是"现在真有数据、详情页真能出图"的子集(电法 ERT + 地质雷达 GPR 两大类的落地)。两者语义吻合;Excel 里 时序(dd_TimeVarious)/测井(dd_Stratigraphy)/地震(dd_Segy)/采样/三维模型/视频 等当前**无活样本,BLOCKED**(与 plan §1.2 一致);KML/shp/dem/bim 等是地图图层,不属本 spec 的详情图范畴。 + +#### 2.5.3 枚举 DS 的 API(实测可用,踩坑记录) + +`POST my/profile/project/page`{pageNo,pageSize} → `GET projectStruct/queryProjectStruct/{pid}`(扁平节点:type=1=项目/GS,type=2=TM,parentId 链表层级;TM 可挂项目或 GS 下)→ `POST dsObject/data/page` body **`{projectId, structParentId:, structParentConfType:2, classifyTypeList:[3], pageNo, pageSize}`**(字段是 `structParentId` 不是 `tmObjectId`;必须带 `classifyTypeList:[3]` 否则后端 `code:500`;`pageNo` 不是 `pageNum`;时序类另走 `dsObject/timeSensor/data/page`;后端间歇 500 需重试)。DS 字段:`ddCode`+`dsTypeCode`(英文)+`name`(中文类型名)+`dsName`(文件)。 + +> 现实约束:当前可访问租户实测有样本的方法类为 ERT/TEM/GPR;测井/地震/采样/三维/时序无活样本。无样本类详情页**无可参照**,须待数据样本到位后再精确 1:1 复刻。 + +--- + +## 3. 原 web 系统分析(源码级) + +> 方法:Playwright 操作 datasetInfo 页面 → 抓取无障碍快照、网络请求、localStorage、活 API 响应;进一步抓取并分析前端 JS chunk(`contourChartAdapter`/`plotlyScatterSimpleChart`/`contourPage`/`colorUtils` 等)。无 sourcemap,代码已压缩但可读。 + +### 3.1 页面结构(客户端只取内容区,左侧 web 导航不复刻) +标题(ds 名)→ `原数据 | 网格数据` 切换 → 工具条 → 图表 → 色阶图例 →(网格视图)底部 `异常列表 | 描述` 表。 + +### 3.2 原数据散点 —— Plotly `scattergl`(`plotlyScatterSimpleChart`) +``` +type:"scattergl", mode:"markers", +marker:{ size:10-12, color:vlist, colorscale, zmin/zmax, symbol:"square", + line:{color:white, width:1} } +yaxis.scaleanchor = x // x:y 等比锁定 +colorbar:{ title:"数值", thickness:15, len:.7 } // 可选 +``` +方形点、按 `vlist` 经色阶着色、白描边、等比。 +→ **客户端用 `QGraphicsRectItem`(方块)按色阶着色复刻输出,不引入 Plotly。** + +### 3.3 网格等值面 —— 自制 marching-squares 引擎(`contourChartAdapter`,canvas) +源码为自带等值线库(`window.contourCore`),API:`computeContours / marchingSquares / pathFinding / levels / smooth / nullHandling / labels / colorbar / renderers / axes / Overlay`。 +- **渲染模式**:`heatmap`(`putImageData` 逐格 + canvas 缩放平滑)/ `fill`(填充色带 `drawFilledPaths`)/ `lines`(等值线 `drawStrokePaths`)/ `fill+lines`(默认显示:色带 + 等值线 + 标注)。 +- **levels** = colorBar 离散分段值;`mapColors(v, vmin, vmax, colorscale)` 着色。 +- **额外处理**:2× 双线性上采样(`DEFAULT_UPSAMPLE_SCALE:2`)+ 平滑(`DEFAULT_SMOOTHING:.3`)+ 简化容差(`simplifyTolerance` 默认 `.5`,滑块)+ **数据凸包裁剪**(`makeBinaryMask`/`createClipPath`/`DEFAULT_CLIP_LEVEL:.95`,对应截图里随地形起伏的不规则白边)+ 标注(`showLabels:true`)。 + +### 3.4 数据接口(全部真实 API 验活) +| 用途 | 接口 | 返回(关键字段) | +|---|---|---| +| 原数据散点 | `GET dd/ert/inversion/getErtRawDataScatterGraph/{dsId}` | `xlist/ylist/vlist/hlist/projectXList/projectYList`(各 1D,本例 240)、`min/max` | +| 网格等值面 | `GET dd/ert/inversion/rows/{dsId}` | `DDErtInversionGraphDataVO`:`x[nx]`、`y[ny]`、`v[ny][nx]`、`z[ny][nx]`(地形)、`elevation[nx]`、`vmin/vmax`、`sectionType`(1垂直/2水平) | +| 色阶 | `POST lvl/colorGradation/getDetail`(body `{dsObjectId, businessCode:"", type}`,type1=原数据 / type2=网格) | `data.properties.colorBar=[[值,"rgba(r,g,b,a)"],…]`、`lvlMinMax`、`lineConfig{showLines,color,lineType}`、`labelConfig{showLabels,color}` | +| 异常 | `GET exception/queryException/{dsId}` | 见 §6.4(与 `core::Anomaly` 一致) | + +> 注:网格等值面数据来自服务端**已网格化**的 `inversion/rows`,前端不做插值;前端"网格"按钮是把 `xSpacing/xsize…` POST 给 `inversion/grid` 让服务端重算(范围外)。 + +--- + +## 4. 架构总览 + +**核心思路**:详情 dock 的渲染器从「VTK 3D 管线」换成「VTK 仅算等值面几何 + QGraphicsView 画 2D 场景」。数据模型、仓储接口复用,新增渲染器 + 面板壳 + dd 策略 + API 仓储 + 一个小详情控制器;并真正接上数据集选择链路。 + +``` +┌─ 数据详情 dock(重建)──────────────────────────────────────┐ +│ DatasetDetailPanel (QTabWidget 壳, 每 ds 一页, R095 多Tab) │ +│ └─ DatasetDetailPage │ +│ ├─ 标题栏(ds 名) │ +│ ├─ 原数据 / 网格数据 切换 + 显示异常/电极/等值线 开关 │ +│ ├─ DatasetChartView : QGraphicsView ← 新渲染器 │ +│ │ scene: 等值面色带 / 散点方块 / 坐标轴 / 异常 / 图例 │ +│ └─ AnomalyTablePanel(ds 级异常表, 行显隐→图表叠加) │ +└─────────────────────────────────────────────────────────────┘ + 右上「对象异常」面板(不动)= TM 异常总和, 眼睛→中央 VTK 地图 + 右下「数据集属性」面板(不动)= dynamicForm 表单 + ▲ 渲染输入(复用现有 core 模型) ▲ dd 类型分派 +┌─ chart 层(新)──────────────────┐ ┌─ 策略注册(新)──────────────┐ +│ ContourBuilder (src/render/) │ │ IDatasetChartStrategy │ +│ VTK 仅算法: 上采样+平滑→ │ │ + ErtInversionStrategy(首个)│ +│ vtkBandedPolyDataContourFilter │ │ registry: ddCode → strategy │ +│ → 分层带多边形+等值线(几何,无窗口)│ │ (扩展点: 测井/时序/GPR…) │ +└──────────────────────────────────┘ └──────────────────────────────┘ + ▲ 数据模型(复用,零改动) +┌─ core::Grid / ScatterField / ColorScale / Anomaly ──────────┐ +└─────────────────────────────────────────────────────────────┘ + ▲ 加载 +┌─ IDatasetRepository(接口已存在)───────────────────────────┐ +│ loadGrid / loadScatter / loadColorScale / loadAnomalies │ +│ · ApiDatasetRepository(新, 真实 API, §6.3) │ +│ · LocalSampleRepository(保留, 离线/测试) │ +└─────────────────────────────────────────────────────────────┘ + ▲ 编排(新小控制器, 不污染 WorkbenchNavController) +┌─ DatasetDetailController(新, 持 IDatasetRepository + 注册表)┐ +│ 在 main.cpp 与 nav.selectDataset 并联挂到 datasetList 单击 │ +│ 读 ddCode → 选策略 → 拉数据 → 发信号 → 面板被动渲染 │ +└─────────────────────────────────────────────────────────────┘ +``` + +**关键边界** +- **中央 2D/3D 地图视图仍是 VTK**(在地图上显示数据集是 VTK 的正职),本次只换数据详情 dock。 +- **VTK 在新链路里只当算法库**(无 render window);**新增依赖仅 Qt Widgets 的 `QGraphicsView`**(LGPL 合规),无第三方图表库。 + +--- + +## 5. 组件细化 + +### 5.1 `ContourBuilder`(新,`src/render/`,VTK 仅算法) +复用 `GridContourActor.cpp:36-82` 已验证的「`core::Grid` → `vtkStructuredGrid` → `vtkDataSetSurfaceFilter` → `vtkBandedPolyDataContourFilter`(按 colorBar 真实非均匀分段 + `GenerateContourEdgesOn`)」管线,**只在输出端分叉**:不建 mapper/actor,而是从 `banded->GetOutput()`(port0, cell 标量) 提取色带多边形、从 port1 提取等值线折线。 + +混合保真预处理(见 §8):入网格先 **2× 双线性上采样 + 高斯平滑** → banded → 按 NaN 掩膜做**数据凸包裁剪** → 等值线 Douglas-Peucker **简化**(容差参数)。 + +- 入:`core::Grid`(含 NaN 标记无效区)+ `core::ColorScale`(分段值)+ `ContourOptions{upsample, smooth, simplifyTol, showLines, showLabels}`。 +- 出:`std::vector ring; }>` + `std::vector pts; }>`。 +- 纯函数、无 Qt 依赖、可单测(给定网格→断言色带数/层级/裁剪边界)。 + +### 5.2 `DatasetChartView : QGraphicsView`(新,`src/app/panels/chart/`) +持 `QGraphicsScene`,对外:`showContour(grid, scale, opts)` / `showScatter(field, scale)` / `setAnomalies(list, hidden)` / `setOverlays(showAnomaly, showElectrode, showContourLine)`。 +- Item:色带 `QGraphicsPathItem`(填充)、等值线 `QGraphicsPathItem`、散点 `QGraphicsRectItem`(方块, 白描边)、异常 `QGraphicsPathItem`(可拾取)、电极顶部标记、标注 `QGraphicsSimpleTextItem`。 +- **坐标轴**:viewport 层 overlay 自绘,监听 `transform` 变化重算刻度(缩放/平移时轴钉边缘)。**y 轴用高程、向上为正**(场景 y 翻转);散点视图 **x:y 等比**(对齐 Plotly `scaleanchor`)。 +- 交互:滚轮缩放、拖动平移、点击异常高亮、重置视图。 +- **同一个类**供详情页、(后续)大视图、自动框注对话框复用——普通 QWidget、无 OpenGL 上下文负担。 + +### 5.3 dd 类型策略(新,扩展点,`src/app/panels/chart/`) +```cpp +struct IDatasetChartStrategy { + virtual ~IDatasetChartStrategy() = default; + virtual std::string ddCode() const = 0; + virtual void load(DatasetDetailController&, const std::string& dsId) = 0; // 决定拉哪些数据、画什么 +}; +``` +- `ErtInversionStrategy`(首个,`ddCode()=="dd_inversion_data"`):原数据→`loadScatter`+散点;网格→`loadGrid`+`ContourBuilder`+色带/等值线;异常→`loadAnomalies`。 +- `ChartStrategyRegistry`:`ddCode → strategy`;未注册 → 页内占位「暂不支持该类型预览」(优雅降级)。 + +### 5.4 `ApiDatasetRepository`(新,实现 `IDatasetRepository`,真实 API,`src/data/api/`) +| 方法 | 接口 | 解析(→ `src/data/parse/`) | +|---|---|---| +| `loadScatter(dsId)` | `getErtRawDataScatterGraph/{dsId}` | → `ScatterField`(xlist/ylist/hlist/vlist→x/y/z/v,projectX/Y) | +| `loadGrid(dsId)` | `inversion/rows/{dsId}` | `DDErtInversionGraphDataVO` → `Grid`(x/y/v/z/elevation/vmin/vmax) | +| `loadColorScale(dsId, variant)` | `colorGradation/getDetail`(type1/2) | `properties.colorBar[[值,rgba]]` → `ColorScale::addStop` | +| `loadAnomalies(dsId)` | `queryException/{dsId}` | → `core::Anomaly[]`(§6.4) | +> 现有 `IDatasetRepository` 的 `loadColorScale` 需加 `variant`(原数据/网格) 参数;`loadScatterColorScale` 可并入。`LocalSampleRepository` 保留为离线/测试桩。 + +### 5.5 `AnomalyTablePanel`(新或复用 `AnomalyListPanel`,`src/app/panels/`) +ds 级异常表,列:名称 / 异常类型 / 几何类型 / 创建时间 / 备注 / 操作(仅展示版:显隐眼睛;定位)。行显隐 → 发信号驱动 `DatasetChartView::setAnomalies` 的 hidden 集(对齐现有 `hiddenAnoms` 语义)。 + +### 5.6 `DatasetDetailPanel` / `DatasetDetailPage`(新,`src/app/panels/`) +- `DatasetDetailPanel`:`QTabWidget`,按 dsId 去重托管多个 `DatasetDetailPage`(R095)。 +- `DatasetDetailPage`:标题 + 原数据/网格数据 切换 + 叠加开关 + `DatasetChartView` + `AnomalyTablePanel`。 + +### 5.7 `DatasetDetailController`(新,`src/controller/`) +持 `IDatasetRepository&` + `ChartStrategyRegistry&`;slot `openDataset(dsId, ddCode)`:选策略→拉数据→`emit chartReady(pageModel)`;被动视图。**不并入 `WorkbenchNavController`**(后者只持 `IProjectRepository`、专注项目/结构/异常树导航,混入图表数据会破坏单一职责)。 + +--- + +## 6. 数据模型与映射 + +### 6.1 复用的 core 模型(零改动) +- `core::Grid`(`Field.hpp`):`nx/ny/valueAt(i,j)`、`x/y`、`z[nx*ny]`、`elevation[nx]`、`vmin/vmax` ⇆ `DDErtInversionGraphDataVO`。 +- `core::ScatterField`:`x/y/z/v`、`projX/projY` ⇆ 散点 VO。 +- `core::ColorScale`:阶梯色阶 `addStop/colorAt/stopValues` ⇆ colorBar。 +- `core::Anomaly`:`markType`(点/线/面)、`localPts`、`lineColor/lineWidth/dashed` ⇆ queryException。 + +### 6.2 NaN / 无效区 +`inversion/rows` 的 `v` 在测区外为无效;`core::Grid` 用 NaN 标记,`ContourBuilder` 据此做凸包裁剪(§8),渲染成不规则白边。 + +### 6.3 色阶(colorBar) +`properties.colorBar` 为 `[[值字符串, "rgba(r,g,b,a)"], …]`(本例 17 段),`type1`(原数据)/`type2`(网格) 颜色相同、分段值不同。映射:每段 `addStop(parseDouble(值), core::parseColor(rgba))`;`lineConfig` 驱动等值线显隐/色/线型。 + +### 6.4 异常(queryException → core::Anomaly,活数据验证) +``` +exceptionName → name +exceptionTypeName(异常区) → typeName +exceptionMarkType(1点/2线/3面) → markType +exceptionMarkTypeName(多段线…) → 表"几何类型"列 +location.coordinate[{x,y}] → localPts (剖面局部坐标: x=桩号, y=高程) +legend.polylineColor/Width → lineColor/lineWidth +legend.polylineShape=="dash"→ dashed +createTime/remark → 表"创建时间/备注"列 +geographicalCoordinates / latitudeLongitude → (地图用, 详情视图不需要) +``` +> 客户端已有 `parseExceptions` 映射此结构(`Anomaly.hpp` 注释印证),活数据已验证一致。 + +--- + +## 7. 数据流与交互 + +### 7.1 选择 → 渲染 +`datasetList` 单击 → 同时:(a) `nav.selectDataset(dsId)`(现状,驱动右下属性表单);(b) `detailCtrl.openDataset(dsId, ddCode)`(新)→ 选策略 → 拉 scatter/grid/colorScale/anomalies → `emit chartReady` → `DatasetDetailPanel` 渲染。 + +### 7.2 Tab 与联动(R055-057, R095) +- 双击数据集 = 新建或聚焦已开页(按 dsId 去重);单击 = 聚焦已开页(若已打开)。 +- 活动 Tab 切换 → 反向高亮数据集列表对应项。 +- 视图切换 原数据/网格数据;叠加开关 显示异常/电极/等值线。 + +### 7.3 异常归属辨析(决策记录) +原 web datasetInfo 是**单页**,底部异常表是单页布局产物。客户端是**多面板工作台**,存在两处不同作用域的异常显示,**经决策保留两者、各管各的视图**: + +| 位置 | 接口 | 作用域 | 眼睛控制 | +|---|---|---|---| +| 右上「对象异常」面板(现状不动) | `queryExceptionByTmObjectId/{tmId}` | 对象/TM 级(勾选驱动,多 TM 聚合) | 中央 VTK 2D/3D **地图** | +| 详情 dock 底部「异常列表」(本 spec, 复刻 web) | `queryException/{dsId}` | 数据集级(更直观) | **图表剖面**叠加 | + +剖面上的**异常叠加**属"画在图上",天然归详情视图;右上面板的列表与地图叠加不变。 + +--- + +## 8. 渲染保真度(混合方案) + +网格等值面采用 **VTK 算法 + 预处理** 混合方案(~95% 保真,复用 VTK,不重写整套引擎): + +1. **2× 双线性上采样** `core::Grid`(对齐 web `DEFAULT_UPSAMPLE_SCALE:2`)——可用 `vtkImageData` + `vtkImageResize`/手写双线性。 +2. **高斯平滑**(对齐 web `DEFAULT_SMOOTHING:.3`)。 +3. `vtkBandedPolyDataContourFilter` 按 colorBar 分段值出**色带多边形 + 等值线**。 +4. **数据凸包裁剪**:按 NaN 掩膜裁掉测区外(对齐 web `makeBinaryMask`/`clipLevel .95`),得不规则白边。 +5. 等值线 **Douglas-Peucker 简化**(容差参数,默认 0.5,对应"简化容差")。 +6. 默认 `fill+lines + showLabels`;等值线色/显隐由 colorBar `lineConfig` 驱动。 + +原数据散点:`QGraphicsRectItem` 方块、按色阶着色、白描边、**x:y 等比**(对齐 Plotly `scaleanchor`)。 + +> 与「轻量 VTK banded 原生」(~85%, 矩形范围/块状) 和「高保真整体移植引擎」(~100%, 工作量最大) 相比,混合方案是保真度/工作量的折中。 + +--- + +## 9. 错误 / 空 / 加载态 +- 加载中:图表区骨架/转圈。 +- 空态:异常表「暂无数据」;未注册 ddCode「暂不支持该类型预览」;无网格/散点数据时空图占位。 +- 错误:API 失败 → 页内内联错误(沿用 `RepoResult` 错误传播,不崩)。 +- 跨项目上下文/数据过期:优雅降级。主题令牌复用(明暗一致)。 + +--- + +## 10. 测试策略(GoogleTest/CTest) +- **单元**:`ContourBuilder`(给网格→断言色带数/层级/凸包裁剪/简化点数);`ColorScale`(colorBar 解析、`colorAt` 边界);解析器(散点/反演网格/色阶/异常 VO→模型);`ChartStrategyRegistry` 查找与降级。 +- **组件**:`DatasetChartView` 按数据生成 scene item 数;叠加开关隐藏对应 item;`AnomalyTablePanel` 行开关发信号;`DatasetDetailPanel` 按 dsId 去重 Tab。 +- **集成**:`DatasetDetailController.openDataset` 编排(用 `LocalSampleRepository` 桩)→ 断言 `chartReady` 负载。 +- **视觉**:对照 `assets/web-datasetinfo-*.png` 人工核对散点/等值面/异常叠加。 + +--- + +## 11. 风险与开放问题 +1. **凸包裁剪精度**:web 用 binary mask + clipLevel .95;VTK 侧用 NaN 掩膜裁剪需调参,可能与 web 边界略有出入(在 95% 目标内可接受)。 +2. **坐标轴钉边重算**:QGraphicsView 缩放时轴刻度重算是已知但非免费的模式,需专门处理。 +3. **`IDatasetRepository.loadColorScale` 签名变更**(加 variant)会动到 `LocalSampleRepository` 与现有 VTK 详情调用点——后者将随本次重建移除,注意配平。 +4. **其它 dd 类型无活样本**(测井/GPR/TEM 详情页):框架预留,落地需数据样本。 +5. **大数据集性能**:散点 240 点、网格 32×100→上采样 64×200 量级,QGraphicsView 可承受;更大测线需评估(web 用 `hightPerformaceContainer`,本版先不优化)。 + +--- + +## 12. 落地顺序(供 plan 细化) +1. `core::Grid` NaN 支持 + `ContourBuilder`(VTK 几何提取 + 上采样/平滑/裁剪/简化)+ 单测。 +2. `ApiDatasetRepository`(4 接口 + 解析)+ `loadColorScale` variant + 单测。 +3. `DatasetChartView`(散点/等值面/轴/图例/异常叠加 + 交互)。 +4. dd 策略框架 + `ErtInversionStrategy` + 注册表。 +5. `AnomalyTablePanel` + `DatasetDetailPage/Panel`(多 Tab)。 +6. `DatasetDetailController` + `main.cpp` 接线(移除旧 VTK 详情 dock,接上数据集单击/双击 + 反向联动)。 +7. 视觉核对 + 集成测试。 diff --git a/docs/superpowers/specs/assets/web-datasetinfo-grid-with-anomaly.png b/docs/superpowers/specs/assets/web-datasetinfo-grid-with-anomaly.png new file mode 100644 index 0000000..f88a571 Binary files /dev/null and b/docs/superpowers/specs/assets/web-datasetinfo-grid-with-anomaly.png differ diff --git a/docs/superpowers/specs/assets/web-datasetinfo-scatter.png b/docs/superpowers/specs/assets/web-datasetinfo-scatter.png new file mode 100644 index 0000000..ffaf9e8 Binary files /dev/null and b/docs/superpowers/specs/assets/web-datasetinfo-scatter.png differ diff --git a/scripts/dev-build.ps1 b/scripts/dev-build.ps1 new file mode 100644 index 0000000..cd44eab --- /dev/null +++ b/scripts/dev-build.ps1 @@ -0,0 +1,8 @@ +# 本地开发构建脚本(封装 VS MSVC 环境 + VS 自带 cmake,构建 build/release)。 +# 用法(在仓库根,用 PowerShell 工具运行): pwsh -File scripts\dev-build.ps1 +$ErrorActionPreference = 'Stop' +$vcvars = 'D:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvars64.bat' +$cmake = 'D:\Program Files\Microsoft Visual Studio\18\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe' +$bdir = 'D:\Git\lanbingtech\geopro\build\release' +cmd /c "`"$vcvars`" >nul 2>&1 && `"$cmake`" --build `"$bdir`"" +exit $LASTEXITCODE diff --git a/scripts/dev-test.ps1 b/scripts/dev-test.ps1 new file mode 100644 index 0000000..d977b11 --- /dev/null +++ b/scripts/dev-test.ps1 @@ -0,0 +1,11 @@ +# 本地开发测试脚本(VS 自带 ctest 跑 build/release,可选 -Filter 按用例名过滤)。 +# 用法: pwsh -File scripts\dev-test.ps1 # 全量 +# pwsh -File scripts\dev-test.ps1 ContourBands # 仅匹配 ContourBands.* 的用例 +param([string]$Filter = '') +$ErrorActionPreference = 'Stop' +$ctest = 'D:\Program Files\Microsoft Visual Studio\18\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\ctest.exe' +$bdir = 'D:\Git\lanbingtech\geopro\build\release' +$cargs = @('--test-dir', $bdir, '--output-on-failure') +if ($Filter) { $cargs += @('-R', $Filter) } +& $ctest @cargs +exit $LASTEXITCODE diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index db8ac21..04437d0 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -12,6 +12,10 @@ find_package(VTK REQUIRED COMPONENTS ) find_package(nlohmann_json CONFIG REQUIRED) find_package(Qt6 REQUIRED COMPONENTS Svg) +# trajectory 地图(QWebEngineView + Leaflet + 天地图)实现于 TrajectoryMapView.{hpp,cpp} + +# resources/map/。WebEngine 硬依赖模块 Qt6WebChannel / Qt6Positioning 已补装,恢复 find_package/link/ +# deploy 三处 + main.cpp 初始化,DetailViewFactory 的 WebMap 分支切回 TrajectoryMapView。 +find_package(Qt6 REQUIRED COMPONENTS WebEngineWidgets WebEngineQuick) add_executable(geopro_desktop WIN32 main.cpp @@ -26,9 +30,31 @@ add_executable(geopro_desktop WIN32 panels/ObjectTreePanel.cpp panels/DynamicFormView.cpp panels/ObjectExceptionPanel.cpp + panels/DescriptionPanel.cpp + panels/chart/RawDataChartView.cpp + panels/chart/GridDataChartView.cpp + panels/chart/DataTableView.cpp + panels/chart/TablePager.cpp + panels/chart/BarChartView.cpp + panels/chart/LineChartView.cpp + panels/chart/TrajectoryMapView.cpp + panels/chart/DetailViewFactory.cpp + resources/map/map.qrc + panels/chart/ChartTheme.cpp + panels/chart/ColorMapService.cpp + panels/chart/ColorBarWidget.cpp + panels/chart/ScatterPlotItem.cpp + panels/chart/ContourPlotItem.cpp + panels/chart/LivePanner.cpp + panels/chart/ScatterHoverTip.cpp + panels/AnomalyTablePanel.cpp + panels/LoadingOverlay.cpp + panels/DatasetDetailPage.cpp + panels/DatasetDetailPanel.cpp CentralScene.cpp ProjectListDialog.cpp - SettingsDialog.cpp) + SettingsDialog.cpp + Logging.cpp) target_include_directories(geopro_desktop PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) # QtKeychain 经 FetchContent 接入,头文件不随 target 传播,显式加源/构建目录(含生成的 export 头)。 @@ -36,6 +62,7 @@ target_include_directories(geopro_desktop PRIVATE ${qtkeychain_SOURCE_DIR} ${qtk target_link_libraries(geopro_desktop PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Svg + Qt6::WebEngineWidgets Qt6::WebEngineQuick ${VTK_LIBRARIES} ads::qt6advanceddocking qt6keychain @@ -47,8 +74,20 @@ target_link_libraries(geopro_desktop PRIVATE geopro_controller # Phase 5:导航编排(WorkbenchNavController) ) +# Qwt(二维科学图表;源码存在时由根 CMake 定义 qwt 目标)。 +# cmake/qwt.cmake 已加 QWT_MOC_INCLUDE=1,Qwt Q_OBJECT 类的 MOC 代码直接编译进各 .cpp.obj, +# 标准按需链接可正确解析所有 MOC 符号(无需 /WHOLEARCHIVE)。 +if(TARGET qwt) + target_link_libraries(geopro_desktop PRIVATE qwt) +endif() + vtk_module_autoinit(TARGETS geopro_desktop MODULES ${VTK_LIBRARIES}) +# 崩溃 minidump:DbgHelp(Windows 自带)提供 MiniDumpWriteDump。 +if(WIN32) + target_link_libraries(geopro_desktop PRIVATE Dbghelp) +endif() + if(WIN32) add_custom_command(TARGET geopro_desktop POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different @@ -64,4 +103,19 @@ if(WIN32) "$/../plugins/${_pl}" "$/${_pl}") endforeach() + + # ── Qt WebEngine 运行期部署(地图页签需)────────────────────────────────── + # WebEngine 子进程 QtWebEngineProcess.exe(Chromium 渲染进程宿主)须与 exe 同目录或在 PATH; + # 其依赖 resources/*.pak + icudtl.dat + v8 快照,以及 qtwebengine_locales/*.pak。dev-build 不跑 + # windeployqt,故显式拷贝。WebEngine DLL(Qt6WebEngineCore/Widgets/Quick、WebChannel、Positioning、 + # Qml、Quick、QuickWidgets 等)由上方 TARGET_RUNTIME_DLLS 步随链接依赖自动拷贝。 + set(_qt_bin "$") + add_custom_command(TARGET geopro_desktop POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${_qt_bin}/QtWebEngineProcess.exe" "$" + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${_qt_bin}/../resources" "$/resources" + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${_qt_bin}/../translations/qtwebengine_locales" + "$/qtwebengine_locales") endif() diff --git a/src/app/Logging.cpp b/src/app/Logging.cpp new file mode 100644 index 0000000..4b2e2e5 --- /dev/null +++ b/src/app/Logging.cpp @@ -0,0 +1,243 @@ +#include "Logging.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifdef Q_OS_WIN +// clang-format off +#include +#include // MiniDumpWriteDump(链接 Dbghelp) +// clang-format on +#endif + +namespace geopro::app { +namespace { + +QFile g_logFile; +QMutex g_mutex; +QString g_logDir; +constexpr int kRetentionDays = 14; // 旧日志/dump 保留天数 + +const char* levelStr(QtMsgType t) { + switch (t) { + case QtDebugMsg: return "DEBUG"; + case QtInfoMsg: return "INFO"; + case QtWarningMsg: return "WARN"; + case QtCriticalMsg: return "ERROR"; + case QtFatalMsg: return "FATAL"; + } + return "INFO"; +} + +// 线程安全写一行(落盘 + 同步到 stderr 便于开发期观察)。 +void writeLine(const QString& line) { + const QByteArray utf8 = line.toUtf8(); + QMutexLocker lock(&g_mutex); + if (g_logFile.isOpen()) { + g_logFile.write(utf8); + g_logFile.write("\n", 1); + g_logFile.flush(); + } + std::fwrite(utf8.constData(), 1, static_cast(utf8.size()), stderr); + std::fputc('\n', stderr); +} + +void messageHandler(QtMsgType type, const QMessageLogContext& ctx, const QString& msg) { + const QString ts = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz")); + QString line = QStringLiteral("%1 [%2] %3").arg(ts, QString::fromLatin1(levelStr(type)), msg); + if (ctx.file && type >= QtWarningMsg) // 警告及以上带源码定位,便于排查 + line += QStringLiteral(" (%1:%2)").arg(QString::fromUtf8(ctx.file)).arg(ctx.line); + writeLine(line); + if (type == QtFatalMsg) { + std::abort(); // qFatal 语义:记录后终止(触发崩溃捕获 → dump) + } +} + +void pruneOldFiles(const QString& dir) { + const QDateTime cutoff = QDateTime::currentDateTime().addDays(-kRetentionDays); + const QFileInfoList files = + QDir(dir).entryInfoList({QStringLiteral("geopro_*.log"), QStringLiteral("crash_*.dmp")}, + QDir::Files); + for (const QFileInfo& fi : files) + if (fi.lastModified() < cutoff) QFile::remove(fi.absoluteFilePath()); +} + +#ifdef Q_OS_WIN +// 崩溃时(写完 dump 后)追加一行摘要。直接写已打开的 g_logFile(进程将终止,不抢 g_mutex — +// 否则崩溃发生在持锁线程时会死锁;另开第二句柄又会因独占共享冲突失败)。best-effort。 +void appendCrashLine(const QString& line) { + const QByteArray utf8 = line.toUtf8(); + if (g_logFile.isOpen()) { + g_logFile.write(utf8); + g_logFile.write("\n", 1); + g_logFile.flush(); + } + std::fwrite(utf8.constData(), 1, static_cast(utf8.size()), stderr); + std::fputc('\n', stderr); +} + +LONG WINAPI crashFilter(EXCEPTION_POINTERS* info) { + const DWORD code = (info && info->ExceptionRecord) ? info->ExceptionRecord->ExceptionCode : 0; + const void* addr = (info && info->ExceptionRecord) ? info->ExceptionRecord->ExceptionAddress : nullptr; + const QString ts = QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_HHmmss_zzz")); + const QString dumpPath = g_logDir + QStringLiteral("/crash_") + ts + QStringLiteral(".dmp"); + + bool dumped = false; + HANDLE hFile = CreateFileW(reinterpret_cast(dumpPath.utf16()), GENERIC_WRITE, 0, + nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (hFile != INVALID_HANDLE_VALUE) { + MINIDUMP_EXCEPTION_INFORMATION mei{}; + mei.ThreadId = GetCurrentThreadId(); + mei.ExceptionPointers = info; + mei.ClientPointers = FALSE; + const MINIDUMP_TYPE flags = static_cast( + MiniDumpWithDataSegs | MiniDumpWithThreadInfo | MiniDumpWithHandleData); + dumped = MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, flags, + info ? &mei : nullptr, nullptr, nullptr); + CloseHandle(hFile); + } + appendCrashLine(QStringLiteral("%1 [FATAL] 崩溃 code=0x%2 addr=0x%3 dump=%4") + .arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz"))) + .arg(static_cast(code), 0, 16) + .arg(reinterpret_cast(addr), 0, 16) + .arg(dumped ? dumpPath : QStringLiteral("写入失败"))); + return EXCEPTION_EXECUTE_HANDLER; // 记录后终止进程 +} +#endif // Q_OS_WIN + +#ifdef Q_OS_WIN +// 向量化异常处理器:在 C++ 异常(0xE06D7363)**抛出瞬间**(栈未展开、进程健康、符号匹配) +// 捕获并符号化调用栈写日志。即使异常随后被 try/catch(如顶层护栏)吞掉,也已留下抛点堆栈。 +constexpr DWORD kCppExceptionCode = 0xE06D7363; +thread_local bool g_inVeh = false; // 防符号化过程自身再抛异常导致的重入 + +LONG WINAPI throwStackVeh(EXCEPTION_POINTERS* info) { + if (!info || !info->ExceptionRecord) return EXCEPTION_CONTINUE_SEARCH; + if (info->ExceptionRecord->ExceptionCode != kCppExceptionCode) return EXCEPTION_CONTINUE_SEARCH; + if (g_inVeh) return EXCEPTION_CONTINUE_SEARCH; + g_inVeh = true; + + void* frames[32]; + const USHORT n = CaptureStackBackTrace(1, 32, frames, nullptr); + const HANDLE proc = GetCurrentProcess(); + SymRefreshModuleList(proc); // 确保已加载模块(含本 exe 的 PDB)在符号表中 + alignas(SYMBOL_INFOW) char symBuf[sizeof(SYMBOL_INFOW) + 1024] = {}; + auto* sym = reinterpret_cast(symBuf); + sym->SizeOfStruct = sizeof(SYMBOL_INFOW); + sym->MaxNameLen = 1024 / sizeof(wchar_t) - 1; + appendCrashLine(QStringLiteral("%1 [THROW] C++ 异常抛出,调用栈:") + .arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz")))); + for (USHORT i = 0; i < n; ++i) { + const DWORD64 addr = reinterpret_cast(frames[i]); + // 模块名 + RVA:总是可得;即使符号未解析,也能离线用匹配 PDB 还原。 + QString modRva; + HMODULE mod = nullptr; + if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(addr), &mod) && + mod) { + wchar_t mpath[MAX_PATH] = {}; + GetModuleFileNameW(mod, mpath, MAX_PATH); + QString base = QString::fromWCharArray(mpath); + base = base.mid(base.lastIndexOf('\\') + 1); + modRva = QStringLiteral("%1+0x%2").arg(base).arg(addr - reinterpret_cast(mod), 0, 16); + } else { + modRva = QStringLiteral("0x%1").arg(addr, 0, 16); + } + // 符号名(宽字符:UNICODE 下 SymFromAddrW + WCHAR Name)。 + QString fn; + DWORD64 symDisp = 0; + if (SymFromAddrW(proc, addr, &symDisp, sym)) + fn = QStringLiteral(" %1+0x%2").arg(QString::fromWCharArray(sym->Name)).arg(symDisp, 0, 16); + // 文件:行。 + QString loc; + DWORD lineDisp = 0; + IMAGEHLP_LINEW64 line = {}; + line.SizeOfStruct = sizeof(IMAGEHLP_LINEW64); + if (SymGetLineFromAddrW64(proc, addr, &lineDisp, &line)) + loc = QStringLiteral(" (%1:%2)").arg(QString::fromWCharArray(line.FileName)).arg(line.LineNumber); + appendCrashLine(QStringLiteral(" #%1 %2%3%4").arg(i).arg(modRva).arg(fn).arg(loc)); + } + g_inVeh = false; + return EXCEPTION_CONTINUE_SEARCH; // 不处理,交回正常流程(顶层护栏 try/catch 仍会接住) +} +#endif // Q_OS_WIN + +void installCrashHandlers() { +#ifdef Q_OS_WIN + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); // 抑制系统崩溃弹窗 + SetUnhandledExceptionFilter(crashFilter); + SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME | SYMOPT_FAIL_CRITICAL_ERRORS); + // 显式把 exe 目录作为符号搜索路径(geopro_desktop.pdb 与 exe 同目录、匹配);立即(非 deferred)加载。 + const QByteArray searchPath = QCoreApplication::applicationDirPath().toLocal8Bit(); + if (!SymInitialize(GetCurrentProcess(), searchPath.constData(), TRUE)) + std::fprintf(stderr, "[Logging] SymInitialize 失败 err=%lu\n", GetLastError()); + // 显式加载本 exe 的符号(invade 偶尔不加载主模块的私有符号 → 内部函数无名)。 + wchar_t selfPath[MAX_PATH] = {}; + GetModuleFileNameW(nullptr, selfPath, MAX_PATH); + const DWORD64 selfBase = + SymLoadModuleExW(GetCurrentProcess(), nullptr, selfPath, nullptr, + reinterpret_cast(GetModuleHandleW(nullptr)), 0, nullptr, 0); + if (selfBase == 0 && GetLastError() != ERROR_SUCCESS) + std::fprintf(stderr, "[Logging] SymLoadModuleExW 失败 err=%lu\n", GetLastError()); + AddVectoredExceptionHandler(1, throwStackVeh); // first=1:先于 SEH 链,抛出瞬间捕获 +#endif + // 未捕获 C++ 异常(跨事件循环逃逸)→ 记录后终止。SEH 过滤器通常已先写 dump。 + std::set_terminate([] { + QString what = QStringLiteral("(无异常信息)"); + if (std::exception_ptr e = std::current_exception()) { + try { + std::rethrow_exception(e); + } catch (const std::exception& ex) { + what = QString::fromUtf8(ex.what()); + } catch (...) { + what = QStringLiteral("(非 std::exception)"); + } + } + writeLine(QStringLiteral("%1 [FATAL] std::terminate 未捕获异常: %2") + .arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz")), what)); + std::abort(); + }); +} + +} // namespace + +void initLogging() { + const QString base = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); + g_logDir = base + QStringLiteral("/logs"); + QDir().mkpath(g_logDir); + pruneOldFiles(g_logDir); + + const QString fname = QStringLiteral("geopro_%1.log") + .arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd"))); + g_logFile.setFileName(g_logDir + QStringLiteral("/") + fname); + const bool opened = g_logFile.open(QIODevice::Append | QIODevice::Text); + if (!opened) { // 打不开(权限等)则仅写 stderr(writeLine 已容错) + std::fprintf(stderr, "[Logging] 无法打开日志文件: %s\n", qUtf8Printable(g_logFile.fileName())); + } else if (g_logFile.size() == 0) { + g_logFile.write("\xEF\xBB\xBF", 3); // 新建文件写 UTF-8 BOM,便于中文 Windows 记事本正确识别编码 + } + + qInstallMessageHandler(messageHandler); + installCrashHandlers(); + + qInfo("=== Geopro 启动 v%s | 日志目录 %s ===", + qUtf8Printable(QCoreApplication::applicationVersion().isEmpty() + ? QStringLiteral("dev") + : QCoreApplication::applicationVersion()), + qUtf8Printable(g_logDir)); +} + +QString logDirectory() { return g_logDir; } + +} // namespace geopro::app diff --git a/src/app/Logging.hpp b/src/app/Logging.hpp new file mode 100644 index 0000000..f9b45a9 --- /dev/null +++ b/src/app/Logging.hpp @@ -0,0 +1,18 @@ +#pragma once +#include + +namespace geopro::app { + +// 初始化全局日志与崩溃捕获。需在 QApplication 构造、setOrganizationName/setApplicationName +// 之后调用一次(依赖 AppLocalDataLocation 定位日志目录)。 +// - 安装 qInstallMessageHandler:全 App 的 qDebug/qInfo/qWarning/qCritical/qFatal 写入 +// 带时间戳/级别的滚动日志文件(%LOCALAPPDATA%///logs/geopro_YYYYMMDD.log)。 +// - 安装崩溃捕获:未处理 SEH 异常(段错误等)+ std::terminate(未捕获 C++ 异常)→ +// 记录异常码/地址 + flush 日志,并在 Windows 上用 DbgHelp 写 crash_*.dmp(可 VS/WinDbg 事后分析)。 +// - 启动时清理 14 天前的旧日志/dump。 +void initLogging(); + +// 当前日志目录绝对路径(供 UI「打开日志目录」等使用,可选)。 +QString logDirectory(); + +} // namespace geopro::app diff --git a/src/app/PanelHeader.cpp b/src/app/PanelHeader.cpp index 61e1021..8b2fea7 100644 --- a/src/app/PanelHeader.cpp +++ b/src/app/PanelHeader.cpp @@ -125,6 +125,7 @@ TabbedPanel buildTabbedPanel(const QVector& tabs, const QVector
badges; + QButtonGroup* tabGroup = nullptr; }; // 构建带 Tab 表头的面板(首个 Tab 默认激活;点击 Tab 切换下方堆叠内容)。 diff --git a/src/app/ProjectListDialog.cpp b/src/app/ProjectListDialog.cpp index 950df35..0324196 100644 --- a/src/app/ProjectListDialog.cpp +++ b/src/app/ProjectListDialog.cpp @@ -15,6 +15,8 @@ #include #include "Theme.hpp" +#include "api/NavLoads.hpp" +#include "api/NavRequest.hpp" namespace geopro::app { @@ -37,7 +39,7 @@ QColor statusColor(int s) { } } // namespace -ProjectListDialog::ProjectListDialog(data::IProjectRepository& repo, QWidget* parent) +ProjectListDialog::ProjectListDialog(data::IAsyncProjectRepository& repo, QWidget* parent) : QDialog(parent), repo_(repo) { setWindowTitle(QStringLiteral("全部项目")); resize(980, 560); @@ -125,57 +127,85 @@ ProjectListDialog::ProjectListDialog(data::IProjectRepository& repo, QWidget* pa query(); } +ProjectListDialog::~ProjectListDialog() { + // 退出契约:模态 exec 关窗后本对话框析构 → abort 在飞请求,防回调打到已析构窗口。 + if (typesReq_) typesReq_->abort(); + if (queryReq_) queryReq_->abort(); +} + void ProjectListDialog::fillTypeFilter() { typeCombo_->addItem(QStringLiteral("全部类型"), QString()); - const auto r = repo_.listProjectTypes(); - if (!r.ok) return; - for (const auto& t : r.value) - typeCombo_->addItem(QString::fromStdString(t.name), QString::fromStdString(t.id)); + // abort-and-replace:最新一次过滤填充为准。 + if (typesReq_) typesReq_->abort(); + auto* r = repo_.listProjectTypesAsync(); + typesReq_ = r; + QObject::connect(r, &data::NavRequest::done, this, [this, r](const QVariant& v) { + if (r != typesReq_) return; // 身份比对:丢弃迟到/被替换信号 + typesReq_.clear(); + const auto types = qvariant_cast>(v); + for (const auto& t : types) + typeCombo_->addItem(QString::fromStdString(t.name), QString::fromStdString(t.id)); + }); + QObject::connect(r, &data::NavRequest::failed, this, [this, r](const QString&) { + if (r != typesReq_) return; + typesReq_.clear(); + // 失败时仅保留“全部类型”项(与原同步版失败仅 return 一致)。 + }); } void ProjectListDialog::query() { const std::string name = nameEdit_->text().trimmed().toStdString(); const std::string typeId = typeCombo_->currentData().toString().toStdString(); - const auto r = repo_.pageProjects(name, typeId, pageNo_, pageSize_); - if (!r.ok) { + // abort-and-replace:丢弃上一查询,仅最新结果落表。 + if (queryReq_) queryReq_->abort(); + auto* r = repo_.pageProjectsAsync(name, typeId, pageNo_, pageSize_); + queryReq_ = r; + QObject::connect(r, &data::NavRequest::done, this, [this, r](const QVariant& v) { + if (r != queryReq_) return; // 身份比对 + queryReq_.clear(); + const auto page = qvariant_cast(v); + total_ = page.total; + const auto& rows = page.rows; + table_->setRowCount(static_cast(rows.size())); + for (int i = 0; i < static_cast(rows.size()); ++i) { + const auto& p = rows[i]; + auto set = [&](int col, const QString& text) { + table_->setItem(i, col, new QTableWidgetItem(text)); + }; + set(0, QString::number((pageNo_ - 1) * pageSize_ + i + 1)); + auto* nameItem = new QTableWidgetItem(QString::fromStdString(p.name)); + nameItem->setData(Qt::UserRole, QString::fromStdString(p.id)); + nameItem->setForeground(tokenColor("accent/primary")); + table_->setItem(i, 1, nameItem); + set(2, QString::fromStdString(p.code)); + // 状态列语义着色:颜色承载“未开始/进行中”分类,进行中加粗强调(不只靠颜色)。 + auto* statusItem = new QTableWidgetItem(statusText(p.status)); + statusItem->setForeground(statusColor(p.status)); + if (p.status == 2) { + QFont f = statusItem->font(); + f.setBold(true); + statusItem->setFont(f); + } + table_->setItem(i, 3, statusItem); + set(4, QString::fromStdString(p.typeName)); + set(5, QString::fromStdString(p.ownerCompany)); + set(6, QString::fromStdString(p.responsiblePerson)); + set(7, QString::fromStdString(p.createTime)); + } + const int pages = total_ > 0 ? (total_ + pageSize_ - 1) / pageSize_ : 1; + pageLabel_->setText( + QStringLiteral("共 %1 条 第 %2 / %3 页").arg(total_).arg(pageNo_).arg(pages)); + prevBtn_->setEnabled(pageNo_ > 1); + nextBtn_->setEnabled(pageNo_ < pages); + }); + QObject::connect(r, &data::NavRequest::failed, this, [this, r](const QString& msg) { + if (r != queryReq_) return; + queryReq_.clear(); table_->setRowCount(0); - pageLabel_->setText(QStringLiteral("加载失败:%1").arg(QString::fromStdString(r.error))); + pageLabel_->setText(QStringLiteral("加载失败:%1").arg(msg)); prevBtn_->setEnabled(false); nextBtn_->setEnabled(false); - return; - } - total_ = r.value.total; - const auto& rows = r.value.rows; - table_->setRowCount(static_cast(rows.size())); - for (int i = 0; i < static_cast(rows.size()); ++i) { - const auto& p = rows[i]; - auto set = [&](int col, const QString& text) { - table_->setItem(i, col, new QTableWidgetItem(text)); - }; - set(0, QString::number((pageNo_ - 1) * pageSize_ + i + 1)); - auto* nameItem = new QTableWidgetItem(QString::fromStdString(p.name)); - nameItem->setData(Qt::UserRole, QString::fromStdString(p.id)); - nameItem->setForeground(tokenColor("accent/primary")); - table_->setItem(i, 1, nameItem); - set(2, QString::fromStdString(p.code)); - // 状态列语义着色:颜色承载“未开始/进行中”分类,进行中加粗强调(不只靠颜色)。 - auto* statusItem = new QTableWidgetItem(statusText(p.status)); - statusItem->setForeground(statusColor(p.status)); - if (p.status == 2) { - QFont f = statusItem->font(); - f.setBold(true); - statusItem->setFont(f); - } - table_->setItem(i, 3, statusItem); - set(4, QString::fromStdString(p.typeName)); - set(5, QString::fromStdString(p.ownerCompany)); - set(6, QString::fromStdString(p.responsiblePerson)); - set(7, QString::fromStdString(p.createTime)); - } - const int pages = total_ > 0 ? (total_ + pageSize_ - 1) / pageSize_ : 1; - pageLabel_->setText(QStringLiteral("共 %1 条 第 %2 / %3 页").arg(total_).arg(pageNo_).arg(pages)); - prevBtn_->setEnabled(pageNo_ > 1); - nextBtn_->setEnabled(pageNo_ < pages); + }); } } // namespace geopro::app diff --git a/src/app/ProjectListDialog.hpp b/src/app/ProjectListDialog.hpp index c0675a8..eda588f 100644 --- a/src/app/ProjectListDialog.hpp +++ b/src/app/ProjectListDialog.hpp @@ -1,7 +1,8 @@ #pragma once #include +#include -#include "repo/IProjectRepository.hpp" +#include "repo/IAsyncProjectRepository.hpp" class QLineEdit; class QComboBox; @@ -9,13 +10,16 @@ class QTableWidget; class QLabel; class QPushButton; +namespace geopro::data { class NavRequest; } + namespace geopro::app { // 项目列表弹窗:名称/类型过滤 + 分页表格;点项目名 → 切换项目并关闭。 class ProjectListDialog : public QDialog { Q_OBJECT public: - explicit ProjectListDialog(data::IProjectRepository& repo, QWidget* parent = nullptr); + explicit ProjectListDialog(data::IAsyncProjectRepository& repo, QWidget* parent = nullptr); + ~ProjectListDialog() override; signals: void projectChosen(const QString& projectId, const QString& projectName); @@ -24,7 +28,9 @@ private: void query(); void fillTypeFilter(); - data::IProjectRepository& repo_; + data::IAsyncProjectRepository& repo_; + QPointer typesReq_; + QPointer queryReq_; QLineEdit* nameEdit_ = nullptr; QComboBox* typeCombo_ = nullptr; QTableWidget* table_ = nullptr; diff --git a/src/app/login/LoginWindow.cpp b/src/app/login/LoginWindow.cpp index f042b27..f2294f3 100644 --- a/src/app/login/LoginWindow.cpp +++ b/src/app/login/LoginWindow.cpp @@ -17,6 +17,7 @@ #include #include +#include "AuthLoads.hpp" #include "AuthService.hpp" #include "Theme.hpp" @@ -219,20 +220,36 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) userEdit_->setFocus(); // 焦点落在第一个待填字段 } +LoginWindow::~LoginWindow() +{ + // 退出契约:窗口析构时 abort 在飞句柄,防回调打到已析构窗口(spec §5.0)。 + if (captchaLoad_) captchaLoad_->abort(); + if (loginLoad_) loginLoad_->abort(); +} + void LoginWindow::refreshCaptcha() { codeEdit_->clear(); - try { - const auto cap = auth_.fetchCaptcha(); - codeId_ = cap.codeId; - captchaLabel_->setPixmap(renderCaptchaPixmap(cap.code)); - } catch (const std::exception& e) { - showError(QStringLiteral("获取验证码失败:%1").arg(QString::fromUtf8(e.what()))); + if (captchaLoad_) captchaLoad_->abort(); // 取消上一张在飞请求 + refreshBtn_->setEnabled(false); + + auto* l = auth_.fetchCaptchaAsync(); + captchaLoad_ = l; + connect(l, &geopro::net::CaptchaLoad::done, this, + [this, l](const geopro::net::AuthService::Captcha& cap) { + if (l != captchaLoad_) return; // 身份比对:仅处理最新请求 + captchaLoad_.clear(); + codeId_ = cap.codeId; + captchaLabel_->setPixmap(renderCaptchaPixmap(cap.code)); + refreshBtn_->setEnabled(true); + }); + connect(l, &geopro::net::CaptchaLoad::failed, this, [this, l](const QString& msg) { + if (l != captchaLoad_) return; + captchaLoad_.clear(); + showError(QStringLiteral("获取验证码失败:%1").arg(msg)); captchaLabel_->setText(QStringLiteral("加载失败")); - } catch (...) { - showError(QStringLiteral("获取验证码失败")); - captchaLabel_->setText(QStringLiteral("加载失败")); - } + refreshBtn_->setEnabled(true); + }); } void LoginWindow::attemptLogin() @@ -249,31 +266,25 @@ void LoginWindow::attemptLogin() errorLabel_->clear(); loginBtn_->setEnabled(false); const QString origText = loginBtn_->text(); - loginBtn_->setText(QStringLiteral("登录中...")); - loginBtn_->repaint(); // 同步阻塞前刷新按钮文案 + loginBtn_->setText(QStringLiteral("登录中...")); // 异步不冻 UI,无需 repaint hack - geopro::net::AuthService::LoginResult result; - try { - result = auth_.login(user, pwd, code, codeId_); - } catch (const std::exception& e) { - result.ok = false; - result.error = QStringLiteral("登录异常:%1").arg(QString::fromUtf8(e.what())); - } catch (...) { - result.ok = false; - result.error = QStringLiteral("登录发生未知错误"); - } - - loginBtn_->setText(origText); - loginBtn_->setEnabled(true); - - if (result.ok) { - token_ = result.token; + if (loginLoad_) loginLoad_->abort(); // 取消上一次在飞登录 + auto* l = auth_.loginAsync(user, pwd, code, codeId_); + loginLoad_ = l; + connect(l, &geopro::net::LoginLoad::done, this, [this, l](const QString& token) { + if (l != loginLoad_) return; // 身份比对 + loginLoad_.clear(); + token_ = token; accept(); - return; - } - - showError(result.error.isEmpty() ? QStringLiteral("登录失败") : result.error); - refreshCaptcha(); // 失败刷新验证码 + }); + connect(l, &geopro::net::LoginLoad::failed, this, [this, l, origText](const QString& msg) { + if (l != loginLoad_) return; + loginLoad_.clear(); + loginBtn_->setText(origText); + loginBtn_->setEnabled(true); + showError(msg.isEmpty() ? QStringLiteral("登录失败") : msg); + refreshCaptcha(); // 失败刷新验证码 + }); } bool LoginWindow::remember() const diff --git a/src/app/login/LoginWindow.hpp b/src/app/login/LoginWindow.hpp index 7d5ce42..510e249 100644 --- a/src/app/login/LoginWindow.hpp +++ b/src/app/login/LoginWindow.hpp @@ -4,6 +4,7 @@ // 成功后 accept() 并经 token() 暴露 accessToken 给主流程注入 ApiClient。 #include +#include #include class QCheckBox; @@ -13,6 +14,8 @@ class QPushButton; namespace geopro::net { class AuthService; +class CaptchaLoad; +class LoginLoad; } namespace geopro::app { @@ -22,6 +25,7 @@ class LoginWindow : public QDialog { public: explicit LoginWindow(geopro::net::AuthService& auth, QWidget* parent = nullptr); + ~LoginWindow() override; // 登录成功后的 accessToken(形如 "Geomative ");未登录为空。 QString token() const { return token_; } @@ -31,12 +35,14 @@ public: private: void refreshCaptcha(); // 拉新验证码并重绘图片 - void attemptLogin(); // 校验输入并发起阻塞登录 + void attemptLogin(); // 校验输入并发起异步登录 void showError(const QString& msg); geopro::net::AuthService& auth_; QString token_; QString codeId_; + QPointer captchaLoad_; + QPointer loginLoad_; QLineEdit* userEdit_ = nullptr; QLineEdit* pwdEdit_ = nullptr; diff --git a/src/app/main.cpp b/src/app/main.cpp index bbafbf4..e602d49 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -56,9 +57,12 @@ #include #include #include +#include #include #include +#include + #include #include #include @@ -73,6 +77,7 @@ #include "AuthService.hpp" #include "Credential.hpp" #include "Glyphs.hpp" +#include "Logging.hpp" #include "PanelHeader.hpp" #include "Theme.hpp" #include "SettingsDialog.hpp" @@ -80,10 +85,18 @@ #include "CentralScene.hpp" #include "ProjectListDialog.hpp" #include "WorkbenchNavController.hpp" +#include "DatasetDetailController.hpp" +#include "panels/chart/ErtInversionStrategy.hpp" +#include "panels/chart/MeasurementStrategy.hpp" +#include "panels/chart/GrMeasurementStrategy.hpp" +#include "panels/chart/TrajectoryStrategy.hpp" +#include "panels/chart/GridStrategy.hpp" #include "api/ApiProjectRepository.hpp" +#include "api/ApiDatasetRepository.hpp" #include "panels/ObjectTreePanel.hpp" #include "login/LoginWindow.hpp" #include "panels/DatasetListPanel.hpp" +#include "panels/DatasetDetailPanel.hpp" #include "panels/DynamicFormView.hpp" #include "panels/ObjectExceptionPanel.hpp" @@ -155,44 +168,6 @@ private: QWidget* host_; }; -// 相机补间 + actor 淡入:从 from 位姿平滑过渡到 to 位姿,同时 actors 透明度 0→1。 -// vtkCameraInterpolator 两关键帧线性插值(缓动交给 QEasingCurve),单条 QVariantAnimation -// 逐帧驱动并 Render;结束回调锁定到目标态(防插值末值误差/残留半透明)。 -// 渐进增强:动效只是过渡,最终一帧永远是正确的目标态,故即使观感不佳也不破坏功能。 -void animateReveal(vtkRenderer* renderer, vtkGenericOpenGLRenderWindow* rw, - vtkSmartPointer fromCam, vtkSmartPointer toCam, - std::vector> actors, int durationMs, QObject* owner) -{ - auto interp = vtkSmartPointer::New(); - interp->SetInterpolationTypeToLinear(); - interp->AddCamera(0.0, fromCam); - interp->AddCamera(1.0, toCam); - - auto* anim = new QVariantAnimation(owner); - anim->setDuration(durationMs); - anim->setStartValue(0.0); - anim->setEndValue(1.0); - anim->setEasingCurve(QEasingCurve::OutCubic); - QObject::connect(anim, &QVariantAnimation::valueChanged, owner, - [interp, renderer, rw, actors](const QVariant& v) { - const double t = v.toDouble(); - interp->InterpolateCamera(t, renderer->GetActiveCamera()); - for (const auto& a : actors) - if (a) a->GetProperty()->SetOpacity(t); - renderer->ResetCameraClippingRange(); - rw->Render(); - }); - QObject::connect(anim, &QVariantAnimation::finished, owner, - [renderer, rw, actors, toCam]() { - renderer->GetActiveCamera()->DeepCopy(toCam); - for (const auto& a : actors) - if (a) a->GetProperty()->SetOpacity(1.0); - renderer->ResetCameraClippingRange(); - rw->Render(); - }); - anim->start(QAbstractAnimation::DeleteWhenStopped); -} - // 读取 RSA 公钥 PEM 全文(登录时密码加密用)。读不到返回空串,登录将报错。 std::string readPem(const std::string& path) { @@ -215,12 +190,6 @@ double median(std::vector v) // 当前中央视图(默认二维地图)。二维地图=测线红线俯视;三维视图=断面墙。 using geopro::app::ViewMode; -// 数据详情显示内容(默认网格数据)。网格数据=#18 banded;原数据=#17 散点(对齐原型命名)。 -enum class DetailMode { Section18, Scatter17 }; - -// #17 散点屏幕像素方块边长。 -constexpr float kScatterPointSize = 4.0F; - // 纵向夸张倍数(Z 基准统一,M-3):全项目共用同一倍数,使 帘面(z) / 体素 / 切片 / // 数据详情剖面(y) / 地形(relief) 的纵向比例一致——避免「剖面×1.5、帘面×3」不一致。 // 单一可调常量:要整体调纵向观感改这一处即可。 @@ -233,8 +202,9 @@ constexpr const char* kWgs84 = "EPSG:4326"; // 在给定 QMainWindow 上构建 M1 工作台。 // repo 生命周期须覆盖到事件循环结束(由调用方保证)。 void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& repo, - geopro::data::IProjectRepository& projectRepo, - geopro::controller::WorkbenchNavController& nav) + geopro::data::IAsyncProjectRepository& projectRepo, + geopro::controller::WorkbenchNavController& nav, + geopro::controller::DatasetDetailController& detailCtrl) { // ── 世界系:启动取一次 grid1 的 lat/lon,用中位数作 GeoLocalFrame 原点 ── // 全项目共享(shared_ptr 持有):所有帘面用同一 frame 投影,保证多条测线空间配准。 @@ -322,23 +292,6 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re centerLayout->setContentsMargins(0, 0, 0, 0); centerLayout->setSpacing(0); - // 分段工具条按钮样式(QToolButton + 主题化 QSS):选中=强调色文字 + 强调色下划线,明暗都清晰。 - // ElaToolButton 选中只画极淡 BasicHover、且不可经 QSS 改,故这类需清晰选中态的用 QToolButton。 - const QString kBarBtnQss = - QStringLiteral( - "QToolButton{ border:none; border-radius:6px; padding:6px 12px; color:{{text/primary}};" - " font-size:%1px; }" - "QToolButton:hover{ background:{{bg/hover}}; }" - "QToolButton:checked{ color:{{accent/primary}}; font-weight:%2;" - " border-bottom:2px solid {{accent/primary}}; }" - "QToolButton#dataTab{ border:none; border-radius:0; background:transparent;" - " border-bottom:2px solid transparent; color:{{text/secondary}}; padding:8px 8px; }" - "QToolButton#dataTab:hover{ color:{{text/primary}}; background:transparent; }" - "QToolButton#dataTab:checked{ color:{{accent/primary}}; font-weight:%2;" - " border-bottom:2px solid {{accent/primary}}; }") - .arg(geopro::app::scaledPx(geopro::app::type::kBody)) - .arg(geopro::app::type::kWeightSemibold); - // 「二维地图/三维视图」分段切换表头:与「异常/属性」面板表头同款(42px 表头底 + 强调色下划线页签)。 auto seg = geopro::app::buildSegmentedHeader( {QStringLiteral("二维地图"), QStringLiteral("三维视图")}, @@ -448,77 +401,16 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re vtkDock->setWidget(centerWidget); auto* centerDockArea = dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock); - // ── 下方「数据详情」dock:独立 QVTK 小视图(独立 renderer/renderWindow)── - // 单击 DS → 显示该数据集平面反演剖面(#18 banded,平躺俯视正交)。 - auto* detailWidget = new QVTKOpenGLStereoWidget(); - vtkNew detailRenderWindow; - vtkNew detailRenderer; - { - double r, g, b; - geopro::app::vtkBackground(r, g, b); // 背景随主题 - detailRenderer->SetBackground(r, g, b); - } - detailWidget->setRenderWindow(detailRenderWindow); - detailRenderWindow->AddRenderer(detailRenderer); - vtkRenderer* detailRendererPtr = detailRenderer.Get(); - vtkGenericOpenGLRenderWindow* detailRenderWindowPtr = detailRenderWindow.Get(); - // 注:VTK 背景随主题切换的连接放在 rebuildCentral/rebuildDetail 定义之后(直接重跑它们, - // 走完整渲染路径必重绘,比手动 SetBackground+Render 稳)。 - - // 数据详情容器:顶部「反演剖面/原数据」工具条 + 下方 QVTK 小视图。 - auto* detailContainer = new QWidget(); - auto* detailLayout = new QVBoxLayout(detailContainer); - detailLayout->setContentsMargins(0, 0, 0, 0); - detailLayout->setSpacing(0); - - // 工具条对齐原型:「原数据 | 网格数据」互斥 +「显示异常/电极/等值线」开关。 - // QToolButton + 主题化 QSS(选中=强调色文字+下划线,明暗都清晰)。 - auto* detailToolBar = new QWidget(); - auto* detailBarLay = new QHBoxLayout(detailToolBar); - detailBarLay->setContentsMargins(8, 6, 8, 6); - detailBarLay->setSpacing(6); - auto makeBarBtn = [detailToolBar](const QString& text, bool checkable) { - auto* b = new QToolButton(detailToolBar); - b->setText(text); - b->setCheckable(checkable); - return b; - }; - auto* detailGroup = new QButtonGroup(detailToolBar); - detailGroup->setExclusive(true); - auto* actScatter = makeBarBtn(QStringLiteral("原数据"), true); - auto* actSection = makeBarBtn(QStringLiteral("网格数据"), true); - actScatter->setObjectName(QStringLiteral("dataTab")); - actSection->setObjectName(QStringLiteral("dataTab")); - detailGroup->addButton(actScatter); - detailGroup->addButton(actSection); - detailBarLay->addWidget(actScatter); - detailBarLay->addWidget(actSection); - actSection->setChecked(true); // 默认网格数据 (#18) - auto* barSep = new QFrame(detailToolBar); - barSep->setFrameShape(QFrame::VLine); - barSep->setObjectName(QStringLiteral("topDivider")); - detailBarLay->addSpacing(4); - detailBarLay->addWidget(barSep); - detailBarLay->addSpacing(4); - auto* actShowAnomaly = makeBarBtn(QStringLiteral("显示异常"), true); - actShowAnomaly->setChecked(true); // 默认显示异常(对齐原型 ☑显示异常) - auto* actShowElectrodes = makeBarBtn(QStringLiteral("显示电极"), true); - actShowElectrodes->setChecked(true); // 默认显示电极 ▼(对齐原型) - auto* actShowContour = makeBarBtn(QStringLiteral("显示等值线"), true); - actShowContour->setChecked(true); // 默认显示等值线(对齐原型) - detailBarLay->addWidget(actShowAnomaly); - detailBarLay->addWidget(actShowElectrodes); - detailBarLay->addWidget(actShowContour); - detailBarLay->addStretch(); - geopro::app::applyTokenizedStyleSheet(detailToolBar, kBarBtnQss); - detailLayout->addWidget(detailToolBar); - detailLayout->addWidget(detailWidget, 1); - + // ── 下方「数据详情」dock:平面图表多 Tab 面板(QGraphicsView,VTK 仅算几何)── + // 单击数据集 → 聚焦已开页;双击 → 新建/聚焦页(真实反演剖面/散点/异常/色阶)。 + auto* detailPanel = new geopro::app::DatasetDetailPanel(); auto* detailDock = new ads::CDockWidget(QStringLiteral("数据详情")); + // ForceNoScrollArea:禁止 ADS 默认把整块内容(含标题栏/页签栏)包进 QScrollArea。 + // 否则内容最小尺寸超过 dock 时,整个面板(标题+页签+图)一起滚动。改为内容自适应填充; + // 需要时由内层(图表内容区)自行滚动,标题/页签固定。 detailDock->setWidget(wrapWithHeader( - geopro::app::Glyph::Detail, QStringLiteral("数据详情"), detailContainer, - {{geopro::app::Glyph::Collapse, QStringLiteral("折叠")}, - {geopro::app::Glyph::Download, QStringLiteral("导出")}})); + geopro::app::Glyph::Detail, QStringLiteral("数据详情"), detailPanel), + ads::CDockWidget::ForceNoScrollArea); // 放在中央视图下方。 dockManager->addDockWidget(ads::BottomDockWidgetArea, detailDock, centerDockArea); @@ -532,7 +424,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 左下 dock:数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。 auto* datasetTabs = new QTabWidget(); - auto* datasetList = new QListWidget(); + // 数据页签:树形列表(原版 el-table 树——派生数据挂源数据下,按 DsRow.parentId 嵌套)。 + auto* datasetList = new QTreeWidget(); + datasetList->setHeaderHidden(true); + datasetList->setColumnCount(1); + datasetList->setRootIsDecorated(true); // 显展开/折叠箭头 + datasetList->setIndentation(geopro::app::scaledPx(14)); + datasetList->setExpandsOnDoubleClick(false); // 双击=打开详情,不切展开(展开靠箭头) + datasetList->setSelectionBehavior(QAbstractItemView::SelectRows); geopro::app::applyDatasetCardDelegate(datasetList); datasetTabs->addTab(datasetList, QStringLiteral("数据")); auto* fileList = new QListWidget(); @@ -599,146 +498,60 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re *frame, kVerticalExaggeration); }; - // ── 数据详情共享状态 + 重建 ────────────────────────────────────────── - // 当前选中数据集 id(空=未选)与详情显示模式(反演剖面/原数据);切模式或换选中都重建。 - auto currentDsId = std::make_shared(); - auto detailMode = std::make_shared(DetailMode::Section18); - auto showAnomalies = std::make_shared(true); // 默认显示异常(对齐原型) - auto showElectrodes = std::make_shared(true); // 默认显示电极 ▼ - auto showContour = std::make_shared(true); // 默认显示等值线 - auto hiddenAnoms = std::make_shared>(); // 异常列表中被取消勾选(隐藏)的异常下标 - auto prevDsId = std::make_shared(); // 上次渲染的 DS id:判定“切换数据集”以触发揭示过渡 - - // 按当前选中 DS + 详情模式重建下方数据详情(平躺俯视正交,纵向夸张填面板)。 - // 勾选「显示异常/电极/等值线」控制对应叠加(同纵向夸张对齐)。 - // overdrive(A):仅“切换数据集”这一加载时刻播放相机补间 + actor 淡入揭示;模式/叠加层开关 - // 属同一数据集内微调,直接落定不放动画(特殊时刻才特殊,避免每次交互都动的疲劳)。 - auto rebuildDetail = [&repo, detailRendererPtr, detailRenderWindowPtr, detailWidget, currentDsId, - prevDsId, detailMode, showAnomalies, showElectrodes, showContour, - hiddenAnoms]() { - const bool dsChanged = (*currentDsId != *prevDsId); - const bool animate = dsChanged && !prevDsId->isEmpty() && !currentDsId->isEmpty(); - - // 过渡起点:清场景前先快照当前相机位姿。 - auto fromCam = vtkSmartPointer::New(); - fromCam->DeepCopy(detailRendererPtr->GetActiveCamera()); - - detailRendererPtr->RemoveAllViewProps(); - { // 背景随主题 - double r, g, b; - geopro::app::vtkBackground(r, g, b); - detailRendererPtr->SetBackground(r, g, b); - } - if (currentDsId->isEmpty()) { // 未选数据集:清空即可 - *prevDsId = *currentDsId; - detailRenderWindowPtr->Render(); - return; - } - std::vector> added; // 本次加入的 actor,供淡入 - const std::string id = currentDsId->toStdString(); - if (*detailMode == DetailMode::Section18) { - // 网格数据:#18 banded 等值面(+「显示等值线」时叠黑色等值线),纵向夸张 1.5x(沿 y)。 - const auto g = repo.loadGrid(id); - const auto cs = repo.loadColorScale(id); - const auto actors = geopro::render::buildGridContour(g, cs); - if (actors.bands) { - actors.bands->SetScale(1.0, kVerticalExaggeration, 1.0); - detailRendererPtr->AddViewProp(actors.bands); - added.push_back(actors.bands); - } - if (actors.edges && *showContour) { - actors.edges->SetScale(1.0, kVerticalExaggeration, 1.0); - detailRendererPtr->AddViewProp(actors.edges); - added.push_back(actors.edges); - } - // 顶部电极标记 ▼(仅网格数据;同纵向夸张对齐)。 - if (*showElectrodes) { - auto elec = geopro::render::buildElectrodes(g); - if (elec) { - elec->SetScale(1.0, kVerticalExaggeration, 1.0); - detailRendererPtr->AddViewProp(elec); - added.push_back(elec); - } - } - } else { - // 原数据:#17 彩色散点,用散点自带色阶;纵向夸张同剖面以对齐观感。 - const auto s = repo.loadScatter(id); - const auto scs = repo.loadScatterColorScale(id); - auto a = geopro::render::buildScatter(s, scs, kScatterPointSize); - if (a) { - a->SetScale(1.0, kVerticalExaggeration, 1.0); - detailRendererPtr->AddViewProp(a); - added.push_back(a); - } - } - // 异常叠加(与剖面同坐标系/同纵向夸张)。逐异常构建以按列表显隐(下标=原 vector 序)过滤。 - if (*showAnomalies) { - const auto anomalies = repo.loadAnomalies(id); - for (int i = 0; i < static_cast(anomalies.size()); ++i) { - if (hiddenAnoms->count(i)) continue; // 列表中取消勾选→隐藏 - for (auto& act : geopro::render::buildAnomalies({anomalies[i]})) { - act->SetScale(1.0, kVerticalExaggeration, 1.0); - detailRendererPtr->AddViewProp(act); - added.push_back(act); - } - } - } - geopro::render::applyTop2D(detailRendererPtr); - detailRendererPtr->ResetCamera(); - *prevDsId = *currentDsId; - - if (animate) { - // 目标位姿快照 → 相机回退到旧位姿 + actors 透明 → 补间到目标并淡入。 - auto toCam = vtkSmartPointer::New(); - toCam->DeepCopy(detailRendererPtr->GetActiveCamera()); - for (const auto& a : added) a->GetProperty()->SetOpacity(0.0); - detailRendererPtr->GetActiveCamera()->DeepCopy(fromCam); - detailRendererPtr->ResetCameraClippingRange(); - animateReveal(detailRendererPtr, detailRenderWindowPtr, fromCam, toCam, added, 450, - detailWidget); - } else { - detailRenderWindowPtr->Render(); - } - }; - - // ── 单击左下数据列表的采集批次(DS) → 加载数据集动态表单(数据集属性面板)── - QObject::connect(datasetList, &QListWidget::itemClicked, datasetList, - [&nav](QListWidgetItem* item) { - if (item->data(geopro::app::kDsLoadMoreRole).toBool()) { + // ── 单击左下数据列表的采集批次(DS) → 属性表单 + 聚焦详情已开页 ── + QObject::connect(datasetList, &QTreeWidget::itemClicked, datasetList, + [&nav, &detailCtrl](QTreeWidgetItem* item, int) { + if (item->data(0, geopro::app::kDsLoadMoreRole).toBool()) { nav.loadMoreData(); return; } - const QString dsId = item->data(geopro::app::kDsIdRole).toString(); - if (!dsId.isEmpty()) nav.selectDataset(dsId); + const QString dsId = item->data(0, geopro::app::kDsIdRole).toString(); + if (dsId.isEmpty()) return; + nav.selectDataset(dsId); // 属性表单(现状) + detailCtrl.focusDataset(dsId); // 单击=聚焦已开页 }); - // ── 数据详情工具条「反演剖面/原数据」:切模式 → 重建数据详情 ── - QObject::connect(actSection, &QAbstractButton::clicked, detailWidget, - [detailMode, rebuildDetail]() { - *detailMode = DetailMode::Section18; - rebuildDetail(); - }); - QObject::connect(actScatter, &QAbstractButton::clicked, detailWidget, - [detailMode, rebuildDetail]() { - *detailMode = DetailMode::Scatter17; - rebuildDetail(); + // ── 双击 → 打开/聚焦该数据集的详情图表页(拉真实反演剖面/散点/异常/色阶)── + QObject::connect(datasetList, &QTreeWidget::itemDoubleClicked, datasetList, + [&detailCtrl](QTreeWidgetItem* item, int) { + const QString dsId = item->data(0, geopro::app::kDsIdRole).toString(); + const QString ddCode = item->data(0, geopro::app::kDsDdCodeRole).toString(); + const QString dsName = item->data(0, geopro::app::kDsNameRole).toString(); + if (!dsId.isEmpty()) detailCtrl.openDataset(dsId, ddCode, dsName); }); - // ──「显示异常 / 显示电极 / 显示等值线」开关:切换叠加 → 重建数据详情 ── - QObject::connect(actShowAnomaly, &QAbstractButton::toggled, detailWidget, - [showAnomalies, rebuildDetail](bool on) { - *showAnomalies = on; - rebuildDetail(); + // ── 控制器信号 → 详情面板(tab 引擎):开页 / 页签就绪 / 加载中 / 聚焦 ── + QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::datasetOpened, + detailPanel, &geopro::app::DatasetDetailPanel::onDatasetOpened); + QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::tabReady, + detailPanel, &geopro::app::DatasetDetailPanel::onTabReady); + QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::tabLoadStarted, + detailPanel, &geopro::app::DatasetDetailPanel::onTabLoadStarted); + QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::focusRequested, + detailPanel, &geopro::app::DatasetDetailPanel::focusDataset); + // ── 页签懒加载:lazy 页签首次激活 → 控制器按 (dsId,ddCode,tabIndex) 拉载荷 → 回填 ── + QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::tabNeeded, &detailCtrl, + &geopro::controller::DatasetDetailController::loadTab); + // ── 分页:分页器翻页/改每页条数 → 控制器按页加载 → 回填(同 tabReady 路径,刷新表格+分页器)── + QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::tabPageNeeded, &detailCtrl, + &geopro::controller::DatasetDetailController::loadTabPaged); + // context 用 detailPanel:析构即自动断连,避免野指针。window 比 detailPanel 活得久, + // 捕 &window 取状态栏安全。失败时清该页 lazy 遮罩(幂等)并状态栏提示。 + QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::loadFailed, detailPanel, + [&window, detailPanel](const QString& dsId, const QString& msg) { + detailPanel->onLoadFailed(dsId, msg); + window.statusBar()->showMessage( + QStringLiteral("数据集 %1 加载失败:%2").arg(dsId, msg), 5000); }); - QObject::connect(actShowElectrodes, &QAbstractButton::toggled, detailWidget, - [showElectrodes, rebuildDetail](bool on) { - *showElectrodes = on; - rebuildDetail(); - }); - QObject::connect(actShowContour, &QAbstractButton::toggled, detailWidget, - [showContour, rebuildDetail](bool on) { - *showContour = on; - rebuildDetail(); + + // ── 详情面板切 Tab → 反向高亮数据集列表对应行 ── + QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::activeDatasetChanged, + datasetList, [datasetList](const QString& dsId) { + for (QTreeWidgetItemIterator it(datasetList); *it; ++it) + if ((*it)->data(0, geopro::app::kDsIdRole).toString() == dsId) { + datasetList->setCurrentItem(*it); + break; + } }); // 「视图详情」浮层显隐:仅三维显示,置于 QVTK 左上(工具条下方)并置顶。 @@ -792,12 +605,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // ── 启动:建立一次空背景中央视图(真实 sections 数据由下一轮接入)。 rebuildCentral(); - // VTK 背景随主题切换:直接重跑 rebuildCentral/rebuildDetail(走完整渲染路径、末尾必 Render, + // VTK 背景随主题切换:直接重跑 rebuildCentral(走完整渲染路径、末尾必 Render, // 比手动 SetBackground+Render 稳;兼顾 syncSystemTheme 异步切暗的时序)。 QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed, &window, - [rebuildCentral, rebuildDetail]() { + [rebuildCentral]() { rebuildCentral(); - rebuildDetail(); }); // 顶部应用区(静态视觉壳,对齐原型):上=菜单栏(视图/项目管理/业务工具/设备), @@ -830,6 +642,25 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re } return loaded; }; + // 数据树的「加载更多」:末尾顶层项;已加载数=树中非"加载更多"项总数(含各层子节点)。 + auto removeTreeLoadMore = [](QTreeWidget* tw) { + const int n = tw->topLevelItemCount(); + if (n > 0 && tw->topLevelItem(n - 1)->data(0, geopro::app::kDsLoadMoreRole).toBool()) + delete tw->takeTopLevelItem(n - 1); + }; + // total = 根节点总数(控制器按根分页);loaded 也按「第一层节点(根)」计 → 加载更多/页签数一致。 + auto addTreeLoadMore = [](QTreeWidget* tw, int total) { + int loaded = 0; + for (int i = 0; i < tw->topLevelItemCount(); ++i) + if (!tw->topLevelItem(i)->data(0, geopro::app::kDsLoadMoreRole).toBool()) ++loaded; + if (loaded < total) { + auto* m = new QTreeWidgetItem(tw); + m->setText(0, QStringLiteral("加载更多(%1/%2)").arg(loaded).arg(total)); + m->setData(0, geopro::app::kDsLoadMoreRole, true); + m->setTextAlignment(0, Qt::AlignCenter); + } + return loaded; + }; QObject::connect(topBar, &geopro::app::TopBar::workspaceSwitchRequested, &nav, &geopro::controller::WorkbenchNavController::switchWorkspace); QObject::connect(topBar, &geopro::app::TopBar::projectSwitchRequested, &nav, @@ -906,12 +737,12 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re datasetTabs->setTabText(1, QStringLiteral("文件")); }); QObject::connect(&nav, &geopro::controller::WorkbenchNavController::datasetsLoaded, datasetList, - [removeLoadMore, addLoadMore, datasetList, datasetTitle, datasetTabs]( + [removeTreeLoadMore, addTreeLoadMore, datasetList, datasetTitle, datasetTabs]( const QString&, const std::vector& rows, int total, bool append) { - removeLoadMore(datasetList); + removeTreeLoadMore(datasetList); geopro::app::populateDatasetList(datasetList, rows, append); - const int loaded = addLoadMore(datasetList, total); + const int loaded = addTreeLoadMore(datasetList, total); if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集")); datasetTabs->setTabText( 0, total > 0 ? QStringLiteral("数据 (%1/%2)").arg(loaded).arg(total) @@ -984,8 +815,40 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re } // namespace +namespace { +// 顶层异常护栏:任何 slot / 事件处理器抛出的 C++ 异常都会经过 QApplication::notify。 +// 默认 Qt 不允许异常穿透事件循环 → terminate(崩溃)。这里拦截 + 记录(异常信息 + 接收者 +// 对象类名 + 事件类型,足以定位崩点),并吞掉以**保证后端故障等异常不致整个客户端退出**。 +// 注:吞异常后该次事件处理中断,可能留下局部不一致;但"不崩 + 有日志"优先于直接退出。 +class GuardedApplication : public QApplication { +public: + using QApplication::QApplication; + bool notify(QObject* receiver, QEvent* e) override { + try { + return QApplication::notify(receiver, e); + } catch (const std::exception& ex) { + qCritical("[guard] 拦截未捕获异常: %s | type=%s | receiver=%s | event=%d", + ex.what(), typeid(ex).name(), + receiver ? receiver->metaObject()->className() : "null", + e ? static_cast(e->type()) : 0); + } catch (...) { + qCritical("[guard] 拦截未捕获非 std 异常 | receiver=%s | event=%d", + receiver ? receiver->metaObject()->className() : "null", + e ? static_cast(e->type()) : 0); + } + return false; + } +}; +} // namespace + int main(int argc, char* argv[]) { + // Qt WebEngine(地图页签的 QWebEngineView):必须在 QApplication 构造前初始化, + // 且需启用跨上下文共享 OpenGL(QtWebEngine 与 QVTK 同进程共用 GL context,避免黑屏/崩溃)。 + // AA_ShareOpenGLContexts 须在 QApplication 之前设置;QtWebEngineQuick::initialize() 同样须前置。 + QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); + QtWebEngineQuick::initialize(); + // 高 DPI 缩放采用直通策略:在 125%/150% 等分数缩放下字体/图标按真实比例渲染,更清晰。 // 必须在 QApplication 构造前设置。 QApplication::setHighDpiScaleFactorRoundingPolicy( @@ -993,12 +856,19 @@ int main(int argc, char* argv[]) // QVTK 默认 surface format 必须在 QApplication 之前设置(全局一次,两个 QVTK widget 共用)。 QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat()); - QApplication app(argc, argv); + GuardedApplication app(argc, argv); // 顶层异常护栏:slot/事件里的异常不致客户端崩溃 + + // 异步 ApiCall::finished 等信号携带 ApiResponse;注册元类型以支持跨 QueuedConnection 传递 + // (当前详情链路为同线程 DirectConnection,非严格必需,但作防御性注册,见 spec §5.1)。 + qRegisterMetaType(); // 组织/应用名:QSettings 持久化(dock 布局、登录记忆等)按此定位存储位置。 QCoreApplication::setOrganizationName(QStringLiteral("Geomative")); QCoreApplication::setApplicationName(QStringLiteral("Geopro3")); + // 日志 + 崩溃捕获:尽早安装(依赖上面的 Org/App 名定位日志目录)。生产桌面端问题可回溯。 + geopro::app::initLogging(); + // 主题与字号:应用持久化偏好(跟随系统/浅/深 + 字号缩放),再套用 Fusion + 全局 QSS + 调色板。 // 在弹登录窗之前完成 → 登录页与主页 明暗/字号 统一。 geopro::app::applyPersistedThemeMode(); @@ -1050,6 +920,17 @@ int main(int argc, char* argv[]) geopro::data::ApiProjectRepository projectRepo(api); geopro::controller::WorkbenchNavController nav(projectRepo); + // 数据详情仓储 + 控制器(接真实反演 API):同一共享会话 ApiClient。 + geopro::data::ApiDatasetRepository datasetRepo(api); + // 策略注册表须比 detailCtrl 活得久(同作用域、在前声明)。 + geopro::controller::ChartStrategyRegistry chartRegistry; + chartRegistry.add(std::make_unique()); + chartRegistry.add(std::make_unique()); + chartRegistry.add(std::make_unique()); + chartRegistry.add(std::make_unique()); + chartRegistry.add(std::make_unique()); + geopro::controller::DatasetDetailController detailCtrl(datasetRepo, chartRegistry); + // ── 外壳:标准 QMainWindow(原生标题栏)。buildWorkbench 直接用其 // setCentralWidget/setMenuWidget/statusBar 承载工作台。 const QString kTitle = QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)"); @@ -1057,7 +938,7 @@ int main(int argc, char* argv[]) window->setWindowTitle(kTitle); window->resize(1280, 800); window->setMinimumSize(1024, 680); - buildWorkbench(*window, repo, projectRepo, nav); + buildWorkbench(*window, repo, projectRepo, nav, detailCtrl); // 主题桥:ThemeManager 明/暗切换 → 重应用全局 QSS+调色板(标准控件 + ADS;内联 chrome 经各自连接)。 QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed, diff --git a/src/app/panels/AnomalyTablePanel.cpp b/src/app/panels/AnomalyTablePanel.cpp new file mode 100644 index 0000000..84c7c0b --- /dev/null +++ b/src/app/panels/AnomalyTablePanel.cpp @@ -0,0 +1,40 @@ +#include "panels/AnomalyTablePanel.hpp" +#include +#include +#include +#include +namespace geopro::app { +static QString markName(int t) { return t == 1 ? "点" : t == 3 ? "多边形" : "多段线"; } + +AnomalyTablePanel::AnomalyTablePanel(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); lay->setContentsMargins(0, 0, 0, 0); + table_ = new QTableWidget(this); + table_->setColumnCount(6); + table_->setHorizontalHeaderLabels({"名称", "异常类型", "几何类型", "创建时间", "备注", "操作"}); + table_->horizontalHeader()->setStretchLastSection(true); + table_->setEditTriggers(QAbstractItemView::NoEditTriggers); + lay->addWidget(table_); +} + +void AnomalyTablePanel::setAnomalies(const std::vector& list, + const std::vector& createTimes, + const std::vector& remarks) { + hidden_.clear(); + table_->setRowCount(static_cast(list.size())); + for (int i = 0; i < static_cast(list.size()); ++i) { + const auto& a = list[i]; + table_->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(a.name))); + table_->setItem(i, 1, new QTableWidgetItem(QString::fromStdString(a.typeName))); + table_->setItem(i, 2, new QTableWidgetItem(markName(static_cast(a.markType)))); + table_->setItem(i, 3, new QTableWidgetItem(i < (int)createTimes.size() ? createTimes[i] : "")); + table_->setItem(i, 4, new QTableWidgetItem(i < (int)remarks.size() ? remarks[i] : "")); + auto* eye = new QToolButton(table_); eye->setCheckable(true); eye->setChecked(true); + eye->setText("👁"); + connect(eye, &QToolButton::toggled, this, [this, i](bool on) { + if (on) hidden_.erase(i); else hidden_.insert(i); + emit hiddenChanged(hidden_); + }); + table_->setCellWidget(i, 5, eye); + } +} +} // namespace geopro::app diff --git a/src/app/panels/AnomalyTablePanel.hpp b/src/app/panels/AnomalyTablePanel.hpp new file mode 100644 index 0000000..b765759 --- /dev/null +++ b/src/app/panels/AnomalyTablePanel.hpp @@ -0,0 +1,23 @@ +#pragma once +#include +#include +#include +#include "model/Anomaly.hpp" +class QTableWidget; +namespace geopro::app { + +// ds 级异常表:名称/异常类型/几何类型/创建时间/备注/操作(显隐眼睛)。行显隐 → 信号驱动图表叠加。 +class AnomalyTablePanel : public QWidget { + Q_OBJECT +public: + explicit AnomalyTablePanel(QWidget* parent = nullptr); + void setAnomalies(const std::vector& list, + const std::vector& createTimes, + const std::vector& remarks); +signals: + void hiddenChanged(const std::set& hiddenIndices); +private: + QTableWidget* table_; + std::set hidden_; +}; +} // namespace geopro::app diff --git a/src/app/panels/DatasetDetailPage.cpp b/src/app/panels/DatasetDetailPage.cpp new file mode 100644 index 0000000..16bfd00 --- /dev/null +++ b/src/app/panels/DatasetDetailPage.cpp @@ -0,0 +1,94 @@ +#include "panels/DatasetDetailPage.hpp" + +#include +#include + +#include "Glyphs.hpp" +#include "PanelHeader.hpp" +#include "panels/LoadingOverlay.hpp" +#include "panels/chart/DataTableView.hpp" +#include "panels/chart/DetailViewFactory.hpp" +#include "panels/chart/IDetailView.hpp" + +namespace geopro::app { + +DatasetDetailPage::DatasetDetailPage(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); + lay->setContentsMargins(0, 0, 0, 0); + lay->setSpacing(0); +} + +void DatasetDetailPage::build(const QString& dsId, const QString& ddCode, const QString& dsName, + const std::vector& tabs) { + Q_ASSERT(views_.empty()); // build() 仅一次:views_ 为已 release 的裸指针,二次构建会泄漏旧视图 + if (!views_.empty()) return; + dsId_ = dsId; + ddCode_ = ddCode; + dsName_ = dsName; + tabs_ = tabs; + views_.assign(tabs.size(), nullptr); + loaded_.assign(tabs.size(), false); + requested_.assign(tabs.size(), false); + overlays_.clear(); + + // 按 TabSpec 经工厂造视图,并组装为带表头页签的面板。 + QVector panelTabs; + for (size_t i = 0; i < tabs.size(); ++i) { + const auto& spec = tabs[i]; + auto view = makeDetailView(spec.kind, this); // 抛出由调用栈兜底(GuardedApplication) + IDetailView* raw = view.release(); // QWidget 由 this/QwtPlot 父子树接管生命周期 + views_[i] = raw; + // lazy 页签:建覆盖该视图的加载遮罩(父为视图 widget,随其尺寸覆盖图区)。 + if (spec.lazy) overlays_[static_cast(i)] = new LoadingOverlay(raw->widget()); + // 分页型页签:把表格视图的分页请求冒泡为页信号(携带 dsId/ddCode/tabIndex + 页参数)。 + if (spec.paginated) { + if (auto* table = qobject_cast(raw->widget())) { + const int idx = static_cast(i); + connect(table, &DataTableView::pageRequested, this, + [this, idx](int pageNo, int pageSize) { + emit tabPageNeeded(dsId_, ddCode_, idx, pageNo, pageSize); + }); + } + } + panelTabs.append({Glyph::Detail, spec.title, raw->widget(), false}); + } + const QVector actions = { + {Glyph::Download, QStringLiteral("导出")}, + }; + + auto tabbedPanel = buildTabbedPanel(panelTabs, actions); + layout()->addWidget(tabbedPanel.container); + + // lazy 页签首次激活 → 发 tabNeeded 请求懒加载(数据慢,故不随开页同步拉)。 + if (tabbedPanel.tabGroup) { + connect(tabbedPanel.tabGroup, &QButtonGroup::idClicked, this, [this](int idx) { + if (idx < 0 || idx >= static_cast(tabs_.size())) return; + if (!tabs_[static_cast(idx)].lazy) return; + if (requested_[static_cast(idx)] || loaded_[static_cast(idx)]) return; + if (dsId_.isEmpty()) return; + requested_[static_cast(idx)] = true; + emit tabNeeded(dsId_, ddCode_, idx); + }); + } +} + +void DatasetDetailPage::setTabPayload(int tabIndex, const QVariant& payload) { + if (tabIndex < 0 || tabIndex >= static_cast(views_.size())) return; + if (auto* v = views_[static_cast(tabIndex)]) v->setPayload(payload); + loaded_[static_cast(tabIndex)] = true; + requested_[static_cast(tabIndex)] = true; // 已加载,切回不再重复请求 + setTabLoading(tabIndex, false); // 数据到达,隐藏遮罩 +} + +void DatasetDetailPage::setTabLoading(int tabIndex, bool on) { + auto it = overlays_.find(tabIndex); + if (it == overlays_.end()) return; // 非 lazy 页签无遮罩 + if (on) it.value()->showOver(); else it.value()->hide(); +} + +void DatasetDetailPage::clearAllLoadingOverlays() { + for (auto* overlay : overlays_) + if (overlay) overlay->hide(); +} + +} // namespace geopro::app diff --git a/src/app/panels/DatasetDetailPage.hpp b/src/app/panels/DatasetDetailPage.hpp new file mode 100644 index 0000000..98dc161 --- /dev/null +++ b/src/app/panels/DatasetDetailPage.hpp @@ -0,0 +1,54 @@ +#pragma once +#include +#include +#include +#include +#include "DatasetDetailTab.hpp" // geopro::controller::TabSpec + +namespace geopro::app { + +class IDetailView; +class LoadingOverlay; + +// 单个数据集详情页:按策略 tabs() 动态建页签 + 右侧「导出」操作。 +// 每页签由工厂造的 IDetailView 承载;lazy 页签首次激活时发 tabNeeded 请求懒加载。 +class DatasetDetailPage : public QWidget { + Q_OBJECT +public: + explicit DatasetDetailPage(QWidget* parent = nullptr); + + // 按页签集构建页签(首次打开调一次)。dsId/ddCode/dsName 用于 tabNeeded。 + void build(const QString& dsId, const QString& ddCode, const QString& dsName, + const std::vector& tabs); + + // 页签载荷到达 → 下发给对应视图并标记已加载、隐藏遮罩。 + void setTabPayload(int tabIndex, const QVariant& payload); + // 页签加载进行中 → 对 lazy 页签显示遮罩(非 lazy 页签无遮罩,幂等忽略)。 + void setTabLoading(int tabIndex, bool on); + // 清掉本页全部加载遮罩(失败兜底用,不假设页签数;幂等)。 + void clearAllLoadingOverlays(); + + QString dsId() const { return dsId_; } + int tabCount() const { return static_cast(tabs_.size()); } + +signals: + // lazy 页签首次激活且未加载 → 请求懒加载。 + void tabNeeded(const QString& dsId, const QString& ddCode, int tabIndex); + // 分页型页签(paginated)分页器翻页/改每页条数 → 请求按页加载。 + void tabPageNeeded(const QString& dsId, const QString& ddCode, int tabIndex, int pageNo, + int pageSize); + +private: + QString dsId_; + QString ddCode_; + QString dsName_; + std::vector tabs_; + // 与 tabs_ 同序。每个 IDetailView 持有的 QWidget 经 build() 以 this 为父接管, + // 生命周期由 Qt 父子树清理(不在此 delete);build() 仅调用一次(见其断言)。 + std::vector views_; + std::vector loaded_; // 各页签是否已加载(避免重复请求) + std::vector requested_; // lazy 页签是否已请求过 + QMap overlays_; // lazy 页签的加载遮罩(覆盖该视图) +}; + +} // namespace geopro::app diff --git a/src/app/panels/DatasetDetailPanel.cpp b/src/app/panels/DatasetDetailPanel.cpp new file mode 100644 index 0000000..4688d62 --- /dev/null +++ b/src/app/panels/DatasetDetailPanel.cpp @@ -0,0 +1,55 @@ +#include "panels/DatasetDetailPanel.hpp" +#include "panels/DatasetDetailPage.hpp" +namespace geopro::app { + +DatasetDetailPanel::DatasetDetailPanel(QWidget* parent) : QTabWidget(parent) { + setTabsClosable(true); + connect(this, &QTabWidget::tabCloseRequested, this, [this](int i) { widget(i)->deleteLater(); }); + connect(this, &QTabWidget::currentChanged, this, [this](int i) { + if (auto* p = qobject_cast(widget(i))) + emit activeDatasetChanged(p->dsId()); + }); +} + +DatasetDetailPage* DatasetDetailPanel::pageFor(const QString& dsId) const { + for (int i = 0; i < count(); ++i) + if (auto* p = qobject_cast(widget(i))) + if (p->dsId() == dsId) return p; + return nullptr; +} + +void DatasetDetailPanel::onDatasetOpened(const QString& dsId, const QString& ddCode, + const QString& dsName, + const std::vector& tabs) { + auto* p = pageFor(dsId); + if (!p) { + p = new DatasetDetailPage(this); + p->build(dsId, ddCode, dsName, tabs); // ddCode 透传 → 页内 tabNeeded 携带 + const QString title = dsName.isEmpty() ? dsId : dsName; // 页签标题用数据名(空则回退 id) + const int idx = addTab(p, title); + setTabToolTip(idx, title); // 名称过长被省略时悬停可见全名 + // 页内 lazy 页签首次激活 → 冒泡为面板信号(外部接控制器 loadTab)。 + connect(p, &DatasetDetailPage::tabNeeded, this, &DatasetDetailPanel::tabNeeded); + // 页内分页器翻页 → 冒泡为面板信号(外部接控制器 loadTabPaged)。 + connect(p, &DatasetDetailPage::tabPageNeeded, this, &DatasetDetailPanel::tabPageNeeded); + } + setCurrentWidget(p); +} + +void DatasetDetailPanel::onTabReady(const QString& dsId, int tabIndex, const QVariant& payload) { + if (auto* p = pageFor(dsId)) p->setTabPayload(tabIndex, payload); +} + +void DatasetDetailPanel::onTabLoadStarted(const QString& dsId, int tabIndex) { + if (auto* p = pageFor(dsId)) p->setTabLoading(tabIndex, true); +} + +void DatasetDetailPanel::onLoadFailed(const QString& dsId, const QString&) { + // 失败:清掉该页全部「加载中」遮罩(不假设页签数;幂等:非 lazy/已隐藏皆无害)。 + if (auto* p = pageFor(dsId)) p->clearAllLoadingOverlays(); +} + +void DatasetDetailPanel::focusDataset(const QString& dsId) { + if (auto* p = pageFor(dsId)) setCurrentWidget(p); +} +} // namespace geopro::app diff --git a/src/app/panels/DatasetDetailPanel.hpp b/src/app/panels/DatasetDetailPanel.hpp new file mode 100644 index 0000000..1707a5d --- /dev/null +++ b/src/app/panels/DatasetDetailPanel.hpp @@ -0,0 +1,33 @@ +#pragma once +#include +#include +#include +#include "DatasetDetailTab.hpp" // geopro::controller::TabSpec +namespace geopro::app { +class DatasetDetailPage; + +// 多 Tab 壳:每数据集一页(按 dsId 去重)。R095。tab 引擎版。 +class DatasetDetailPanel : public QTabWidget { + Q_OBJECT +public: + explicit DatasetDetailPanel(QWidget* parent = nullptr); + + // 数据集打开:find-or-create 页 → build(tabs) → 加/抬该面板页签。 + void onDatasetOpened(const QString& dsId, const QString& ddCode, const QString& dsName, + const std::vector& tabs); + void onTabReady(const QString& dsId, int tabIndex, const QVariant& payload); + void onTabLoadStarted(const QString& dsId, int tabIndex); + void onLoadFailed(const QString& dsId, const QString& message); + void focusDataset(const QString& dsId); // 单击聚焦已开页 + +signals: + void activeDatasetChanged(const QString& dsId); // 反向联动数据集列表 + void tabNeeded(const QString& dsId, const QString& ddCode, int tabIndex); // lazy 页首激活 → 懒加载 + // 分页型页签分页器翻页 → 按页加载(外部接控制器 loadTabPaged)。 + void tabPageNeeded(const QString& dsId, const QString& ddCode, int tabIndex, int pageNo, + int pageSize); + +private: + DatasetDetailPage* pageFor(const QString& dsId) const; +}; +} // namespace geopro::app diff --git a/src/app/panels/DatasetListPanel.cpp b/src/app/panels/DatasetListPanel.cpp index 4856a85..d2e2ab8 100644 --- a/src/app/panels/DatasetListPanel.cpp +++ b/src/app/panels/DatasetListPanel.cpp @@ -1,6 +1,8 @@ #include "panels/DatasetListPanel.hpp" +#include #include +#include #include #include #include @@ -8,6 +10,9 @@ #include #include #include +#include +#include +#include #include "Theme.hpp" @@ -104,19 +109,53 @@ public: }; } // namespace -void populateDatasetList(QListWidget* list, const std::vector& rows, bool append) { - if (!list) return; - if (!append) list->clear(); - for (const auto& d : rows) { - QString text = QString::fromStdString(d.dsName); - QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间 - if (!d.typeName.empty()) - sub += QStringLiteral(" · %1").arg(QString::fromStdString(d.typeName)); // 再跟类型 - if (!sub.isEmpty()) text += QStringLiteral("\n%1").arg(sub); - auto* item = new QListWidgetItem(text, list); - item->setData(kDsIdRole, QString::fromStdString(d.id)); - item->setData(kDsDdTypeRole, QString::fromStdString(d.ddCode)); +namespace { +// 建一条数据集树项(不挂载):列0 文本 = dsName +「创建时间 · 类型名」,data 存各角色。 +QTreeWidgetItem* makeDatasetItem(const geopro::data::DsRow& d) { + QString text = QString::fromStdString(d.dsName); + QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间 + if (!d.typeName.empty()) + sub += QStringLiteral(" · %1").arg(QString::fromStdString(d.typeName)); // 再跟类型 + if (!sub.isEmpty()) text += QStringLiteral("\n%1").arg(sub); + auto* item = new QTreeWidgetItem(); + item->setText(0, text); + item->setData(0, kDsIdRole, QString::fromStdString(d.id)); + item->setData(0, kDsDdTypeRole, QString::fromStdString(d.ddCode)); + item->setData(0, kDsDdCodeRole, QString::fromStdString(d.ddCode)); + item->setData(0, kDsNameRole, QString::fromStdString(d.dsName)); + return item; +} +} // namespace + +void populateDatasetList(QTreeWidget* tree, const std::vector& rows, bool append) { + if (!tree) return; + if (!append) tree->clear(); + + // id→已在树中的项:!append 时为空;append(分页)时含已加载行,使新行能挂到既有父下。 + QHash byId; + for (QTreeWidgetItemIterator it(tree); *it; ++it) { + const QString id = (*it)->data(0, kDsIdRole).toString(); + if (!id.isEmpty()) byId.insert(id, *it); } + + // 第一遍:本批全部建项(不挂载)并登记 id,使同批内的父子也能互相找到。 + std::vector batch; + batch.reserve(rows.size()); + for (const auto& d : rows) { + auto* item = makeDatasetItem(d); + byId.insert(QString::fromStdString(d.id), item); + batch.push_back(item); + } + // 第二遍:按 parentId 挂载。父在集合内→作其子;否则(父是源文件节点/不在本批)→作树根。 + for (std::size_t i = 0; i < rows.size(); ++i) { + const QString pid = QString::fromStdString(rows[i].parentId); + QTreeWidgetItem* parent = pid.isEmpty() ? nullptr : byId.value(pid, nullptr); + if (parent && parent != batch[i]) + parent->addChild(batch[i]); + else + tree->addTopLevelItem(batch[i]); + } + // 默认折叠(对齐原版:仅显源数据根行,派生数据收在展开箭头内)。 } void populateFileList(QListWidget* list, const std::vector& rows, bool append) { @@ -140,13 +179,13 @@ void populateFileList(QListWidget* list, const std::vector& } } -void applyDatasetCardDelegate(QListWidget* list) { - if (!list) return; - list->setItemDelegate(new DatasetCardDelegate(list)); - list->setMouseTracking(true); // 让委托收到 hover 状态 - list->setSpacing(0); // 卡间距由委托内边距控制 - QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, list, - [list]() { list->viewport()->update(); }); +void applyDatasetCardDelegate(QAbstractItemView* view) { + if (!view) return; + view->setItemDelegate(new DatasetCardDelegate(view)); + view->setMouseTracking(true); // 让委托收到 hover 状态 + if (auto* list = qobject_cast(view)) list->setSpacing(0); // 卡间距由委托内边距控制 + QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, view, + [view]() { view->viewport()->update(); }); } } // namespace geopro::app diff --git a/src/app/panels/DatasetListPanel.hpp b/src/app/panels/DatasetListPanel.hpp index 8ac2887..a06d937 100644 --- a/src/app/panels/DatasetListPanel.hpp +++ b/src/app/panels/DatasetListPanel.hpp @@ -4,6 +4,8 @@ #include "repo/RepoTypes.hpp" class QListWidget; +class QTreeWidget; +class QAbstractItemView; namespace geopro::app { @@ -12,13 +14,18 @@ constexpr int kDsIdRole = 0x0100; // Qt::UserRole constexpr int kDsDdTypeRole = 0x0101; // Qt::UserRole + 1 constexpr int kDsFileUrlRole = 0x0102; // Qt::UserRole + 2(文件下载 url,备用) constexpr int kDsLoadMoreRole = 0x0103; // 标记"加载更多"行 +constexpr int kDsDdCodeRole = 0x0104; // Qt::UserRole + 4(ddCode,双击详情选策略用) +constexpr int kDsNameRole = 0x0105; // Qt::UserRole + 5(dsName,详情页签标题用) -// 数据页签:每条 = dsName +(类型名);UserRole 存 dsId、+1 存 ddCode。 -void populateDatasetList(QListWidget* list, const std::vector& rows, bool append); +// 数据页签:树形(按 DsRow.parentId 嵌套,源数据为根、派生数据挂其下,对齐原版 el-table 树)。 +// 每项列0:文本 = dsName +「创建时间 · 类型名」;data(0,角色) 存 dsId/ddCode/dsName。 +// append=true 时把新行挂到已加载的父节点下(分页)。 +void populateDatasetList(QTreeWidget* tree, const std::vector& rows, bool append); // 文件页签:每条 = 文件名 +(可读大小);UserRole 存 dsId、+2 存文件 url。空时显示占位。 void populateFileList(QListWidget* list, const std::vector& rows, bool append); // 给数据/文件列表套用卡片委托(标题+元信息双行、悬停/选中圆角高亮+左强调竖条,规范§6.2)。 -void applyDatasetCardDelegate(QListWidget* list); +// 接受 QListWidget(文件)或 QTreeWidget(数据树)——故形参为其共同基类 QAbstractItemView。 +void applyDatasetCardDelegate(QAbstractItemView* view); } // namespace geopro::app diff --git a/src/app/panels/DescriptionPanel.cpp b/src/app/panels/DescriptionPanel.cpp new file mode 100644 index 0000000..5289b9d --- /dev/null +++ b/src/app/panels/DescriptionPanel.cpp @@ -0,0 +1,22 @@ +#include "panels/DescriptionPanel.hpp" + +#include +#include + +namespace geopro::app { + +DescriptionPanel::DescriptionPanel(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); + lay->setContentsMargins(8, 8, 8, 8); + + edit_ = new QTextEdit(this); + edit_->setReadOnly(true); + edit_->setPlaceholderText(QStringLiteral("暂无描述")); + lay->addWidget(edit_); +} + +void DescriptionPanel::setText(const QString& text) { + edit_->setPlainText(text); +} + +} // namespace geopro::app diff --git a/src/app/panels/DescriptionPanel.hpp b/src/app/panels/DescriptionPanel.hpp new file mode 100644 index 0000000..b364fae --- /dev/null +++ b/src/app/panels/DescriptionPanel.hpp @@ -0,0 +1,18 @@ +#pragma once +#include + +class QTextEdit; + +namespace geopro::app { + +// 数据集描述面板:只读文本,供网格数据底部页签「描述」使用。 +class DescriptionPanel : public QWidget { + Q_OBJECT +public: + explicit DescriptionPanel(QWidget* parent = nullptr); + void setText(const QString& text); +private: + QTextEdit* edit_; +}; + +} // namespace geopro::app diff --git a/src/app/panels/LoadingOverlay.cpp b/src/app/panels/LoadingOverlay.cpp new file mode 100644 index 0000000..095c1f9 --- /dev/null +++ b/src/app/panels/LoadingOverlay.cpp @@ -0,0 +1,46 @@ +#include "panels/LoadingOverlay.hpp" +#include +#include +#include +#include + +#include "Theme.hpp" + +namespace geopro::app { + +LoadingOverlay::LoadingOverlay(QWidget* parent) : QWidget(parent), label_(new QLabel(this)) { + Q_ASSERT(parent); // 契约:必须有父(遮罩几何跟随父,无父无法工作) + setAttribute(Qt::WA_StyledBackground, true); + label_->setText(QStringLiteral("加载中…")); + label_->setAlignment(Qt::AlignCenter); + auto* lay = new QVBoxLayout(this); + lay->addWidget(label_); + if (parent) parent->installEventFilter(this); + + // 半透明遮罩跟随主题:浅色白纱深字(原版),暗色深纱浅字,避免暗色下刺眼白蒙板。 + const auto applyTheme = [this]() { + const QColor veil = isDarkTheme() ? tokenColor("bg/app") : QColor(255, 255, 255); + setStyleSheet(QStringLiteral("background: rgba(%1,%2,%3,160);") + .arg(veil.red()).arg(veil.green()).arg(veil.blue())); + label_->setStyleSheet(QStringLiteral("background: transparent; color: %1;") + .arg(tokenColor("text/primary").name())); + }; + applyTheme(); + connect(&ThemeManager::instance(), &ThemeManager::changed, this, applyTheme); + hide(); +} + +void LoadingOverlay::showOver() { + if (parentWidget()) setGeometry(parentWidget()->rect()); + raise(); + show(); +} + +bool LoadingOverlay::eventFilter(QObject* obj, QEvent* ev) { + if (obj == parentWidget() && ev->type() == QEvent::Resize && isVisible()) { + setGeometry(parentWidget()->rect()); + } + return QWidget::eventFilter(obj, ev); +} + +} // namespace geopro::app diff --git a/src/app/panels/LoadingOverlay.hpp b/src/app/panels/LoadingOverlay.hpp new file mode 100644 index 0000000..624b4eb --- /dev/null +++ b/src/app/panels/LoadingOverlay.hpp @@ -0,0 +1,18 @@ +#pragma once +#include +class QLabel; +namespace geopro::app { + +// 半透明「加载中…」遮罩。贴在目标视图上层,showOver()/hide() 切换,几何随父 resize 跟随。 +class LoadingOverlay : public QWidget { + Q_OBJECT +public: + explicit LoadingOverlay(QWidget* parent); + void showOver(); // 铺满父尺寸、置顶、显示 +protected: + bool eventFilter(QObject* obj, QEvent* ev) override; // 跟随父 resize +private: + QLabel* label_; +}; + +} // namespace geopro::app diff --git a/src/app/panels/chart/BarChartView.cpp b/src/app/panels/chart/BarChartView.cpp new file mode 100644 index 0000000..2e872a3 --- /dev/null +++ b/src/app/panels/chart/BarChartView.cpp @@ -0,0 +1,215 @@ +#include "panels/chart/BarChartView.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "Theme.hpp" +#include "panels/chart/ChartTheme.hpp" + +namespace geopro::app { + +namespace { + +// 类目轴刻度:把整数刻度位(0,1,2,…)映射为类目标签 "#1","#2",…(来自 categories)。 +// 非整数/越界刻度返回空标签(避免次刻度污染)。 +class CategoryScaleDraw : public QwtScaleDraw { +public: + explicit CategoryScaleDraw(std::vector labels) : labels_(std::move(labels)) { + enableComponent(QwtScaleDraw::Backbone, true); + enableComponent(QwtScaleDraw::Ticks, true); + } + QwtText label(double v) const override { + const double r = std::round(v); + if (std::abs(v - r) > 1e-6) return QwtText(); // 仅整数刻度出标签 + const int i = static_cast(r); + if (i < 0 || i >= static_cast(labels_.size())) return QwtText(); + return labels_[static_cast(i)]; + } + +private: + std::vector labels_; +}; + +QColor barColor(const QString& hex) { + QColor c(hex); + return c.isValid() ? c : QColor(0x54, 0x70, 0xc6); // 回退 ECharts 蓝 +} + +// 造一个 Box 样式柱符号(实心填充、无边框)。 +QwtColumnSymbol* makeBarSymbol(const QColor& fill) { + auto* sym = new QwtColumnSymbol(QwtColumnSymbol::Box); + sym->setLineWidth(0); + sym->setFrameStyle(QwtColumnSymbol::NoFrame); + QPalette pal(fill); + pal.setColor(QPalette::Window, fill); + pal.setColor(QPalette::Dark, fill); + sym->setPalette(pal); + return sym; +} + +} // namespace + +BarChartView::BarChartView(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); + lay->setContentsMargins(0, 0, 0, 0); + lay->setSpacing(0); + + // 左上水平 y 轴标题(ECharts 风格,置于图例行左侧)。 + yTitle_ = new QLabel(this); + auto* titleRow = new QWidget(this); + auto* titleLay = new QHBoxLayout(titleRow); + titleLay->setContentsMargins(48, 6, 8, 0); // 左缩进对齐 y 轴上方 + titleLay->setSpacing(0); + titleLay->addWidget(yTitle_); + titleLay->addStretch(); + lay->addWidget(titleRow); + + plot_ = new QwtPlot(this); + plot_->setObjectName(QStringLiteral("grBarPlotArea")); + plot_->enableAxis(QwtPlot::xBottom, true); + plot_->enableAxis(QwtPlot::yLeft, true); + + // x 轴标题「电极点」底部居中(setData 设文本)。 + plot_->setAxisTitle(QwtPlot::xBottom, QwtText()); + + // 顶部图例(P1/P2)。 + plot_->insertLegend(new QwtLegend(), QwtPlot::TopLegend); + + // 仅横向(y)网格,弱化(与原版 ECharts 一致:仅水平刻度线)。 + auto* grid = new QwtPlotGrid(); + grid->enableX(false); + grid->enableY(true); + grid->enableXMin(false); + grid->enableYMin(false); + grid->setMajorPen(QColor(225, 225, 225), 1.0, Qt::SolidLine); + grid->attach(plot_); + + plot_->setMinimumSize(0, 0); + lay->addWidget(plot_, 1); + + // 主题:底色/轴字/网格按当前主题套一次 + 热切换。 + applyChartPlotTheme(plot_); + QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, plot_, + [this]() { + applyChartPlotTheme(plot_); + // y 轴标题文字色随主题。 + QPalette pal = yTitle_->palette(); + pal.setColor(QPalette::WindowText, + isDarkTheme() ? tokenColor("text/secondary") + : QColor(90, 90, 90)); + yTitle_->setPalette(pal); + }); +} + +BarChartView::~BarChartView() { + // 卸载并删除已挂的柱状项,先于 QwtPlot autoDelete 触发(与 RawDataChartView/ + // GridDataChartView 析构对称,避免悬垂/双删)。 + clearSeries(); +} + +void BarChartView::clearSeries() { + for (QwtPlotItem* it : barItems_) { + it->detach(); + delete it; + } + barItems_.clear(); +} + +void BarChartView::setPayload(const QVariant& payload) { + if (!payload.canConvert()) return; // 坏/空 → 空态 + setData(payload.value()); +} + +void BarChartView::setData(const geopro::core::BarPayload& p) { + data_ = p; + clearSeries(); + + const int n = static_cast(p.categories.size()); + + // y 轴标题(左上水平 QLabel)。 + yTitle_->setText(p.yTitle); + QPalette tpal = yTitle_->palette(); + tpal.setColor(QPalette::WindowText, + isDarkTheme() ? tokenColor("text/secondary") : QColor(90, 90, 90)); + yTitle_->setPalette(tpal); + + // x 轴标题「电极点」+ 类目刻度 "#1".."#40"。 + plot_->setAxisTitle(QwtPlot::xBottom, p.xTitle); + plot_->setAxisScaleDraw(QwtPlot::xBottom, new CategoryScaleDraw(p.categories)); + plot_->setAxisScale(QwtPlot::xBottom, -0.5, n - 0.5, 1.0); // 每类目一刻度 + plot_->setAxisMaxMinor(QwtPlot::xBottom, 0); + + // y 范围 0..max(自动从数据取上界,留 5% 余量)。 + double yMax = 0.0; + for (const auto& s : p.series) + for (double v : s.values) yMax = std::max(yMax, v); + plot_->setAxisScale(QwtPlot::yLeft, 0.0, yMax > 0 ? yMax * 1.05 : 1.0); + + if (p.series.empty()) { + plot_->replot(); + return; + } + + if (p.series.size() == 1) { + // 单系列 → QwtPlotBarChart。 + const auto& s = p.series.front(); + auto* chart = new QwtPlotBarChart(s.name); + QVector samples; + samples.reserve(n); + for (int i = 0; i < n && i < static_cast(s.values.size()); ++i) + samples.append(QPointF(i, s.values[static_cast(i)])); + chart->setSamples(samples); + chart->setSymbol(makeBarSymbol(barColor(s.color))); + chart->setLegendMode(QwtPlotBarChart::LegendChartTitle); + chart->setLayoutPolicy(QwtPlotBarChart::AutoAdjustSamples); + chart->setSpacing(10); // 柱间留白(默认柱状观感) + chart->setMargin(4); + chart->attach(plot_); + barItems_.push_back(chart); + } else { + // 多系列(P1+P2)→ QwtPlotMultiBarChart(分组柱)。 + auto* chart = new QwtPlotMultiBarChart(); + QList titles; + for (size_t k = 0; k < p.series.size(); ++k) { + chart->setSymbol(static_cast(k), makeBarSymbol(barColor(p.series[k].color))); + titles.append(QwtText(p.series[k].name)); + } + chart->setBarTitles(titles); + chart->setStyle(QwtPlotMultiBarChart::Grouped); + chart->setLayoutPolicy(QwtPlotMultiBarChart::AutoAdjustSamples); + chart->setSpacing(10); + chart->setMargin(4); + + QVector samples; + samples.reserve(n); + for (int i = 0; i < n; ++i) { + QVector set; + set.reserve(static_cast(p.series.size())); + for (const auto& s : p.series) + set.append(i < static_cast(s.values.size()) ? s.values[static_cast(i)] + : 0.0); + samples.append(QwtSetSample(i, set)); + } + chart->setSamples(samples); + chart->attach(plot_); + barItems_.push_back(chart); + } + + plot_->replot(); +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/BarChartView.hpp b/src/app/panels/chart/BarChartView.hpp new file mode 100644 index 0000000..c889eee --- /dev/null +++ b/src/app/panels/chart/BarChartView.hpp @@ -0,0 +1,40 @@ +#pragma once +#include +#include + +#include "model/detail/DetailPayloads.hpp" +#include "panels/chart/IDetailView.hpp" + +class QLabel; +class QwtPlot; +class QwtPlotItem; + +namespace geopro::app { + +// 柱状图视图(dd_ert_measurement_gr_data 接地电阻 柱状图页签): +// QwtPlot + QwtPlotBarChart(单系列 P1)或 QwtPlotMultiBarChart(分组 P1+P2)。 +// x 轴类目标签 "#1".."#40"(自定义 QwtScaleDraw);x 轴标题「电极点」(底部居中); +// y 轴标题「电阻(单位:欧姆)」用左上水平 QLabel(ECharts 风格,对齐原版); +// 顶部图例(P1/P2);柱填充 #5470c6(P1,数据色,两主题一致)。 +// 背景/轴字/网格随主题(ChartTheme / ThemeManager)。 +class BarChartView : public QWidget, public IDetailView { + Q_OBJECT +public: + explicit BarChartView(QWidget* parent = nullptr); + ~BarChartView() override; + + void setData(const geopro::core::BarPayload& p); + + QWidget* widget() override { return this; } + void setPayload(const QVariant& payload) override; // 坏/空 variant → 空态不崩 + +private: + void clearSeries(); // 卸载并删除已挂的柱状项(避免 QwtPlot autoDelete 双删) + + geopro::core::BarPayload data_; + QwtPlot* plot_; + QLabel* yTitle_; // 左上水平 y 轴标题(ECharts 风格) + std::vector barItems_; // 当前挂载的柱状项(已 attach;卸载时 detach+delete) +}; + +} // namespace geopro::app diff --git a/src/app/panels/chart/ChartTheme.cpp b/src/app/panels/chart/ChartTheme.cpp new file mode 100644 index 0000000..6abb138 --- /dev/null +++ b/src/app/panels/chart/ChartTheme.cpp @@ -0,0 +1,45 @@ +#include "panels/chart/ChartTheme.hpp" + +#include +#include +#include +#include +#include +#include + +#include "Theme.hpp" + +namespace geopro::app { + +void applyChartPlotTheme(QwtPlot* plot) { + if (!plot) return; + const bool dark = isDarkTheme(); + // 浅色分支=原有硬编码值(保持与原版 1:1);暗色分支=主题 token。 + const QColor bg = dark ? tokenColor("bg/panel") : QColor(Qt::white); + const QColor axisText = dark ? tokenColor("text/secondary"): QColor(90, 90, 90); + const QColor gridMajor = dark ? tokenColor("border/default"): QColor(225, 225, 225); + const QColor gridMinor = dark ? tokenColor("bg/hover") : QColor(240, 240, 240); + const QColor zeroPen = dark ? tokenColor("border/strong") : QColor(180, 180, 180); + + plot->setCanvasBackground(QBrush(bg)); + plot->setAutoFillBackground(true); + QPalette pal = plot->palette(); + pal.setColor(QPalette::Window, bg); + pal.setColor(QPalette::WindowText, axisText); + pal.setColor(QPalette::Text, axisText); + plot->setPalette(pal); + + // 网格线 / 零线(line marker)按主题重着色——不动其它 item(散点/网格填充自带配色)。 + const QwtPlotItemList items = plot->itemList(); + for (QwtPlotItem* it : items) { + if (auto* g = dynamic_cast(it)) { + g->setMajorPen(gridMajor, 1.0, Qt::SolidLine); + g->setMinorPen(gridMinor, 1.0, Qt::DotLine); + } else if (auto* m = dynamic_cast(it)) { + if (m->lineStyle() != QwtPlotMarker::NoLine) m->setLinePen(zeroPen, 1.0); + } + } + plot->replot(); +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/ChartTheme.hpp b/src/app/panels/chart/ChartTheme.hpp new file mode 100644 index 0000000..81fb6e9 --- /dev/null +++ b/src/app/panels/chart/ChartTheme.hpp @@ -0,0 +1,14 @@ +#pragma once + +class QwtPlot; + +namespace geopro::app { + +// 按当前主题给 QwtPlot 套底色/轴文字/网格/零线配色,并 replot。 +// 浅色:与原版 web 图表一致(白底 + 深灰轴字)——保持 1:1,不动。 +// 暗色:改深色画布(bg/panel) + 浅色轴字,避免暗色主题下刺眼白底。 +// 网格线 / 零线标记按主题重新着色(遍历 plot 的 item 列表,无需各视图持有指针)。 +// 在视图构造末尾调用一次;并连 ThemeManager::changed 再调用以热切换。 +void applyChartPlotTheme(QwtPlot* plot); + +} // namespace geopro::app diff --git a/src/app/panels/chart/ColorBarWidget.cpp b/src/app/panels/chart/ColorBarWidget.cpp new file mode 100644 index 0000000..030fb04 --- /dev/null +++ b/src/app/panels/chart/ColorBarWidget.cpp @@ -0,0 +1,150 @@ +#include "panels/chart/ColorBarWidget.hpp" +#include +#include + +#include "Theme.hpp" + +namespace geopro::app { + +static constexpr int kBarHeight = 18; // 色带高度(px) +static constexpr int kTickHeight = 4; // 刻度短线高度(px) +static constexpr int kFontSize = 9; // 刻度字号 +static constexpr int kVBarWidth = 16; // 竖条色带宽度(px) +static constexpr int kVLabelGap = 4; // 竖条值标签与色带间距(px) + +ColorBarWidget::ColorBarWidget(QWidget* parent, Orientation orient) + : QWidget(parent), orient_(orient) { + if (orient_ == Orientation::Vertical) { + setFixedWidth(64); // 竖条 + 左侧值标签 + } else { + setFixedHeight(36); + } + // 主题热切换:底色/边框/刻度字跟随主题重绘(色带本身=数据色,不变)。 + connect(&ThemeManager::instance(), &ThemeManager::changed, this, + qOverload<>(&QWidget::update)); +} + +void ColorBarWidget::setColorScale(const core::ColorScale& scale) { + scale_ = scale; + update(); +} + +void ColorBarWidget::paintEvent(QPaintEvent* /*event*/) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing, false); + if (orient_ == Orientation::Vertical) { + paintVertical(p); + } else { + paintHorizontal(p); + } +} + +void ColorBarWidget::paintHorizontal(QPainter& p) { + const int W = width(); + const int H = height(); + // 浅色=白底深字(原版 1:1);暗色=深底浅字,避免刺眼白条。色带格(数据色)两者不变。 + const bool dark = isDarkTheme(); + const QColor bgColor = dark ? tokenColor("bg/panel") : QColor(Qt::white); + const QColor borderColor = dark ? tokenColor("border/strong") : QColor(120, 120, 120); + const QColor textColor = dark ? tokenColor("text/secondary"): QColor(60, 60, 60); + p.fillRect(0, 0, W, H, bgColor); + + auto stops = scale_.stops(); + if (stops.size() < 2) return; + + const int barY = 2; + const int barH = kBarHeight; + // 居中约 74% 宽(对齐原版:色阶条不占满,两侧各留 ~13% 边距)。 + const int marginX = static_cast(W * 0.13); + const int barLeft = marginX; + const int barRight = W - marginX; + const int barW = barRight - barLeft; + if (barW < 10) return; + // 等宽色带:每相邻断点一格,等宽(对齐原版图例,非按值比例)。 + const int nSeg = static_cast(stops.size()) - 1; + auto segX = [&](int i) { return barLeft + static_cast(static_cast(i) / nSeg * barW); }; + + for (int i = 0; i < nSeg; ++i) { + int xL = segX(i), xR = segX(i + 1); + if (xR <= xL) xR = xL + 1; + const auto& c = stops[i].second; + p.fillRect(xL, barY, xR - xL, barH, QColor(c.r, c.g, c.b, c.a)); + } + // 外边框 + p.setPen(QPen(borderColor, 1)); + p.drawRect(barLeft, barY, barW - 1, barH - 1); + + // 边界值标签(随主题:浅色深字 / 暗色浅字),在各分格边界下方。 + QFont font = p.font(); + font.setPixelSize(kFontSize); + p.setFont(font); + p.setPen(textColor); + QFontMetrics fm(font); + const int tickY = barY + barH + 1; + for (int i = 0; i <= nSeg; ++i) { + int x = segX(i); + p.drawLine(x, tickY, x, tickY + kTickHeight); + const QString label = QString::number(stops[i].first, 'g', 4); + int tw = fm.horizontalAdvance(label); + int tx = x - tw / 2; + if (tx < 0) tx = 0; + if (tx + tw > W) tx = W - tw; + p.drawText(tx, H - 2, label); + } +} + +void ColorBarWidget::paintVertical(QPainter& p) { + const int W = width(); + const int H = height(); + const bool dark = isDarkTheme(); + const QColor bgColor = dark ? tokenColor("bg/panel") : QColor(Qt::white); + const QColor borderColor = dark ? tokenColor("border/strong") : QColor(120, 120, 120); + const QColor textColor = dark ? tokenColor("text/secondary"): QColor(60, 60, 60); + p.fillRect(0, 0, W, H, bgColor); + + auto stops = scale_.stops(); + if (stops.size() < 2) return; + + QFont font = p.font(); + font.setPixelSize(kFontSize); + p.setFont(font); + QFontMetrics fm(font); + + // 竖条靠右;左侧留出值标签宽度。上下各留半行高,避免顶/底标签出界。 + const int halfLine = fm.height() / 2 + 1; + const int barRight = W - 2; + const int barLeft = barRight - kVBarWidth; + const int barTop = halfLine; + const int barBottom = H - halfLine; + const int barH = barBottom - barTop; + if (barH < 10 || barLeft < 0) return; + + // 等高色带:每相邻断点一格,最大值在顶、最小值在底(与原版 1323→0 自上而下一致)。 + const int nSeg = static_cast(stops.size()) - 1; + // segY(i): 第 i 个边界的 y(i=0 为最小值在底,i=nSeg 为最大值在顶)。 + auto segY = [&](int i) { + return barBottom - static_cast(static_cast(i) / nSeg * barH); + }; + for (int i = 0; i < nSeg; ++i) { + int yB = segY(i), yT = segY(i + 1); + if (yT >= yB) yT = yB - 1; + const auto& c = stops[i].second; // 段 [stop_i, stop_{i+1}) 取 stop_i 色(与离散一致) + p.fillRect(barLeft, yT, kVBarWidth, yB - yT, QColor(c.r, c.g, c.b, c.a)); + } + // 外边框 + p.setPen(QPen(borderColor, 1)); + p.drawRect(barLeft, barTop, kVBarWidth - 1, barH - 1); + + // 边界值标签(色带左侧,垂直居中于各边界)。 + p.setPen(textColor); + for (int i = 0; i <= nSeg; ++i) { + int y = segY(i); + const QString label = QString::number(stops[i].first, 'f', 2); + int tw = fm.horizontalAdvance(label); + int tx = barLeft - kVLabelGap - tw; + if (tx < 0) tx = 0; + p.drawText(tx, y + fm.ascent() / 2 - 1, label); + } +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/ColorBarWidget.hpp b/src/app/panels/chart/ColorBarWidget.hpp new file mode 100644 index 0000000..b90c857 --- /dev/null +++ b/src/app/panels/chart/ColorBarWidget.hpp @@ -0,0 +1,32 @@ +#pragma once +#include "model/ColorScale.hpp" +#include + +namespace geopro::app { + +// 独立色阶条 Widget,作为 QwtPlot 的兄弟 widget,不进入 Qwt 坐标系, +// 因此不随图表缩放/平移移动,也不与轴标注重叠。 +// Horizontal(默认,反演原数据):底部横条,等宽色带 + 下方边界刻度值。 +// Vertical(measurement):右侧竖条,等高色带,最大值在顶、最小值在底, +// 边界值标在色带左侧(对齐原版 Plotly 离散图例)。 +class ColorBarWidget : public QWidget { + Q_OBJECT +public: + enum class Orientation { Horizontal, Vertical }; + + explicit ColorBarWidget(QWidget* parent = nullptr, Orientation orient = Orientation::Horizontal); + + void setColorScale(const core::ColorScale& scale); + +protected: + void paintEvent(QPaintEvent* event) override; + +private: + void paintHorizontal(QPainter& p); + void paintVertical(QPainter& p); + + core::ColorScale scale_; + Orientation orient_; +}; + +} // namespace geopro::app diff --git a/src/app/panels/chart/ColorMapService.cpp b/src/app/panels/chart/ColorMapService.cpp new file mode 100644 index 0000000..e316468 --- /dev/null +++ b/src/app/panels/chart/ColorMapService.cpp @@ -0,0 +1,83 @@ +#include "panels/chart/ColorMapService.hpp" +#include +#include + +namespace geopro::app { + +namespace { +inline double clamp01(double v) { + return v < 0.0 ? 0.0 : (v > 1.0 ? 1.0 : v); +} +inline unsigned char lerpByte(unsigned char a, unsigned char b, double t) { + return static_cast(a + (b - a) * t + 0.5); +} +} // namespace + +ColorMapService::ColorMapService(const core::ColorScale& scale) + : scale_(scale) { + auto raw = scale.stops(); + if (raw.empty()) { + minVal_ = 0.0; + maxVal_ = 1.0; + dataMin_ = minVal_; + dataMax_ = maxVal_; + return; + } + minVal_ = raw.front().first; + maxVal_ = raw.back().first; + dataMin_ = minVal_; // 默认数据范围 = 断点范围;setDataRange 可改为数据 min/max(cauto) + dataMax_ = maxVal_; + double range = maxVal_ - minVal_; + normStops_.reserve(raw.size()); + for (const auto& [val, color] : raw) { + double pos = (range > 0.0) ? (val - minVal_) / range : 0.0; + normStops_.push_back({clamp01(pos), color}); + } +} + +void ColorMapService::setDataRange(double dataMin, double dataMax) { + dataMin_ = dataMin; + dataMax_ = dataMax; +} + +double ColorMapService::normalized(double v) const { + double range = dataMax_ - dataMin_; + if (range <= 0.0) return 0.0; + return clamp01((v - dataMin_) / range); +} + +core::Rgba ColorMapService::colorAtContinuous(double v) const { + if (normStops_.empty()) return core::Rgba{0, 0, 0, 255}; + if (normStops_.size() == 1) return normStops_.front().color; + + double t = normalized(v); + // 非有限值(NaN/Inf,可能来自降级后端的脏数据或退化数据范围):回退首断点色, + // 避免下方 upper_bound 用 NaN 比较返回 end() 后解引用越界(崩溃)。 + if (!std::isfinite(t)) return normStops_.front().color; + + // 找到 t 落在哪两个 normStop 之间 + if (t <= normStops_.front().pos) return normStops_.front().color; + if (t >= normStops_.back().pos) return normStops_.back().color; + + // 二分查找第一个 pos > t + auto it = std::upper_bound(normStops_.begin(), normStops_.end(), t, + [](double val, const NormStop& s) { return val < s.pos; }); + + const NormStop& hi = *it; + const NormStop& lo = *(it - 1); + double segLen = hi.pos - lo.pos; + double frac = (segLen > 0.0) ? (t - lo.pos) / segLen : 0.0; + + return core::Rgba{ + lerpByte(lo.color.r, hi.color.r, frac), + lerpByte(lo.color.g, hi.color.g, frac), + lerpByte(lo.color.b, hi.color.b, frac), + lerpByte(lo.color.a, hi.color.a, frac) + }; +} + +core::Rgba ColorMapService::colorAtDiscrete(double v) const { + return scale_.colorAt(v); +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/ColorMapService.hpp b/src/app/panels/chart/ColorMapService.hpp new file mode 100644 index 0000000..f686851 --- /dev/null +++ b/src/app/panels/chart/ColorMapService.hpp @@ -0,0 +1,45 @@ +#pragma once +#include "model/ColorScale.hpp" +#include +#include + +namespace geopro::app { + +// 连续/离散双模式色阶服务,供散点渲染(连续插值)和网格/图例(阶梯)使用。 +// 连续模式:按归一化位置在相邻断点之间线性插值 RGB,与原版 Plotly colorscale 一致。 +class ColorMapService { +public: + explicit ColorMapService(const core::ColorScale& scale); + + // 设置数据值归一化范围(散点用,对齐原版 Plotly cauto=数据 min/max)。 + // 与色阶形状(断点位置,按断点值范围)解耦:色阶位置不变,只改输入值→[0,1] 的映射。 + // 默认(不调用)数据范围 = 断点值范围(首/末断点)。 + void setDataRange(double dataMin, double dataMax); + + // 将数据值归一化到 [0,1](按数据范围,默认=断点值范围),超范围 clamp。 + double normalized(double v) const; + + // 连续插值取色(散点用):按断点位置线性插值 RGB。 + core::Rgba colorAtContinuous(double v) const; + + // 离散阶梯取色(网格/图例用):复用 ColorScale::colorAt。 + core::Rgba colorAtDiscrete(double v) const; + + // 返回原始 ColorScale 引用(色阶条图例用)。 + const core::ColorScale& scale() const { return scale_; } + +private: + core::ColorScale scale_; + // 归一化位置与颜色预计算缓存(stops 已升序)。 + struct NormStop { + double pos; // normalized position in [0,1] + core::Rgba color; + }; + std::vector normStops_; + double minVal_; // 断点值范围下界(色阶形状用) + double maxVal_; // 断点值范围上界 + double dataMin_; // 数据归一化下界(默认=minVal_,setDataRange 可改) + double dataMax_; // 数据归一化上界(默认=maxVal_) +}; + +} // namespace geopro::app diff --git a/src/app/panels/chart/ContourPlotItem.cpp b/src/app/panels/chart/ContourPlotItem.cpp new file mode 100644 index 0000000..663bd61 --- /dev/null +++ b/src/app/panels/chart/ContourPlotItem.cpp @@ -0,0 +1,281 @@ +#include "panels/chart/ContourPlotItem.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "panels/chart/ColorMapService.hpp" + +namespace geopro::app { + +namespace { +constexpr int kFillUpsample = 4; // 填充图像每网格格细分 K(双线性插值平滑带边界) +constexpr int kMaxFillDim = 2400; // 填充图像单边像素上限(防极端网格爆内存) +constexpr int kLabelFontPx = 10; // 等值线标注字号 +constexpr double kLabelMinLenPx = 24.0; // 整条线像素长度小于此不标注(极短碎线) +constexpr double kRad2Deg = 57.29577951308232; // 180/π(避免依赖 M_PI) +} // namespace + +ContourPlotItem::ContourPlotItem() : QwtPlotItem() { + setRenderHint(QwtPlotItem::RenderAntialiased, false); + // 网格数据 x 轴在底部(与 RawDataChartView 的顶部 x 轴不同);y 轴在左。 + setXAxis(QwtPlot::xBottom); + setYAxis(QwtPlot::yLeft); +} + +void ContourPlotItem::setData(const core::Grid& g, ColorMapService* svc, + const std::vector& anoms, bool showLines, + bool showLabels) { + showLines_ = showLines; + showLabels_ = showLabels; + anoms_ = anoms; + + const int nx = g.nx(), ny = g.ny(); + if (nx < 2 || ny < 2 || static_cast(g.x.size()) < nx || + static_cast(g.y.size()) < ny) { + fillImage_ = QImage(); + dataBBox_ = QRectF(); + lines_.clear(); + return; + } + + const double xmin = g.x.front(), xmax = g.x.back(); + const double ymin = g.y.front(), ymax = g.y.back(); + dataBBox_ = QRectF(xmin, ymin, xmax - xmin, ymax - ymin); + + buildFillImage(g, svc); + + // 等值线(矢量):复用 render 管线,仅取 lines(填充走栅格)。 + if (showLines_) { + render::ContourOptions opt; + opt.upsample = 2; + opt.makeLines = true; + auto res = render::buildContourBands(g, svc->scale(), opt); + lines_ = std::move(res.lines); + // buildContourBands 当前未回填 level(恒 0);在此按线上代表点采网格值并吸附到最近色阶级, + // 使标注显示真实等值线值。 + resolveLineLevels(g, svc->scale()); + } else { + lines_.clear(); + } +} + +void ContourPlotItem::resolveLineLevels(const core::Grid& g, const core::ColorScale& cs) { + const auto stops = cs.stopValues(); + if (stops.empty() || lines_.empty()) return; + + const int nx = g.nx(), ny = g.ny(); + const double xmin = g.x.front(), xmax = g.x.back(); + const double ymin = g.y.front(), ymax = g.y.back(); + const double xspan = (xmax - xmin), yspan = (ymax - ymin); + + // 在数据坐标点做双线性采样(NaN 安全)。 + auto sampleAt = [&](const core::Vec2& p) -> double { + if (xspan <= 0 || yspan <= 0) return std::nan(""); + double fi = (p.x - xmin) / xspan * (nx - 1); + double fj = (p.y - ymin) / yspan * (ny - 1); + fi = std::clamp(fi, 0.0, static_cast(nx - 1)); + fj = std::clamp(fj, 0.0, static_cast(ny - 1)); + int i0 = std::min(static_cast(fi), nx - 2); + int j0 = std::min(static_cast(fj), ny - 2); + double ti = fi - i0, tj = fj - j0; + double v00 = g.valueAt(i0, j0), v10 = g.valueAt(i0 + 1, j0); + double v01 = g.valueAt(i0, j0 + 1), v11 = g.valueAt(i0 + 1, j0 + 1); + if (std::isnan(v00) || std::isnan(v10) || std::isnan(v01) || std::isnan(v11)) + return std::nan(""); + return (v00 * (1 - ti) + v10 * ti) * (1 - tj) + (v01 * (1 - ti) + v11 * ti) * tj; + }; + + for (auto& ln : lines_) { + double sampled = std::nan(""); + for (const auto& p : ln.pts) { // 取首个非 NaN 采样点 + sampled = sampleAt(p); + if (!std::isnan(sampled)) break; + } + if (std::isnan(sampled)) { + ln.level = std::nan(""); + continue; + } + // 吸附到最近色阶级(等值线恰落在某级上)。 + double best = stops.front(); + double bestD = std::fabs(sampled - best); + for (double s : stops) { + double d = std::fabs(sampled - s); + if (d < bestD) { bestD = d; best = s; } + } + ln.level = best; + } +} + +void ContourPlotItem::buildFillImage(const core::Grid& g, ColorMapService* svc) { + const int nx = g.nx(), ny = g.ny(); + int W = (nx - 1) * kFillUpsample + 1; + int H = (ny - 1) * kFillUpsample + 1; + // 限幅:极端网格下按**统一比例**降采样(W/H 独立截断会使采样非均匀→内容横/纵失真)。 + if (W > kMaxFillDim || H > kMaxFillDim) { + const double s = std::min(static_cast(kMaxFillDim) / W, + static_cast(kMaxFillDim) / H); + W = std::max(2, static_cast(W * s)); + H = std::max(2, static_cast(H * s)); + } + + QImage img(W, H, QImage::Format_ARGB32); + img.fill(Qt::transparent); + + // 每像素 → 归一化网格坐标 (fi,fj) → 四邻格双线性插值;任一邻格 NaN 则该像素透明。 + for (int py = 0; py < H; ++py) { + // 图像顶行 py=0 对应 y 最大(ymax,地表);底行对应 y 最小(最深)。 + double fj = static_cast(H - 1 - py) / (H - 1) * (ny - 1); + int j0 = std::min(static_cast(fj), ny - 2); + double tj = fj - j0; + auto* scan = reinterpret_cast(img.scanLine(py)); + for (int px = 0; px < W; ++px) { + double fi = static_cast(px) / (W - 1) * (nx - 1); + int i0 = std::min(static_cast(fi), nx - 2); + double ti = fi - i0; + + double v00 = g.valueAt(i0, j0), v10 = g.valueAt(i0 + 1, j0); + double v01 = g.valueAt(i0, j0 + 1), v11 = g.valueAt(i0 + 1, j0 + 1); + if (std::isnan(v00) || std::isnan(v10) || std::isnan(v01) || std::isnan(v11)) + continue; // 含无数据格 → 像素透明(不规则白边) + + double v = (v00 * (1 - ti) + v10 * ti) * (1 - tj) + + (v01 * (1 - ti) + v11 * ti) * tj; + auto c = svc->colorAtDiscrete(v); // 离散色带 → 平滑填充带边界 + scan[px] = qRgba(c.r, c.g, c.b, c.a ? c.a : 255); + } + } + fillImage_ = std::move(img); +} + +QRectF ContourPlotItem::boundingRect() const { + return dataBBox_; +} + +void ContourPlotItem::draw(QPainter* painter, const QwtScaleMap& xMap, const QwtScaleMap& yMap, + const QRectF& /*canvasRect*/) const { + if (dataBBox_.isNull()) return; + + const double xmin = dataBBox_.left(), xmax = dataBBox_.right(); + const double ymin = dataBBox_.top(), ymax = dataBBox_.bottom(); + + // 1) 填充:数据 bbox → 像素矩形(注意 y 翻转:ymax→画布上沿、ymin→下沿),blit + 平滑缩放。 + if (!fillImage_.isNull()) { + const double pxL = xMap.transform(xmin); + const double pxR = xMap.transform(xmax); + const double pyTop = yMap.transform(ymax); + const double pyBot = yMap.transform(ymin); + QRectF target(pxL, pyTop, pxR - pxL, pyBot - pyTop); + painter->save(); + painter->setRenderHint(QPainter::SmoothPixmapTransform, true); + painter->drawImage(target, fillImage_); + painter->restore(); + } + + auto mapPt = [&](const core::Vec2& p) { + return QPointF(xMap.transform(p.x), yMap.transform(p.y)); + }; + + // 2) 等值线:黑色 0 宽(cosmetic)细线。 + if (showLines_ && !lines_.empty()) { + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + QPen pen(QColor(0, 0, 0)); + pen.setWidthF(1.0); // 1px 黑色等值线 + painter->setPen(pen); + for (const auto& ln : lines_) { + if (ln.pts.size() < 2) continue; + QPolygonF poly; + poly.reserve(static_cast(ln.pts.size())); + for (const auto& p : ln.pts) poly << mapPt(p); + painter->drawPolyline(poly); + } + painter->restore(); + + // 3) 标注:每条等值线只标一个(对齐原版),放弧长中点,随该处方向旋转。 + if (showLabels_) { + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + QFont f = painter->font(); + f.setPixelSize(kLabelFontPx); + painter->setFont(f); + painter->setPen(QColor(0, 0, 0)); + const QFontMetricsF fm(f); + for (const auto& ln : lines_) { + if (ln.pts.size() < 2 || std::isnan(ln.level)) continue; + // 映射到像素 + 累计像素弧长。 + std::vector px; + px.reserve(ln.pts.size()); + for (const auto& p : ln.pts) px.push_back(mapPt(p)); + double total = 0.0; + for (std::size_t i = 1; i < px.size(); ++i) + total += std::hypot(px[i].x() - px[i - 1].x(), px[i].y() - px[i - 1].y()); + if (total < kLabelMinLenPx) continue; // 极短碎线不标注 + const QString txt = QString::number(ln.level, 'g', 4); + const double halfW = fm.horizontalAdvance(txt) * 0.5; + const double targetAt = total * 0.5; // 弧长中点 + double acc = 0.0; + for (std::size_t i = 1; i < px.size(); ++i) { + const double seg = + std::hypot(px[i].x() - px[i - 1].x(), px[i].y() - px[i - 1].y()); + if (acc + seg >= targetAt || i == px.size() - 1) { + const double t = seg > 1e-6 ? (targetAt - acc) / seg : 0.0; + const QPointF pos(px[i - 1].x() + (px[i].x() - px[i - 1].x()) * t, + px[i - 1].y() + (px[i].y() - px[i - 1].y()) * t); + double ang = + std::atan2(px[i].y() - px[i - 1].y(), px[i].x() - px[i - 1].x()) * + kRad2Deg; + if (ang > 90.0) ang -= 180.0; // 文字大体正向 + if (ang < -90.0) ang += 180.0; + painter->save(); + painter->translate(pos); + painter->rotate(ang); + painter->drawText(QPointF(-halfW, -2), txt); + painter->restore(); + break; // 只标一个 + } + acc += seg; + } + } + painter->restore(); + } + } + + // 4) 异常叠加:点=小方块、线=折线、面=闭合多边形;颜色用 lineColor,dashed→虚线。 + if (showAnomalies_ && !anoms_.empty()) { + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + painter->setBrush(Qt::NoBrush); + for (const auto& a : anoms_) { + if (a.localPts.empty()) continue; + QColor col(QString::fromStdString(a.lineColor)); + if (!col.isValid()) col = QColor(0, 0, 0); + QPen pen(col); + pen.setWidthF(a.lineWidth > 0 ? a.lineWidth : 1.0); + pen.setStyle(a.dashed ? Qt::DashLine : Qt::SolidLine); + painter->setPen(pen); + + if (a.markType == core::AnomalyMarkType::Point) { + const QPointF c = mapPt(a.localPts.front()); + painter->drawRect(QRectF(c.x() - 3, c.y() - 3, 6, 6)); + } else { + QPolygonF poly; + poly.reserve(static_cast(a.localPts.size())); + for (const auto& p : a.localPts) poly << mapPt(p); + if (a.markType == core::AnomalyMarkType::Polygon) + painter->drawPolygon(poly); // 闭合 + else + painter->drawPolyline(poly); + } + } + painter->restore(); + } +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/ContourPlotItem.hpp b/src/app/panels/chart/ContourPlotItem.hpp new file mode 100644 index 0000000..5052773 --- /dev/null +++ b/src/app/panels/chart/ContourPlotItem.hpp @@ -0,0 +1,58 @@ +#pragma once +#include + +#include +#include +#include + +#include "model/Anomaly.hpp" +#include "model/Field.hpp" +#include "ContourBands.hpp" + +class QPainter; +class QwtScaleMap; + +namespace geopro::app { + +class ColorMapService; + +// 网格等值线图项(QwtPlotItem,非 Q_OBJECT): +// - 填充:预渲染 QImage 热力图(双线性插值 + 离散色带取色),draw 时 blit+平滑缩放 +// (避免 banded 多边形数万顶点导致拖动卡顿);含 NaN 的像素透明 → 不规则白边。 +// - 等值线:buildContourBands 返回的矢量折线(黑细线),随轴变换映射后 drawPolyline。 +// - 标注:沿线方向旋转的 level 数值(小字黑)。 +// - 异常叠加:按 markType 画 点(方块)/线(折线)/面(闭合多边形),dashed→虚线。 +class ContourPlotItem : public QwtPlotItem { +public: + ContourPlotItem(); + + // 构建填充图像 + 缓存等值线/异常。svc 不被拥有(由 GridDataChartView 持有)。 + void setData(const core::Grid& g, ColorMapService* svc, + const std::vector& anoms, bool showLines, bool showLabels); + + void setShowLines(bool on) { showLines_ = on; } + void setShowLabels(bool on) { showLabels_ = on; } + void setShowAnomalies(bool on) { showAnomalies_ = on; } + + int rtti() const override { return QwtPlotItem::Rtti_PlotUserItem; } + + QRectF boundingRect() const override; + + void draw(QPainter* painter, const QwtScaleMap& xMap, const QwtScaleMap& yMap, + const QRectF& canvasRect) const override; + +private: + void buildFillImage(const core::Grid& g, ColorMapService* svc); + void resolveLineLevels(const core::Grid& g, const core::ColorScale& cs); + + QImage fillImage_; // 预渲染填充热力图(ARGB32,含透明无数据区) + QRectF dataBBox_; // 数据包围盒(x[xmin,xmax] y[ymin,ymax]) + std::vector lines_; // 矢量等值线(含 level) + std::vector anoms_; // 异常叠加 + + bool showLines_ = true; + bool showLabels_ = true; + bool showAnomalies_ = true; +}; + +} // namespace geopro::app diff --git a/src/app/panels/chart/DataTableView.cpp b/src/app/panels/chart/DataTableView.cpp new file mode 100644 index 0000000..410edf0 --- /dev/null +++ b/src/app/panels/chart/DataTableView.cpp @@ -0,0 +1,167 @@ +#include "panels/chart/DataTableView.hpp" + +#include +#include +#include +#include + +#include "panels/chart/TablePager.hpp" + +namespace geopro::app { + +namespace { +// 药丸开关配色(原版蓝色 ON)。 +const QColor kSwitchOn(64, 158, 255); // 蓝色(ON/可见) +const QColor kSwitchOff(190, 190, 190); // 灰色(OFF/隐藏) +constexpr int kSwitchW = 36; // 轨道宽 +constexpr int kSwitchH = 18; // 轨道高(=直径) +constexpr int kKnobMargin = 2; // 旋钮与轨道边距 +constexpr int kToggleColW = 80; // Toggle 列固定窄宽(开关 36 + 两侧留白) +} // namespace + +TablePayloadModel::TablePayloadModel(QObject* parent) : QAbstractTableModel(parent) {} + +void TablePayloadModel::setPayload(const geopro::core::TablePayload& payload) { + beginResetModel(); + payload_ = payload; + endResetModel(); +} + +int TablePayloadModel::rowCount(const QModelIndex& parent) const { + if (parent.isValid()) return 0; + return static_cast(payload_.rows.size()); +} + +int TablePayloadModel::columnCount(const QModelIndex& parent) const { + if (parent.isValid()) return 0; + return static_cast(payload_.columns.size()); +} + +QVariant TablePayloadModel::data(const QModelIndex& index, int role) const { + if (!index.isValid()) return {}; + const size_t r = static_cast(index.row()); + const size_t c = static_cast(index.column()); + if (r >= payload_.rows.size() || c >= payload_.rows[r].size()) return {}; + if (role == Qt::TextAlignmentRole) + return static_cast(Qt::AlignCenter); // 单元内容水平+垂直居中 + if (role == Qt::DisplayRole) return payload_.rows[r][c]; + return {}; +} + +QVariant TablePayloadModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (role != Qt::DisplayRole) return {}; + if (orientation == Qt::Horizontal) { + if (section < 0 || section >= static_cast(payload_.columns.size())) return {}; + return payload_.columns[static_cast(section)].title; + } + return section + 1; // 行号 +} + +geopro::core::TableColumnKind TablePayloadModel::columnKind(int column) const { + if (column < 0 || column >= static_cast(payload_.columns.size())) + return geopro::core::TableColumnKind::Text; + return payload_.columns[static_cast(column)].kind; +} + +ToggleSwitchDelegate::ToggleSwitchDelegate(const TablePayloadModel* model, QObject* parent) + : QStyledItemDelegate(parent), model_(model) {} + +void ToggleSwitchDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const { + if (!model_ || model_->columnKind(index.column()) != geopro::core::TableColumnKind::Toggle) { + QStyledItemDelegate::paint(painter, option, index); + return; + } + // 画背景(选中态/交替行),但不画文本。 + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + opt.text.clear(); + if (const QWidget* w = opt.widget) + w->style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter, w); + + const bool on = index.data(Qt::DisplayRole).toString() == QLatin1String("1"); + + // 居中药丸开关。 + const QRect cell = option.rect; + QRectF track(cell.center().x() - kSwitchW / 2.0, cell.center().y() - kSwitchH / 2.0, + kSwitchW, kSwitchH); + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + painter->setPen(Qt::NoPen); + painter->setBrush(on ? kSwitchOn : kSwitchOff); + painter->drawRoundedRect(track, kSwitchH / 2.0, kSwitchH / 2.0); + // 白色旋钮(ON 靠右,OFF 靠左)。 + const double d = kSwitchH - 2.0 * kKnobMargin; + const double knobX = on ? track.right() - kKnobMargin - d : track.left() + kKnobMargin; + painter->setBrush(Qt::white); + painter->drawEllipse(QRectF(knobX, track.top() + kKnobMargin, d, d)); + painter->restore(); +} + +QSize ToggleSwitchDelegate::sizeHint(const QStyleOptionViewItem& option, + const QModelIndex& index) const { + if (!model_ || model_->columnKind(index.column()) != geopro::core::TableColumnKind::Toggle) + return QStyledItemDelegate::sizeHint(option, index); + // 贴合开关的窄宽(+两侧内边距),高度沿用默认行高,避免 ResizeToContents 撑宽该列。 + return QSize(kToggleColW, kSwitchH + 6); +} + +DataTableView::DataTableView(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); + lay->setContentsMargins(0, 0, 0, 0); + lay->setSpacing(0); + + model_ = new TablePayloadModel(this); + table_ = new QTableView(this); + table_->setModel(model_); + table_->setAlternatingRowColors(true); + table_->setEditTriggers(QAbstractItemView::NoEditTriggers); + table_->setSelectionBehavior(QAbstractItemView::SelectRows); + // 列宽策略在 setPayload 按列种类设置(Toggle 列固定窄宽;文本列均分 Stretch)。 + // 不强制拉伸末列——末列通常是窄的 Toggle 列,拉伸会把它撑得过宽。 + table_->horizontalHeader()->setStretchLastSection(false); + table_->horizontalHeader()->setDefaultAlignment(Qt::AlignCenter); // 表头文本居中 + table_->verticalHeader()->setDefaultSectionSize(24); + table_->verticalHeader()->setVisible(false); // 原版无行号列 + + // Toggle 列委托:把“隐藏/显示”列画成蓝色药丸开关。 + table_->setItemDelegate(new ToggleSwitchDelegate(model_, table_)); + + lay->addWidget(table_); + + // 分页器(默认隐藏;分页型载荷 pageSize>0 时显示)。转发翻页请求给壳。 + pager_ = new TablePager(this); + pager_->hide(); + connect(pager_, &TablePager::pageRequested, this, &DataTableView::pageRequested); + lay->addWidget(pager_); +} + +void DataTableView::setPayload(const QVariant& payload) { + if (!payload.canConvert()) return; // 坏/空 → 空态 + const auto t = payload.value(); + model_->setPayload(t); + + // 列宽策略:文本列等宽均分,Toggle 列窄。 + // Toggle 列 → Fixed 固定窄宽(贴合开关 + 留白),开关居中。 + // 其余文本列 → Stretch(均分可用宽度,整体均衡,无单一巨列、无右侧空白)。 + auto* header = table_->horizontalHeader(); + for (size_t i = 0; i < t.columns.size(); ++i) { + const int col = static_cast(i); + if (t.columns[i].kind == geopro::core::TableColumnKind::Toggle) { + header->setSectionResizeMode(col, QHeaderView::Fixed); + table_->setColumnWidth(col, kToggleColW); + } else { + header->setSectionResizeMode(col, QHeaderView::Stretch); + } + } + + // 分页器:分页型载荷(pageSize>0,dd_grid)显示并同步状态;否则隐藏(全量列表)。 + if (t.pageSize > 0) { + pager_->setState(t.total, t.pageNo, t.pageSize); + pager_->show(); + } else { + pager_->hide(); + } +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/DataTableView.hpp b/src/app/panels/chart/DataTableView.hpp new file mode 100644 index 0000000..d0eaaa5 --- /dev/null +++ b/src/app/panels/chart/DataTableView.hpp @@ -0,0 +1,69 @@ +#pragma once +#include +#include +#include +#include "model/detail/DetailPayloads.hpp" +#include "panels/chart/IDetailView.hpp" + +class QTableView; + +namespace geopro::app { + +// TablePayload 驱动的只读表模型(列标题来自 TableColumn,单元为预格式化 QString)。 +class TablePayloadModel : public QAbstractTableModel { + Q_OBJECT +public: + explicit TablePayloadModel(QObject* parent = nullptr); + void setPayload(const geopro::core::TablePayload& payload); + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + // 列渲染种类(供委托判断是否画开关)。越界返回 Text。 + geopro::core::TableColumnKind columnKind(int column) const; + +private: + geopro::core::TablePayload payload_; +}; + +// Toggle 列委托:把单元值("1"=ON/"0"=OFF)画成蓝色药丸开关(仅展示状态,暂不联动散点)。 +// Text 列回退到默认绘制。 +class ToggleSwitchDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + explicit ToggleSwitchDelegate(const TablePayloadModel* model, QObject* parent = nullptr); + void paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const override; + // Toggle 列返回贴合开关的窄尺寸(供 ResizeToContents 不至于撑宽);其余回退默认。 + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; + +private: + const TablePayloadModel* model_; // 不拥有 +}; + +class TablePager; + +// 通用数据列表视图:IDetailView + QTableView(+ 分页型载荷时底部 TablePager 分页器)。 +// measurement/grid/trajectory 列表共用。载荷 pageSize>0(dd_grid)时显示分页器并转发翻页请求; +// 否则隐藏分页器(全量列表)。 +class DataTableView : public QWidget, public IDetailView { + Q_OBJECT +public: + explicit DataTableView(QWidget* parent = nullptr); + + QWidget* widget() override { return this; } + void setPayload(const QVariant& payload) override; // 坏/空 variant → 保持空态不崩 + +signals: + // 分页器请求加载某页(翻页/跳页/改每页条数)。壳据此触发控制器 loadTabPaged。 + void pageRequested(int pageNo, int pageSize); + +private: + QTableView* table_; + TablePayloadModel* model_; + TablePager* pager_; // 分页器(pageSize>0 时显示,否则隐藏) +}; + +} // namespace geopro::app diff --git a/src/app/panels/chart/DetailViewFactory.cpp b/src/app/panels/chart/DetailViewFactory.cpp new file mode 100644 index 0000000..5185893 --- /dev/null +++ b/src/app/panels/chart/DetailViewFactory.cpp @@ -0,0 +1,36 @@ +#include "panels/chart/DetailViewFactory.hpp" + +#include + +#include "panels/chart/BarChartView.hpp" +#include "panels/chart/DataTableView.hpp" +#include "panels/chart/GridDataChartView.hpp" +#include "panels/chart/LineChartView.hpp" +#include "panels/chart/RawDataChartView.hpp" +#include "panels/chart/TrajectoryMapView.hpp" + +namespace geopro::app { + +std::unique_ptr makeDetailView(controller::ViewKind kind, QWidget* parent) { + switch (kind) { + case controller::ViewKind::Scatter: + return std::unique_ptr(new RawDataChartView(parent)); + case controller::ViewKind::FilledContour: + return std::unique_ptr(new GridDataChartView(parent)); + case controller::ViewKind::Table: + return std::unique_ptr(new DataTableView(parent)); + case controller::ViewKind::Bar: + return std::unique_ptr(new BarChartView(parent)); + case controller::ViewKind::LineProfile: + return std::unique_ptr(new LineChartView(parent)); + case controller::ViewKind::WebMap: + // trajectory 地图:QWebEngineView + Leaflet + 天地图瓦片,电极经纬点画橙色空心圈并取景。 + return std::unique_ptr(new TrajectoryMapView(parent)); + case controller::ViewKind::PolylineMap: + // 后续阶段补:PolylineMap。 + throw std::runtime_error("makeDetailView: ViewKind not yet implemented"); + } + throw std::runtime_error("makeDetailView: unknown ViewKind"); +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/DetailViewFactory.hpp b/src/app/panels/chart/DetailViewFactory.hpp new file mode 100644 index 0000000..98464b0 --- /dev/null +++ b/src/app/panels/chart/DetailViewFactory.hpp @@ -0,0 +1,16 @@ +#pragma once +#include +#include "DatasetDetailTab.hpp" // geopro::controller::ViewKind + +class QWidget; + +namespace geopro::app { + +class IDetailView; + +// 按 render kind 造详情视图。E1b 仅支持 Scatter / FilledContour(反演两页签); +// Bar/LineProfile/PolylineMap/Table 留待后续阶段(measurement/trajectory/grid)补, +// 现阶段命中会抛 std::runtime_error(明确失败,而非静默空指针)。 +std::unique_ptr makeDetailView(controller::ViewKind kind, QWidget* parent); + +} // namespace geopro::app diff --git a/src/app/panels/chart/ErtInversionStrategy.hpp b/src/app/panels/chart/ErtInversionStrategy.hpp new file mode 100644 index 0000000..62a5ffa --- /dev/null +++ b/src/app/panels/chart/ErtInversionStrategy.hpp @@ -0,0 +1,17 @@ +#pragma once +#include +#include "IDatasetChartStrategy.hpp" // geopro::controller(geopro_controller PUBLIC include) +namespace geopro::app { +// ERT 反演策略:散点(原数据,同步) + 网格等值面(网格数据,懒加载) 两页签。 +struct ErtInversionStrategy : controller::IDatasetChartStrategy { + std::string ddCode() const override { return "dd_inversion_data"; } + std::vector tabs() const override { + return { + {QStringLiteral("原数据"), controller::ViewKind::Scatter, + QStringLiteral("inversion.scatter"), /*lazy*/ false, /*paginated*/ false}, + {QStringLiteral("网格数据"), controller::ViewKind::FilledContour, + QStringLiteral("inversion.grid"), /*lazy*/ true, /*paginated*/ false}, + }; + } +}; +} // namespace geopro::app diff --git a/src/app/panels/chart/GrMeasurementStrategy.hpp b/src/app/panels/chart/GrMeasurementStrategy.hpp new file mode 100644 index 0000000..0d21849 --- /dev/null +++ b/src/app/panels/chart/GrMeasurementStrategy.hpp @@ -0,0 +1,19 @@ +#pragma once +#include +#include "IDatasetChartStrategy.hpp" // geopro::controller +namespace geopro::app { + +// ERT 接地电阻(measurement gr_data)策略:柱状图(同步)+ 列表(懒加载)两页签。 +// 两页签同一端点 measurement/gr/rows,loaderKey 不同(gr.bar 产 BarPayload / gr.rows 产 TablePayload)。 +struct GrMeasurementStrategy : controller::IDatasetChartStrategy { + std::string ddCode() const override { return "dd_ert_measurement_gr_data"; } + std::vector tabs() const override { + return { + {QStringLiteral("柱状图"), controller::ViewKind::Bar, + QStringLiteral("gr.bar"), /*lazy*/ false, /*paginated*/ false}, + {QStringLiteral("列表"), controller::ViewKind::Table, + QStringLiteral("gr.rows"), /*lazy*/ true, /*paginated*/ false}, + }; + } +}; +} // namespace geopro::app diff --git a/src/app/panels/chart/GridDataChartView.cpp b/src/app/panels/chart/GridDataChartView.cpp new file mode 100644 index 0000000..d268977 --- /dev/null +++ b/src/app/panels/chart/GridDataChartView.cpp @@ -0,0 +1,235 @@ +#include "panels/chart/GridDataChartView.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "PanelHeader.hpp" +#include "Theme.hpp" +#include "panels/AnomalyTablePanel.hpp" +#include "panels/DescriptionPanel.hpp" +#include "panels/chart/ChartTheme.hpp" +#include "panels/chart/ColorBarWidget.hpp" +#include "panels/chart/ColorMapService.hpp" +#include "panels/chart/ContourPlotItem.hpp" +#include "panels/chart/LivePanner.hpp" + +namespace geopro::app { + +GridDataChartView::GridDataChartView(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); + lay->setContentsMargins(0, 0, 0, 0); + lay->setSpacing(0); + + // ---- 工具条 ---- + auto* toolbar = new QWidget(this); + auto* tbLay = new QHBoxLayout(toolbar); + tbLay->setContentsMargins(4, 4, 4, 4); + tbLay->setSpacing(4); + + auto* btnGrid = new QToolButton(toolbar); + btnGrid->setText(QStringLiteral("网格")); + + auto* btnColorScale = new QToolButton(toolbar); + btnColorScale->setText(QStringLiteral("色阶配置")); + + auto* btnWhiten = new QToolButton(toolbar); + btnWhiten->setText(QStringLiteral("白化")); + + auto* btnFilter = new QToolButton(toolbar); + btnFilter->setText(QStringLiteral("滤波处理")); + + auto* chkShowAnom = new QCheckBox(QStringLiteral("显示异常"), toolbar); + chkShowAnom->setChecked(true); + + auto* chkShowContourLabel = new QCheckBox(QStringLiteral("显示等值线标注"), toolbar); + chkShowContourLabel->setChecked(true); + + auto* chkContourTip = new QCheckBox(QStringLiteral("显示等值线提示信息"), toolbar); + chkContourTip->setChecked(false); + + auto* lblSimplify = new QLabel(QStringLiteral("简化容差:"), toolbar); + + simplifySlider_ = new QSlider(Qt::Horizontal, toolbar); + simplifySlider_->setRange(0, 100); + simplifySlider_->setValue(50); + simplifySlider_->setFixedWidth(80); + + simplifyValueLabel_ = new QLabel(QStringLiteral("0.5"), toolbar); + simplifyValueLabel_->setFixedWidth(28); + + connect(simplifySlider_, &QSlider::valueChanged, this, [this](int v) { + simplifyValueLabel_->setText(QString::number(v / 100.0, 'f', 1)); + }); + + auto* btnAnomalyLabel = new QToolButton(toolbar); + btnAnomalyLabel->setText(QStringLiteral("异常标注")); + + auto* btnAutoLabel = new QToolButton(toolbar); + btnAutoLabel->setText(QStringLiteral("自动标注")); + + auto* btnSaveAs = new QToolButton(toolbar); + btnSaveAs->setText(QStringLiteral("另存为")); + + tbLay->addWidget(btnGrid); + tbLay->addWidget(btnColorScale); + tbLay->addWidget(btnWhiten); + tbLay->addWidget(btnFilter); + tbLay->addWidget(chkShowAnom); + tbLay->addWidget(chkShowContourLabel); + tbLay->addWidget(chkContourTip); + tbLay->addWidget(lblSimplify); + tbLay->addWidget(simplifySlider_); + tbLay->addWidget(simplifyValueLabel_); + tbLay->addWidget(btnAnomalyLabel); + tbLay->addWidget(btnAutoLabel); + tbLay->addWidget(btnSaveAs); + tbLay->addStretch(); + + lay->addWidget(toolbar); + + // ---- QwtPlot(仿 RawDataChartView,差异:x 轴在底部、不画过原点零线、网格线被填充覆盖故省)---- + plot_ = new QwtPlot(this); + plot_->setObjectName(QStringLiteral("gridPlotArea")); + plot_->enableAxis(QwtPlot::xBottom, true); + plot_->enableAxis(QwtPlot::xTop, false); + plot_->enableAxis(QwtPlot::yLeft, true); + + // 底色/轴字由 applyChartPlotTheme 按主题设置(ctor 末尾 + 主题热切换): + // 浅色=原版白底深灰字(1:1),暗色=深色画布避免刺眼白底。 + + // 交互:LivePanner 统一左键实时平移 + 滚轮缩放(消费滚轮事件,不冒泡触发滚动条)。 + new LivePanner(plot_, QwtPlot::xBottom, QwtPlot::yLeft, this); + + plot_->setMinimumSize(0, 0); + + // 锁定 x:y 真实比尺(参考轴 xBottom,Expanding)。 + rescaler_ = new QwtPlotRescaler(plot_->canvas(), QwtPlot::xBottom, QwtPlotRescaler::Expanding); + rescaler_->setAspectRatio(1.0); + rescaler_->setEnabled(true); + + // 图表区 = 绘图 + 色阶条(色阶条固定高 36)。 + colorBar_ = new ColorBarWidget(this); + colorBar_->setObjectName(QStringLiteral("gridColorScaleBar")); + auto* chartArea = new QWidget(this); + auto* chartLay = new QVBoxLayout(chartArea); + chartLay->setContentsMargins(0, 0, 0, 0); + chartLay->setSpacing(0); + chartLay->addWidget(plot_, 1); + chartLay->addWidget(colorBar_); + + // ---- 底部双页签(异常列表 / 描述)---- + anomalyTable_ = new AnomalyTablePanel(this); + descriptionPanel_ = new DescriptionPanel(this); + const QVector bottomTabs = { + {Glyph::Anomaly, QStringLiteral("异常列表"), anomalyTable_, true}, + {Glyph::Property, QStringLiteral("描述"), descriptionPanel_, false}, + }; + auto tabbedBottom = buildTabbedPanel(bottomTabs, {}); + + // 图表区 | 底部表 竖直 QSplitter(可拖拽调整比例)。给定最小高度→分割器有最小尺寸。 + chartArea->setMinimumHeight(280); + tabbedBottom.container->setMinimumHeight(160); + auto* splitter = new QSplitter(Qt::Vertical); + splitter->addWidget(chartArea); + splitter->addWidget(tabbedBottom.container); + splitter->setStretchFactor(0, 3); + splitter->setStretchFactor(1, 1); + splitter->setChildrenCollapsible(false); + + // 页签内滚动:把(图表+异常)分割器放进 QScrollArea。dock 够高→分割器填满(可拖动调整); + // dock 太矮→在页签内部出现竖滚动条(工具条/页签/标题固定),而非整个面板滚动。 + auto* scroll = new QScrollArea(this); + scroll->setWidgetResizable(true); + scroll->setFrameShape(QFrame::NoFrame); + scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scroll->setWidget(splitter); + lay->addWidget(scroll, 1); + + // ---- 工具条开关 → 重建/重绘 ---- + connect(chkShowAnom, &QCheckBox::toggled, this, [this](bool on) { + showAnomalies_ = on; + if (contourItem_) { contourItem_->setShowAnomalies(on); plot_->replot(); } + }); + connect(chkShowContourLabel, &QCheckBox::toggled, this, [this](bool on) { + showLabels_ = on; + if (contourItem_) { contourItem_->setShowLabels(on); plot_->replot(); } + }); + + // 主题配色:当前主题套一次 + 监听切换热更新。 + applyChartPlotTheme(plot_); + QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, plot_, + [this]() { applyChartPlotTheme(plot_); }); +} + +GridDataChartView::~GridDataChartView() { + // colorSvc_ 非 QObject、无 parent,需手动释放(contourItem_ 由 QwtPlot autoDelete 处理)。 + delete colorSvc_; +} + +void GridDataChartView::setPayload(const QVariant& payload) { + if (!payload.canConvert()) { + // 坏/空 variant:保持空态(不渲染、不崩)。 + return; + } + const auto p = payload.value(); + setGridData(p.grid, p.scale, p.anomalies); +} + +void GridDataChartView::setGridData(const geopro::core::Grid& grid, + const geopro::core::ColorScale& gridScale, + const std::vector& anoms) { + grid_ = grid; + gridScale_ = gridScale; + anoms_ = anoms; + hasGrid_ = true; + + // 重建色彩服务(旧 contourItem 已 detach/delete)。 + delete colorSvc_; + colorSvc_ = new ColorMapService(gridScale_); + + rebuildContour(); + + // 色阶条 + 底部异常表(懒加载结果含真实异常)。 + colorBar_->setColorScale(gridScale_); + anomalyTable_->setAnomalies(anoms_, {}, {}); +} + +void GridDataChartView::rebuildContour() { + if (!hasGrid_ || !colorSvc_) return; + + // 卸载旧项:QwtPlot 默认 autoDelete=true(析构时 delete 仍在 dict 的 item)。 + // 必须先 detach()(从 dict 移除)再 delete,否则 QwtPlot 析构时会 double-free。 + if (contourItem_) { + contourItem_->detach(); + delete contourItem_; + contourItem_ = nullptr; + } + + contourItem_ = new ContourPlotItem(); + contourItem_->setData(grid_, colorSvc_, anoms_, /*showLines*/ true, showLabels_); + contourItem_->setShowAnomalies(showAnomalies_); + contourItem_->attach(plot_); + + // 轴范围 = 数据范围(x=距离、y=深度/高程)。 + const QRectF bbox = contourItem_->boundingRect(); + if (!bbox.isNull()) { + plot_->setAxisScale(QwtPlot::xBottom, bbox.left(), bbox.right()); + plot_->setAxisScale(QwtPlot::yLeft, bbox.top(), bbox.bottom()); + } + plot_->updateAxes(); + if (rescaler_) rescaler_->rescale(); // 应用真实比尺 + plot_->replot(); +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/GridDataChartView.hpp b/src/app/panels/chart/GridDataChartView.hpp new file mode 100644 index 0000000..ab8c6d4 --- /dev/null +++ b/src/app/panels/chart/GridDataChartView.hpp @@ -0,0 +1,66 @@ +#pragma once +#include + +#include + +#include "model/Anomaly.hpp" +#include "model/ColorScale.hpp" +#include "model/Field.hpp" +#include "model/detail/DetailPayloads.hpp" +#include "panels/chart/IDetailView.hpp" + +class QSlider; +class QLabel; +class QwtPlot; +class QwtPlotRescaler; + +namespace geopro::app { + +class AnomalyTablePanel; +class DescriptionPanel; +class ColorBarWidget; +class ColorMapService; +class ContourPlotItem; + +// 网格数据图表视图:工具条 + QwtPlot(白底 + 真实比尺 + 实时平移/滚轮缩放,x 轴在底部) +// + 独立色阶条 + 底部双页签(异常列表/描述)。 +// 填充走 ContourPlotItem 栅格热力图 + 矢量等值线 + 标注 + 异常叠加。 +class GridDataChartView : public QWidget, public IDetailView { + Q_OBJECT +public: + explicit GridDataChartView(QWidget* parent = nullptr); + ~GridDataChartView() override; + + // 网格数据到达(懒加载结果):建色彩服务 + ContourPlotItem,挂到 QwtPlot,更新色阶条/异常表。 + void setGridData(const geopro::core::Grid& grid, const geopro::core::ColorScale& gridScale, + const std::vector& anoms); + + // IDetailView:解包 ContourPayload(grid + 色阶 + 异常)→ setGridData。坏/空 variant 保持空态。 + QWidget* widget() override { return this; } + void setPayload(const QVariant& payload) override; + +private: + void rebuildContour(); // 按当前显隐开关重建并重绘 ContourPlotItem + + QwtPlot* plot_ = nullptr; + QwtPlotRescaler* rescaler_ = nullptr; + ColorBarWidget* colorBar_ = nullptr; + AnomalyTablePanel* anomalyTable_ = nullptr; + DescriptionPanel* descriptionPanel_ = nullptr; + QSlider* simplifySlider_ = nullptr; + QLabel* simplifyValueLabel_ = nullptr; + + // 渲染状态 + ColorMapService* colorSvc_ = nullptr; // heap,setGridData 重建 + ContourPlotItem* contourItem_ = nullptr; + geopro::core::Grid grid_{1, 1}; + geopro::core::ColorScale gridScale_; + std::vector anoms_; + bool hasGrid_ = false; + + // 工具条显隐开关 + bool showAnomalies_ = true; + bool showLabels_ = true; +}; + +} // namespace geopro::app diff --git a/src/app/panels/chart/GridStrategy.hpp b/src/app/panels/chart/GridStrategy.hpp new file mode 100644 index 0000000..45a9613 --- /dev/null +++ b/src/app/panels/chart/GridStrategy.hpp @@ -0,0 +1,18 @@ +#pragma once +#include +#include "IDatasetChartStrategy.hpp" // geopro::controller +namespace geopro::app { + +// dd_grid(白化数据)策略:单「列表」页签,服务端分页(vxe-pager)。 +// 列表 = Table(paginated;grid.rows 产 TablePayload:序号/x/y 列 + total/pageNo/pageSize; +// 端点 dd/ert/grid/rows)。非 lazy(单页签,开页即载首页)。 +struct GridStrategy : controller::IDatasetChartStrategy { + std::string ddCode() const override { return "dd_grid"; } + std::vector tabs() const override { + return { + {QStringLiteral("列表"), controller::ViewKind::Table, + QStringLiteral("grid.rows"), /*lazy*/ false, /*paginated*/ true}, + }; + } +}; +} // namespace geopro::app diff --git a/src/app/panels/chart/IDetailView.hpp b/src/app/panels/chart/IDetailView.hpp new file mode 100644 index 0000000..35782e8 --- /dev/null +++ b/src/app/panels/chart/IDetailView.hpp @@ -0,0 +1,17 @@ +#pragma once +#include + +class QWidget; + +namespace geopro::app { + +// 详情页签视图统一接口:壳/工厂只依赖它,渲染细节由具体视图自行解包载荷。 +// widget() 返回承载的 QWidget(多为 this);setPayload 解包 QVariant 载荷并渲染。 +class IDetailView { +public: + virtual ~IDetailView() = default; + virtual QWidget* widget() = 0; + virtual void setPayload(const QVariant& payload) = 0; +}; + +} // namespace geopro::app diff --git a/src/app/panels/chart/LineChartView.cpp b/src/app/panels/chart/LineChartView.cpp new file mode 100644 index 0000000..2d8e868 --- /dev/null +++ b/src/app/panels/chart/LineChartView.cpp @@ -0,0 +1,380 @@ +#include "panels/chart/LineChartView.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "Theme.hpp" +#include "panels/chart/ChartTheme.hpp" + +namespace geopro::app { + +namespace { + +// 类目轴刻度:把整数刻度位(0,1,2,…)映射为类目标签 "#1","#2",…(来自 categories)。 +// 非整数/越界刻度返回空标签(避免次刻度污染)。与 BarChartView::CategoryScaleDraw 同款。 +class CategoryScaleDraw : public QwtScaleDraw { +public: + explicit CategoryScaleDraw(std::vector labels) : labels_(std::move(labels)) { + enableComponent(QwtScaleDraw::Backbone, true); + enableComponent(QwtScaleDraw::Ticks, true); + } + QwtText label(double v) const override { + const double r = std::round(v); + if (std::abs(v - r) > 1e-6) return QwtText(); // 仅整数刻度出标签 + const int i = static_cast(r); + if (i < 0 || i >= static_cast(labels_.size())) return QwtText(); + return labels_[static_cast(i)]; + } + +private: + std::vector labels_; +}; + +QColor lineColor(const QString& hex) { + QColor c(hex); + return c.isValid() ? c : QColor(0x54, 0x70, 0xc6); // 回退 ECharts 蓝 +} + +} // namespace + +LineChartView::LineChartView(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); + lay->setContentsMargins(0, 0, 0, 0); + lay->setSpacing(0); + + // 左上水平 y 轴标题(ECharts 风格,对齐 BarChartView)。 + yTitle_ = new QLabel(this); + auto* titleRow = new QWidget(this); + auto* titleLay = new QHBoxLayout(titleRow); + titleLay->setContentsMargins(48, 6, 8, 0); // 左缩进对齐 y 轴上方 + titleLay->setSpacing(0); + titleLay->addWidget(yTitle_); + titleLay->addStretch(); + lay->addWidget(titleRow); + + plot_ = new QwtPlot(this); + plot_->setObjectName(QStringLiteral("trajLinePlotArea")); + plot_->enableAxis(QwtPlot::xBottom, true); + plot_->enableAxis(QwtPlot::yLeft, true); + + // x 轴标题「电极号」底部居中(setData 设文本)。 + plot_->setAxisTitle(QwtPlot::xBottom, QwtText()); + + // 仅横向(y)网格,弱化(与原版 ECharts 一致:仅水平刻度线)。 + auto* grid = new QwtPlotGrid(); + grid->enableX(false); + grid->enableY(true); + grid->enableXMin(false); + grid->enableYMin(false); + grid->setMajorPen(QColor(225, 225, 225), 1.0, Qt::SolidLine); + grid->attach(plot_); + + plot_->setMinimumSize(0, 0); + lay->addWidget(plot_, 1); + + // 鼠标 hover 追踪:虚线参考线 + 实心点 + 浮动框(data_ 地址稳定,setData 只改其内容)。 + hover_ = new LineHoverTip(plot_, &data_, this); + + // 主题:底色/轴字/网格按当前主题套一次 + 热切换。 + applyChartPlotTheme(plot_); + QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, plot_, [this]() { + applyChartPlotTheme(plot_); + QPalette pal = yTitle_->palette(); + pal.setColor(QPalette::WindowText, + isDarkTheme() ? tokenColor("text/secondary") : QColor(90, 90, 90)); + yTitle_->setPalette(pal); + }); +} + +LineChartView::~LineChartView() { + // 卸载并删除已挂折线,先于 QwtPlot autoDelete 触发(与 BarChartView 析构对称,避免双删)。 + clearCurve(); +} + +void LineChartView::clearCurve() { + if (curve_) { + curve_->detach(); + delete curve_; + curve_ = nullptr; + } +} + +void LineChartView::setPayload(const QVariant& payload) { + if (!payload.canConvert()) return; // 坏/空 → 空态 + setData(payload.value()); +} + +void LineChartView::setData(const geopro::core::LinePayload& p) { + data_ = p; + clearCurve(); + + const int n = static_cast(p.categories.size()); + + // y 轴标题(左上水平 QLabel)。 + yTitle_->setText(p.yTitle); + QPalette tpal = yTitle_->palette(); + tpal.setColor(QPalette::WindowText, + isDarkTheme() ? tokenColor("text/secondary") : QColor(90, 90, 90)); + yTitle_->setPalette(tpal); + + // 锁定两轴为固定刻度(关闭自动缩放),使 hover 时挂/卸标记的 replot 不会重新拟合 → + // 曲线不再随鼠标进入而上下漂移(FIX 3)。 + plot_->setAxisAutoScale(QwtPlot::xBottom, false); + plot_->setAxisAutoScale(QwtPlot::yLeft, false); + + // x 轴标题「电极号」+ 类目刻度 "#1".."#40"。 + plot_->setAxisTitle(QwtPlot::xBottom, p.xTitle); + plot_->setAxisScaleDraw(QwtPlot::xBottom, new CategoryScaleDraw(p.categories)); + plot_->setAxisScale(QwtPlot::xBottom, 0.0, n > 0 ? n - 1 : 0.0, 1.0); // 每类目一刻度 + plot_->setAxisMaxMinor(QwtPlot::xBottom, 0); + + // y 范围固定 0..max*1.1(从数据取上界,留 10% 余量;对齐原版 0–30 观感)。 + // 固定刻度后 hover 标记不再触发 y 轴重算。 + double yMax = 0.0; + for (double v : p.y) yMax = std::max(yMax, v); + plot_->setAxisScale(QwtPlot::yLeft, 0.0, yMax > 0 ? yMax * 1.1 : 1.0); + + if (p.y.empty()) { + plot_->replot(); + return; + } + + // 单条折线:电极号(0..n-1) → 高程。 + // + // 平滑必须在【数据坐标】里一次性算好,绝不能用 QwtPlotCurve::Fitted。原因(本 BUG 根因): + // QwtPlotCurve 的 Fitted 拟合器在【绘制时】对【已映射到画布像素】的点跑样条 + // (见 qwt_plot_curve.cpp 注释 “The curve fitter operates on the translated points + // ( = widget coordinates)”)。于是拟合结果取决于当时画布的像素几何;当首次 hover 触发 + // replot 时画布几何与首帧哪怕有一丁点差异,样条就在不同像素网格上重采样 → 曲线竖直方向 + // 细微漂移。锁轴并不能消除这一点(漂移源是像素级重拟合,不是自动缩放)。 + // 解法(QWT 官方建议:把拟合结果缓存进 series):在数据坐标里把样条展开成稠密折线, + // 作为静态 samples 交给普通 Lines 曲线。此后每次 replot 都映射同一条固定折线 → 首帧与 + // hover 帧像素完全一致,零漂移。Cardinal + ParameterUniform 与原 QwtSplineCurveFitter + // 内部一致,平滑观感不变。 + curve_ = new QwtPlotCurve(p.seriesName); + QVector nodes; + nodes.reserve(n); + for (int i = 0; i < n && i < static_cast(p.y.size()); ++i) + nodes.append(QPointF(i, p.y[static_cast(i)])); + + QVector samples = nodes; + if (p.smooth && nodes.size() > 2) { + QwtSplineLocal spline(QwtSplineLocal::Cardinal); + spline.setParametrization(QwtSplineParametrization::ParameterUniform); + // 展平容差取数据范围的极小比例 → 折线足够稠密、肉眼平滑;数据坐标空间,缩放无关。 + const double xSpan = nodes.isEmpty() ? 1.0 : (nodes.last().x() - nodes.first().x()); + const double tolerance = std::max(std::abs(xSpan), 1.0) / 2000.0; + const QPolygonF smooth = spline.polygon(QPolygonF(nodes), tolerance); + if (smooth.size() > 1) samples = QVector(smooth.begin(), smooth.end()); + } + curve_->setSamples(samples); + curve_->setStyle(QwtPlotCurve::Lines); + curve_->setPen(QPen(lineColor(p.color), 2)); + curve_->setRenderHint(QwtPlotItem::RenderAntialiased, true); + curve_->attach(plot_); + + plot_->replot(); +} + +// ── LineHoverOverlay / LineHoverTip:折线 hover 虚线参考线 + 实心点 + 浮动框 ────── + +namespace { + +// 从类目标签 "#N" 解析电极号;无前缀/非数字 → 回退 1-based 索引。 +int electrodeNoFromCategory(const std::vector& cats, int i) { + if (i >= 0 && i < static_cast(cats.size())) { + QString s = cats[static_cast(i)]; + if (s.startsWith(QLatin1Char('#'))) s = s.mid(1); + bool ok = false; + const int no = s.toInt(&ok); + if (ok) return no; + } + return i + 1; +} + +} // namespace + +// hover 叠层:画在 plot canvas 之上的 QwtWidgetOverlay。激活时用 plot 的 canvasMap +// 把数据坐标 (idx, yVal) 变换为像素,画一条垂直虚线参考线(满画布高)+ 曲线上实心点。 +// updateOverlay() 只重绘本叠层、【不】触碰 plot/曲线 → 曲线零漂移。颜色每帧按主题重算(廉价)。 +class LineHoverOverlay : public QwtWidgetOverlay { +public: + explicit LineHoverOverlay(QwtPlot* plot) + : QwtWidgetOverlay(plot->canvas()), plot_(plot) { + // 不用 mask(默认 MaskHint 在未重写 maskHint 时会把可见区域裁空 → 叠层不显示)。 + setMaskMode(QwtWidgetOverlay::NoMask); + } + + // 设置 hover 状态并重绘叠层(仅叠层,不 replot plot)。 + void setHover(bool active, int idx, double yVal, const QColor& dotColor) { + active_ = active; + idx_ = idx; + yVal_ = yVal; + dotColor_ = dotColor; + updateOverlay(); + } + + void clearHover() { + if (!active_) return; + active_ = false; + updateOverlay(); + } + +protected: + void drawOverlay(QPainter* painter) const override { + if (!active_ || !plot_) return; + + const QwtScaleMap xMap = plot_->canvasMap(QwtPlot::xBottom); + const QwtScaleMap yMap = plot_->canvasMap(QwtPlot::yLeft); + const double xPix = xMap.transform(idx_); + const double yPix = yMap.transform(yVal_); + + // 垂直虚线参考线:满画布高(主题感知颜色)。 + const QColor lineCol = + isDarkTheme() ? tokenColor("border/strong") : QColor(150, 170, 210); + painter->setRenderHint(QPainter::Antialiasing, true); + painter->setPen(QPen(lineCol, 1.0, Qt::DashLine)); + painter->drawLine(QPointF(xPix, 0.0), QPointF(xPix, static_cast(height()))); + + // 曲线上实心圆点(系列色填充 + 同色描边,直径 ~8px)。 + QColor dot = dotColor_.isValid() ? dotColor_ : QColor(0x54, 0x70, 0xc6); + painter->setPen(QPen(dot, 1.0)); + painter->setBrush(QBrush(dot)); + painter->drawEllipse(QPointF(xPix, yPix), 4.0, 4.0); + } + +private: + QwtPlot* plot_; + bool active_ = false; + int idx_ = 0; + double yVal_ = 0.0; + QColor dotColor_; +}; + +LineHoverTip::LineHoverTip(QwtPlot* plot, const geopro::core::LinePayload* data, QObject* parent) + : QObject(parent), plot_(plot), data_(data) { + if (plot_ && plot_->canvas()) { + // hover(无按键)需开启鼠标跟踪,否则默认仅按键按下时才收到 MouseMove。 + plot_->canvas()->setMouseTracking(true); + plot_->canvas()->installEventFilter(this); + // 叠层 parent 为 canvas → 随 canvas 尺寸/位置自动跟随(QwtWidgetOverlay 处理 resize)。 + overlay_ = new LineHoverOverlay(plot_); + overlay_->show(); + } +} + +LineHoverTip::~LineHoverTip() { + // tip_ 是无父顶层 widget,须显式删除。overlay_ parent 为 canvas,随其析构(无需手删)。 + delete tip_; + tip_ = nullptr; +} + +void LineHoverTip::hideHover() { + // 叠层置非激活并重绘(仅叠层,不 replot plot);隐藏浮动框。 + if (overlay_) overlay_->clearHover(); + if (tip_) tip_->hide(); +} + +void LineHoverTip::showTip(const QString& html, const QPoint& globalPos) { + // 自定义浮动框:Qt::ToolTip 顶层 QLabel(富文本)。每次 MouseMove 必更新文本与位置, + // 不经 QToolTip 去重逻辑 → 提示稳定显示(FIX 4)。主题感知配色(白框/暗框)。 + if (!tip_) { + tip_ = new QLabel(nullptr, Qt::ToolTip | Qt::FramelessWindowHint); + tip_->setObjectName(QStringLiteral("lineHoverTip")); + tip_->setTextFormat(Qt::RichText); + tip_->setMargin(8); + tip_->setAttribute(Qt::WA_ShowWithoutActivating, true); + } + const bool dark = isDarkTheme(); + const QColor bg = dark ? tokenColor("bg/panel") : QColor(0xFF, 0xFF, 0xFF); + const QColor border = dark ? tokenColor("border/strong") : QColor(0xE3, 0xE6, 0xEB); + const QColor fg = dark ? tokenColor("text/primary") : QColor(0x27, 0x2C, 0x35); + tip_->setStyleSheet(QStringLiteral( + "QLabel#lineHoverTip{background:%1;color:%2;border:1px solid %3;" + "border-radius:4px;}") + .arg(bg.name(), fg.name(), border.name())); + tip_->setText(html); + tip_->adjustSize(); + + // 在光标右下偏移摆放;越右/下边界则翻向左/上,避免被屏幕裁切。 + QPoint pos = globalPos + QPoint(14, 16); + if (QScreen* scr = QGuiApplication::screenAt(globalPos)) { + const QRect g = scr->availableGeometry(); + if (pos.x() + tip_->width() > g.right()) pos.setX(globalPos.x() - tip_->width() - 14); + if (pos.y() + tip_->height() > g.bottom()) pos.setY(globalPos.y() - tip_->height() - 16); + } + tip_->move(pos); + if (!tip_->isVisible()) tip_->show(); +} + +bool LineHoverTip::eventFilter(QObject* obj, QEvent* ev) { + if (!plot_ || obj != plot_->canvas()) return QObject::eventFilter(obj, ev); + + if (ev->type() == QEvent::Leave) { + hideHover(); + return false; + } + if (ev->type() != QEvent::MouseMove) return QObject::eventFilter(obj, ev); + + auto* me = static_cast(ev); + // 拖动中(有按键)或无数据 → 不弹提示。 + if (me->buttons() != Qt::NoButton || !data_ || data_->y.empty()) { + hideHover(); + return false; + } + + const auto& ys = data_->y; + const int n = static_cast(ys.size()); + + // 按 x 吸附到最近电极索引(类目位于整数 x = 0..n-1)。 + const QwtScaleMap xMap = plot_->canvasMap(QwtPlot::xBottom); + const double xVal = xMap.invTransform(me->position().x()); + int idx = static_cast(std::lround(xVal)); + idx = std::clamp(idx, 0, n - 1); + + const double yVal = ys[static_cast(idx)]; + const int electrodeNo = electrodeNoFromCategory(data_->categories, idx); + + // 实心点系列色(虚线参考线颜色在 overlay 内按主题重算)。 + QColor dotCol(data_->color); + if (!dotCol.isValid()) dotCol = QColor(0x54, 0x70, 0xc6); + + // 关键:把虚线 + 实心点交给叠层重绘(仅叠层重绘,【绝不】replot plot → 曲线零漂移)。 + if (overlay_) overlay_->setHover(true, idx, yVal, dotCol); + + // 浮动框:#<电极号> 表头 + 系列点·标签·值(高程保留 3 位小数)。系列点用系列色圆点。 + const QString tip = QStringLiteral("#%1
高程(m)" + "  %3") + .arg(electrodeNo) + .arg(dotCol.name()) + .arg(yVal, 0, 'f', 3); + showTip(tip, me->globalPosition().toPoint()); + + return false; // 不消费,保留其它过滤器链路 +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/LineChartView.hpp b/src/app/panels/chart/LineChartView.hpp new file mode 100644 index 0000000..7fa1009 --- /dev/null +++ b/src/app/panels/chart/LineChartView.hpp @@ -0,0 +1,78 @@ +#pragma once +#include +#include + +#include "model/detail/DetailPayloads.hpp" +#include "panels/chart/IDetailView.hpp" + +class QEvent; +class QLabel; +class QPoint; +class QwtPlot; +class QwtPlotCurve; + +namespace geopro::app { + +class LineHoverTip; +class LineHoverOverlay; + +// 折线图视图(dd_trajectory_data 高程页签): +// QwtPlot + QwtPlotCurve(单条平滑折线,Fitted + QwtSplineCurveFitter)。 +// x 轴类目标签 "#1".."#40"(自定义 QwtScaleDraw,复用 BarChartView 同款思路); +// x 轴标题「电极号」(底部居中);y 轴标题「高程」用左上水平 QLabel(ECharts 风格); +// 线色 #5470c6(ECharts 默认蓝,数据色,两主题一致);无图例(单系列)。 +// 背景/轴字/网格随主题(ChartTheme / ThemeManager)。 +class LineChartView : public QWidget, public IDetailView { + Q_OBJECT +public: + explicit LineChartView(QWidget* parent = nullptr); + ~LineChartView() override; + + void setData(const geopro::core::LinePayload& p); + + QWidget* widget() override { return this; } + void setPayload(const QVariant& payload) override; // 坏/空 variant → 空态不崩 + +private: + void clearCurve(); // 卸载并删除已挂折线(避免 QwtPlot autoDelete 双删) + + geopro::core::LinePayload data_; + QwtPlot* plot_; + QLabel* yTitle_; // 左上水平 y 轴标题(ECharts 风格) + QwtPlotCurve* curve_ = nullptr; // 当前挂载的折线(已 attach;卸载时 detach+delete) + LineHoverTip* hover_ = nullptr; // 鼠标 hover 追踪器(虚线参考线 + 实心点 + 浮动框) +}; + +// 折线 hover 追踪器(对齐原版 ECharts 高程折线 hover):监听画布鼠标移动(无按键时), +// 按 x 吸附到最近电极索引,绘制:垂直虚线参考线 + 曲线上该点实心圆点 + +// 自定义浮动框(#<电极号> / ● 高程(m): <值,3 位>)。 +// +// 关键(DEFINITIVE FIX):虚线参考线 + 实心点用 QwtWidgetOverlay 画在 canvas 之上, +// hover 路径【绝不】调用 plot_->replot()。此前用 QwtPlotMarker + 每次 MouseMove replot, +// 那次 replot 重跑 plot 布局/updateAxes,使曲线随 hover 标记的挂/卸而上下漂移(锁轴 + 数据 +// 坐标样条都无法阻止——漂移源是 replot/marker-attach 本身)。改为 overlay 后曲线在 setData +// 里只渲染一次、hover 全程不再 replot → 曲线不可能移动。 +// 浮动框仍用独立的 Qt::ToolTip 顶层 QLabel(富文本):QToolTip 会去重抑制相同/相近位置的重复 +// showText(FIX 4),自定义 widget 每次 MouseMove 必更新位置/内容。 +// 鼠标离开/越界时隐藏。不消费事件(镜像 ScatterHoverTip)。 +class LineHoverTip : public QObject { + Q_OBJECT +public: + LineHoverTip(QwtPlot* plot, const geopro::core::LinePayload* data, QObject* parent = nullptr); + ~LineHoverTip() override; + +protected: + bool eventFilter(QObject* obj, QEvent* ev) override; + +private: + void hideHover(); // overlay 置非激活 + 隐藏浮动框(无 replot) + // 在 globalPos 旁显示富文本浮动框(按需创建、主题感知样式;每次必更新)。 + void showTip(const QString& html, const QPoint& globalPos); + + QwtPlot* plot_; + const geopro::core::LinePayload* data_; // 由 LineChartView 持有(data_ 成员,地址稳定);只读不拥有 + LineHoverOverlay* overlay_ = nullptr; // 画布之上的 hover 叠层(虚线 + 实心点;不触发 plot replot) + QLabel* tip_ = nullptr; // 自定义浮动框(Qt::ToolTip 顶层 QLabel,富文本;替代 QToolTip) +}; + +} // namespace geopro::app diff --git a/src/app/panels/chart/LivePanner.cpp b/src/app/panels/chart/LivePanner.cpp new file mode 100644 index 0000000..d7c765b --- /dev/null +++ b/src/app/panels/chart/LivePanner.cpp @@ -0,0 +1,82 @@ +#include "panels/chart/LivePanner.hpp" + +#include +#include +#include +#include +#include + +namespace geopro::app { + +LivePanner::LivePanner(QwtPlot* plot, int xAxis, int yAxis, QObject* parent) + : QObject(parent), plot_(plot), xAxis_(xAxis), yAxis_(yAxis) { + if (plot_ && plot_->canvas()) plot_->canvas()->installEventFilter(this); +} + +bool LivePanner::eventFilter(QObject* obj, QEvent* ev) { + if (!plot_ || obj != plot_->canvas()) return QObject::eventFilter(obj, ev); + + switch (ev->type()) { + case QEvent::MouseButtonPress: { + auto* me = static_cast(ev); + if (me->button() == Qt::LeftButton) { + panning_ = true; + startPos_ = me->pos(); + x0Min_ = plot_->axisScaleDiv(xAxis_).lowerBound(); + x0Max_ = plot_->axisScaleDiv(xAxis_).upperBound(); + y0Min_ = plot_->axisScaleDiv(yAxis_).lowerBound(); + y0Max_ = plot_->axisScaleDiv(yAxis_).upperBound(); + return true; + } + break; + } + case QEvent::MouseMove: { + if (!panning_) break; + auto* me = static_cast(ev); + const int W = plot_->canvas()->width(); + const int H = plot_->canvas()->height(); + if (W <= 0 || H <= 0) break; + const double dxData = (me->pos().x() - startPos_.x()) * (x0Max_ - x0Min_) / W; + const double dyData = (me->pos().y() - startPos_.y()) * (y0Max_ - y0Min_) / H; + // 拖右(dx>0)→窗口左移(xmin/xmax 减),内容随光标右移; + // 拖下(像素 dy>0)→yLeft 数据向上→窗口上移(ymin/ymax 增),内容随光标下移。 + plot_->setAxisScale(xAxis_, x0Min_ - dxData, x0Max_ - dxData); + plot_->setAxisScale(yAxis_, y0Min_ + dyData, y0Max_ + dyData); + plot_->replot(); + return true; + } + case QEvent::MouseButtonRelease: { + auto* me = static_cast(ev); + if (me->button() == Qt::LeftButton) { + panning_ = false; + return true; + } + break; + } + case QEvent::Wheel: { + // 滚轮缩放(以光标为中心,上滚=放大),并 **消费事件**(return true)避免冒泡到外层 + // 滚动区域触发滚动条。两轴同因子缩放→保持锁定的真实比尺。 + auto* we = static_cast(ev); + const int d = we->angleDelta().y(); + if (d == 0) return true; + const double f = (d > 0) ? (1.0 / 1.15) : 1.15; // 上滚→区间缩小→放大 + const QPointF pos = we->position(); + const double cx = plot_->invTransform(xAxis_, pos.x()); + const double cy = plot_->invTransform(yAxis_, pos.y()); + const double xMin = plot_->axisScaleDiv(xAxis_).lowerBound(); + const double xMax = plot_->axisScaleDiv(xAxis_).upperBound(); + const double yMin = plot_->axisScaleDiv(yAxis_).lowerBound(); + const double yMax = plot_->axisScaleDiv(yAxis_).upperBound(); + plot_->setAxisScale(xAxis_, cx - (cx - xMin) * f, cx + (xMax - cx) * f); + plot_->setAxisScale(yAxis_, cy - (cy - yMin) * f, cy + (yMax - cy) * f); + plot_->replot(); + we->accept(); + return true; + } + default: + break; + } + return QObject::eventFilter(obj, ev); +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/LivePanner.hpp b/src/app/panels/chart/LivePanner.hpp new file mode 100644 index 0000000..7ef146b --- /dev/null +++ b/src/app/panels/chart/LivePanner.hpp @@ -0,0 +1,29 @@ +#pragma once +#include +#include + +class QwtPlot; + +namespace geopro::app { + +// 实时拖动平移:左键拖动时连续平移两轴并 replot(坐标轴 + 内容一起实时移动), +// 区别于 QwtPlotPanner 的"像素抓取"(拖动中轴不动、边缘空白、松手才定稿)。 +// 平移为纯平移,不改变纵横比(与 QwtPlotRescaler 锁定的真实比尺兼容)。 +class LivePanner : public QObject { + Q_OBJECT +public: + LivePanner(QwtPlot* plot, int xAxis, int yAxis, QObject* parent = nullptr); + +protected: + bool eventFilter(QObject* obj, QEvent* ev) override; + +private: + QwtPlot* plot_; + int xAxis_; + int yAxis_; + bool panning_ = false; + QPoint startPos_; + double x0Min_ = 0, x0Max_ = 0, y0Min_ = 0, y0Max_ = 0; +}; + +} // namespace geopro::app diff --git a/src/app/panels/chart/MeasurementStrategy.hpp b/src/app/panels/chart/MeasurementStrategy.hpp new file mode 100644 index 0000000..0904fdb --- /dev/null +++ b/src/app/panels/chart/MeasurementStrategy.hpp @@ -0,0 +1,18 @@ +#pragma once +#include +#include "IDatasetChartStrategy.hpp" // geopro::controller +namespace geopro::app { + +// ERT 原始数据(measurement)策略:散点伪剖面(同步)+ 数据列表(懒加载)两页签。 +struct MeasurementStrategy : controller::IDatasetChartStrategy { + std::string ddCode() const override { return "dd_ert_measurement_data"; } + std::vector tabs() const override { + return { + {QStringLiteral("散点图"), controller::ViewKind::Scatter, + QStringLiteral("ert_measurement.scatter"), /*lazy*/ false, /*paginated*/ false}, + {QStringLiteral("数据列表"), controller::ViewKind::Table, + QStringLiteral("ert_measurement.rows"), /*lazy*/ true, /*paginated*/ false}, + }; + } +}; +} // namespace geopro::app diff --git a/src/app/panels/chart/RawDataChartView.cpp b/src/app/panels/chart/RawDataChartView.cpp new file mode 100644 index 0000000..7268f10 --- /dev/null +++ b/src/app/panels/chart/RawDataChartView.cpp @@ -0,0 +1,468 @@ +#include "panels/chart/RawDataChartView.hpp" +#include "panels/chart/ChartTheme.hpp" +#include "panels/chart/ColorBarWidget.hpp" +#include "panels/chart/ScatterHoverTip.hpp" +#include "panels/chart/ScatterPlotItem.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "panels/chart/LivePanner.hpp" +#include "Theme.hpp" + +namespace geopro::app { + +RawDataChartView::RawDataChartView(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); + lay->setContentsMargins(0, 0, 0, 0); + lay->setSpacing(0); + rootLay_ = lay; + + // ---- 工具条(默认 = 反演原数据;measurement 到来时 buildMeasurementToolbar 替换)---- + auto* toolbar = new QWidget(this); + toolbar_ = toolbar; + auto* tbLay = new QHBoxLayout(toolbar); + tbLay->setContentsMargins(4, 4, 4, 4); + tbLay->setSpacing(4); + + auto* btnGrid = new QToolButton(toolbar); + btnGrid->setText(QStringLiteral("网格")); + + auto* btnColorScale = new QToolButton(toolbar); + btnColorScale->setText(QStringLiteral("色阶配置")); + + auto* lblCurrentChart = new QLabel(QStringLiteral("当前图形:"), toolbar); + + chartTypeCombo_ = new QComboBox(toolbar); + chartTypeCombo_->addItem(QStringLiteral("散点图")); + + auto* btnSaveAs = new QToolButton(toolbar); + btnSaveAs->setText(QStringLiteral("另存为")); + + tbLay->addWidget(btnGrid); + tbLay->addWidget(btnColorScale); + tbLay->addWidget(lblCurrentChart); + tbLay->addWidget(chartTypeCombo_); + tbLay->addWidget(btnSaveAs); + tbLay->addStretch(); + + lay->addWidget(toolbar); + + // ---- QwtPlot(stretch 填满剩余空间)---- + plot_ = new QwtPlot(this); + plot_->setObjectName(QStringLiteral("rawPlotArea")); + + // x 轴顶部,关闭底部 x 轴 + plot_->enableAxis(QwtPlot::xTop, true); + plot_->enableAxis(QwtPlot::xBottom, false); + plot_->enableAxis(QwtPlot::yLeft, true); + + // 底色/轴字/网格/零线配色由 applyChartPlotTheme 按主题统一设置(见 ctor 末尾 + 主题热切换)。 + // 浅色与原版 web 一致(白底深灰字);暗色改深色画布避免刺眼白底。 + + // 横纵网格线(浅灰,暗色下由 applyChartPlotTheme 重着色)。 + auto* grid = new QwtPlotGrid(); + grid->setMajorPen(QColor(225, 225, 225), 1.0, Qt::SolidLine); + grid->setMinorPen(QColor(240, 240, 240), 1.0, Qt::DotLine); + grid->enableXMin(false); + grid->enableYMin(false); + grid->setXAxis(QwtPlot::xTop); + grid->setYAxis(QwtPlot::yLeft); + grid->attach(plot_); + + // 过原点零线(对齐原版 zeroline:x=0 竖线 + y=0 横线 → "四象限"观感)。 + auto* zeroX = new QwtPlotMarker(); + zeroX->setLineStyle(QwtPlotMarker::VLine); + zeroX->setXValue(0.0); + zeroX->setLinePen(QColor(180, 180, 180), 1.0); + zeroX->setXAxis(QwtPlot::xTop); + zeroX->attach(plot_); + auto* zeroY = new QwtPlotMarker(); + zeroY->setLineStyle(QwtPlotMarker::HLine); + zeroY->setYValue(0.0); + zeroY->setLinePen(QColor(180, 180, 180), 1.0); + zeroY->attach(plot_); + + // 交互:LivePanner 统一处理左键实时平移 + 滚轮缩放(并消费滚轮事件,不冒泡触发滚动条)。 + new LivePanner(plot_, QwtPlot::xTop, QwtPlot::yLeft, this); + + // 散点 hover 提示(X/Y/值)。后装(晚于 LivePanner)→ 事件链中先收到 MouseMove, + // 无按键时弹提示且不消费;有按键(拖动)跳过,交给 LivePanner。数据地址稳定,装配期绑定一次。 + hoverTip_ = new ScatterHoverTip(plot_, QwtPlot::xTop, QwtPlot::yLeft, this); + hoverTip_->setField(&data_.scatter); + + // 允许随停靠面板自由收缩(不强制最小宽度)。 + plot_->setMinimumSize(0, 0); + + // 锁定 x:y 真实比尺(用户选定):1 数据单位 x = 1 数据单位 y(像素)。 + // 参考轴 xTop、Expanding 策略——resize/缩放时维持等比,剖面呈真实"宽扁"形状。 + rescaler_ = new QwtPlotRescaler(plot_->canvas(), QwtPlot::xTop, QwtPlotRescaler::Expanding); + rescaler_->setAspectRatio(1.0); + rescaler_->setEnabled(true); + + // ---- 图表行:plot(stretch)+ 右侧竖向色阶条(measurement 用,默认隐藏)---- + auto* plotRow = new QWidget(this); + auto* plotRowLay = new QHBoxLayout(plotRow); + plotRowLay->setContentsMargins(0, 0, 0, 0); + plotRowLay->setSpacing(0); + plotRowLay->addWidget(plot_, 1); + + colorBarV_ = new ColorBarWidget(plotRow, ColorBarWidget::Orientation::Vertical); + colorBarV_->setObjectName(QStringLiteral("rawColorScaleBarV")); + colorBarV_->setVisible(false); // 默认(反演原数据)用底部横条 + plotRowLay->addWidget(colorBarV_); + + lay->addWidget(plotRow, 1); + + // ---- 底部独立色阶条(固定高 36px,反演原数据用)---- + colorBar_ = new ColorBarWidget(this); + colorBar_->setObjectName(QStringLiteral("rawColorScaleBar")); + lay->addWidget(colorBar_); + + // 主题配色:当前主题套一次 + 监听切换热更新(暗色给深底,浅色保持白底=原版)。 + applyChartPlotTheme(plot_); + QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, plot_, + [this]() { applyChartPlotTheme(plot_); }); +} + +RawDataChartView::~RawDataChartView() { + // colorSvc_ 非 QObject、无 parent,需手动释放(plot_ 的 item 由 QwtPlot autoDelete 处理)。 + delete colorSvc_; +} + +QWidget* RawDataChartView::plotArea() const { + return plot_; +} + +namespace { + +// 把一组 FieldOption 填进下拉,并按 defaultCode(或 defaultName 回退)选中默认项。 +void fillCombo(QComboBox* combo, const std::vector& opts, + const QString& defaultCode, const QString& defaultName) { + for (const auto& o : opts) { + // 用户可见名为 name;userData 存 fieldCode(重绘/识别用)。 + combo->addItem(o.name, o.code); + } + // 默认选中:优先匹配 fieldCode,否则匹配 name(method 的 code 全为 null,用 name)。 + int idx = defaultCode.isEmpty() ? -1 : combo->findData(defaultCode); + if (idx < 0 && !defaultName.isEmpty()) idx = combo->findText(defaultName); + if (idx >= 0) combo->setCurrentIndex(idx); +} + +// ── 工具条占位图标(线性风格,2× 超采样 → HiDPI 清晰)──────────────────── +// 原版用 Arco line icons(viewBox 0 0 48 48,stroke-width 4,stroke=currentColor)。 +// 这里以 QPainter 画细线风格,逻辑边长 logical px(按钮 setIconSize 用同值), +// 像素 2× 渲染并 setDevicePixelRatio(2) 保证缩放下不糊。 +constexpr int kToolIconPx = 16; // 逻辑图标边长(与 setIconSize 对齐) +constexpr qreal kToolIconScale = 2.0; // 超采样倍率(HiDPI 清晰) + +QPixmap makeToolIconCanvas(QPainter& p) { + // 调用方在 [0,kToolIconPx] 逻辑坐标系下作画;返回前缩放 + 设 dpr。 + const int dim = qRound(kToolIconPx * kToolIconScale); + QPixmap pm(dim, dim); + pm.fill(Qt::transparent); + p.begin(&pm); + p.setRenderHint(QPainter::Antialiasing, true); + p.setRenderHint(QPainter::SmoothPixmapTransform, true); + p.scale(kToolIconScale, kToolIconScale); // 之后用逻辑 px 坐标 + return pm; +} + +// 信息图标:细描边圆 + 内部 "i"(圆点 + 竖线),品牌蓝(两主题一致,对应原版小蓝圈-i)。 +QIcon makeInfoIcon() { + const QColor accent = tokenColor("accent/primary"); + QPainter p; + QPixmap pm = makeToolIconCanvas(p); + const qreal s = kToolIconPx; + QPen pen(accent, 1.4); + pen.setCapStyle(Qt::RoundCap); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + // 外圈(留 1.5px 边距,避免描边被裁)。 + const qreal m = 1.5; + p.drawEllipse(QRectF(m, m, s - 2 * m, s - 2 * m)); + // "i":上点 + 下竖线(居中)。 + const qreal cx = s / 2.0; + p.setBrush(accent); + p.setPen(Qt::NoPen); + p.drawEllipse(QPointF(cx, s * 0.34), 0.9, 0.9); // 点 + QPen stem(accent, 1.4); + stem.setCapStyle(Qt::RoundCap); + p.setPen(stem); + p.drawLine(QPointF(cx, s * 0.46), QPointF(cx, s * 0.70)); // 竖 + p.end(); + pm.setDevicePixelRatio(kToolIconScale); + return QIcon(pm); +} + +// 框选图标:虚线方框(选区)+ 右下角小箭头光标。描边随主题(次要文本色,两主题可见)。 +QIcon makeMarqueeIcon() { + const QColor stroke = tokenColor("text/secondary"); + QPainter p; + QPixmap pm = makeToolIconCanvas(p); + const qreal s = kToolIconPx; + // 虚线选区框(左上偏移,给右下角箭头让位)。 + QPen dash(stroke, 1.3); + dash.setCapStyle(Qt::FlatCap); + dash.setJoinStyle(Qt::MiterJoin); + QVector pattern{2.0, 1.6}; + dash.setDashPattern(pattern); + p.setPen(dash); + p.setBrush(Qt::NoBrush); + p.drawRect(QRectF(2.0, 2.0, s - 6.0, s - 6.0)); + // 右下角小箭头光标(实线填充,指向右下)。 + QPainterPath cur; + const qreal ax = s - 5.0, ay = s - 5.0; + cur.moveTo(ax, ay); + cur.lineTo(ax + 4.6, ay + 1.8); + cur.lineTo(ax + 1.9, ay + 2.6); + cur.lineTo(ax + 1.1, ay + 4.9); + cur.closeSubpath(); + p.setPen(Qt::NoPen); + p.setBrush(stroke); + p.drawPath(cur); + p.end(); + pm.setDevicePixelRatio(kToolIconScale); + return QIcon(pm); +} + +// 把占位 QToolButton 配成图标按钮:清文字、设图标 + 固定尺寸(与工具条其它按钮一致高度)。 +void styleToolIconButton(QToolButton* btn, const QIcon& icon) { + btn->setText(QString()); + btn->setIcon(icon); + btn->setIconSize(QSize(kToolIconPx, kToolIconPx)); + btn->setAutoRaise(true); + btn->setFixedSize(QSize(28, 28)); // 与下拉/按钮行高协调;不再过小或锯齿 + btn->setCursor(Qt::PointingHandCursor); +} + +} // namespace + +void RawDataChartView::showNotImplemented(QWidget* anchor) { + // 轻提示:占位按钮/下拉点击 → 暂未实现(不阻塞,不弹窗)。 + const QPoint pos = anchor ? anchor->mapToGlobal(QPoint(0, anchor->height())) + : QCursor::pos(); + QToolTip::showText(pos, QStringLiteral("暂未实现"), anchor); +} + +void RawDataChartView::replotForAxis() { + // 本地换 x/y(无网络):按下拉 fieldCode 从备选列取数据,重设 scatter.x/.y 并重绘。 + if (!xCombo_ || !yCombo_ || !scatterItem_) return; + const QString xCode = xCombo_->currentData().toString(); + const QString yCode = yCombo_->currentData().toString(); + + if (xCode == QStringLiteral("horizontalDistance") && !data_.altXHorizontal.empty()) + data_.scatter.x = data_.altXHorizontal; + else if (!data_.altXSlope.empty()) + data_.scatter.x = data_.altXSlope; // 默认/斜距 + + if (yCode == QStringLiteral("elevationPseudoDepth") && !data_.altYElevationPseudo.empty()) + data_.scatter.y = data_.altYElevationPseudo; + else if (yCode == QStringLiteral("pseudoDepth") && !data_.altYPseudo.empty()) + data_.scatter.y = data_.altYPseudo; + // 层数(Layer No):数据为 null → 不改轴(保持当前),选项可选但 no-op。 + + scatterItem_->setData(data_.scatter, colorSvc_); + if (hoverTip_) hoverTip_->setField(&data_.scatter); + QRectF bbox = scatterItem_->boundingRect(); + if (!bbox.isEmpty()) { + plot_->setAxisScale(QwtPlot::xTop, bbox.left(), bbox.right()); + plot_->setAxisScale(QwtPlot::yLeft, bbox.top(), bbox.bottom()); + } + plot_->updateAxes(); + if (rescaler_) rescaler_->rescale(); + plot_->replot(); +} + +void RawDataChartView::buildMeasurementToolbar(const geopro::core::ScatterToolbarConf& conf) { + auto* toolbar = new QWidget(this); + auto* tbLay = new QHBoxLayout(toolbar); + tbLay->setContentsMargins(4, 4, 4, 4); + tbLay->setSpacing(4); + + // [i] info + [▣] 框选:占位(暂未实现)。用 QPainter 画的线性图标(HiDPI 清晰,随主题)。 + auto* btnInfo = new QToolButton(toolbar); + btnInfo->setToolTip(QStringLiteral("信息")); + styleToolIconButton(btnInfo, makeInfoIcon()); + connect(btnInfo, &QToolButton::clicked, this, [this, btnInfo]() { showNotImplemented(btnInfo); }); + auto* btnMarquee = new QToolButton(toolbar); + btnMarquee->setToolTip(QStringLiteral("框选")); + styleToolIconButton(btnMarquee, makeMarqueeIcon()); + connect(btnMarquee, &QToolButton::clicked, this, [this, btnMarquee]() { showNotImplemented(btnMarquee); }); + // 主题热切:重绘图标(info 锚定品牌蓝,marquee 描边随次要文本色)。 + connect(&ThemeManager::instance(), &ThemeManager::changed, btnInfo, + [btnInfo]() { btnInfo->setIcon(makeInfoIcon()); }); + connect(&ThemeManager::instance(), &ThemeManager::changed, btnMarquee, + [btnMarquee]() { btnMarquee->setIcon(makeMarqueeIcon()); }); + + // 显示 / 隐藏:功能性——切换全部数据方块可见性(电极保留)。 + auto* btnShow = new QPushButton(QStringLiteral("显示"), toolbar); + auto* btnHide = new QPushButton(QStringLiteral("隐藏"), toolbar); + connect(btnShow, &QPushButton::clicked, this, [this]() { + if (scatterItem_) { scatterItem_->setScatterVisible(true); plot_->replot(); } + }); + connect(btnHide, &QPushButton::clicked, this, [this]() { + if (scatterItem_) { scatterItem_->setScatterVisible(false); plot_->replot(); } + }); + + // 数据过滤:占位(暂未实现)。 + auto* btnFilter = new QPushButton(QStringLiteral("数据过滤"), toolbar); + connect(btnFilter, &QPushButton::clicked, this, [this, btnFilter]() { showNotImplemented(btnFilter); }); + + // x / y 下拉:功能性(本地换列重绘);v / method 下拉:视觉占位(选不同 v/method 提示暂未实现)。 + xCombo_ = new QComboBox(toolbar); + fillCombo(xCombo_, conf.x, conf.defaultX, QString()); + yCombo_ = new QComboBox(toolbar); + fillCombo(yCombo_, conf.y, conf.defaultY, QString()); + auto* vCombo = new QComboBox(toolbar); + fillCombo(vCombo, conf.v, conf.defaultV, QString()); + auto* methodCombo = new QComboBox(toolbar); + fillCombo(methodCombo, conf.method, QString(), conf.defaultMethod); + + connect(xCombo_, QOverload::of(&QComboBox::currentIndexChanged), this, + [this](int) { replotForAxis(); }); + connect(yCombo_, QOverload::of(&QComboBox::currentIndexChanged), this, + [this](int) { replotForAxis(); }); + // v / method:换选项 → 暂未实现(需重新请求散点/色阶,属重交互,本轮不做)。 + connect(vCombo, QOverload::of(&QComboBox::activated), this, + [this, vCombo](int) { showNotImplemented(vCombo); }); + connect(methodCombo, QOverload::of(&QComboBox::activated), this, + [this, methodCombo](int) { showNotImplemented(methodCombo); }); + + // 色阶配置:占位(暂未实现)。 + auto* btnColorScale = new QPushButton(QStringLiteral("色阶配置"), toolbar); + connect(btnColorScale, &QPushButton::clicked, this, [this, btnColorScale]() { showNotImplemented(btnColorScale); }); + + // 右侧主操作(蓝色):生成视电阻率数据 / 反演运算 / 另存为 —— 占位(暂未实现)。 + auto* btnGen = new QPushButton(QStringLiteral("生成视电阻率数据"), toolbar); + auto* btnInvert = new QPushButton(QStringLiteral("反演运算"), toolbar); + auto* btnSaveAs = new QPushButton(QStringLiteral("另存为"), toolbar); + for (auto* b : {btnGen, btnInvert, btnSaveAs}) { + b->setObjectName(QStringLiteral("primaryBtn")); // 蓝色主按钮(下方 QSS) + connect(b, &QPushButton::clicked, this, [this, b]() { showNotImplemented(b); }); + } + + tbLay->addWidget(btnInfo); + tbLay->addWidget(btnMarquee); + tbLay->addWidget(btnShow); + tbLay->addWidget(btnHide); + tbLay->addWidget(btnFilter); + tbLay->addWidget(xCombo_); + tbLay->addWidget(yCombo_); + tbLay->addWidget(vCombo); + tbLay->addWidget(methodCombo); + tbLay->addWidget(btnColorScale); + tbLay->addStretch(); // 把主操作推到右侧 + tbLay->addWidget(btnGen); + tbLay->addWidget(btnInvert); + tbLay->addWidget(btnSaveAs); + + // 蓝色主按钮样式(随主题;普通按钮/下拉走全局 QSS 已支持明暗)。 + applyTokenizedStyleSheet( + toolbar, + QStringLiteral( + "QPushButton#primaryBtn { background: {{accent/primary}}; color: {{text/on-primary}};" + " border: 1px solid {{accent/primary}}; border-radius: 6px; padding: 6px 14px; }" + "QPushButton#primaryBtn:hover { background: {{accent/primary-hover}}; border-color: {{accent/primary-hover}}; }" + "QPushButton#primaryBtn:pressed { background: {{accent/primary-pressed}}; }")); + + // 替换 ctor 建的 inversion 工具条(同一顶层布局首位)。 + rootLay_->replaceWidget(toolbar_, toolbar); + toolbar_->deleteLater(); + toolbar_ = toolbar; + chartTypeCombo_ = nullptr; // 已随旧工具条移除 + measurementToolbar_ = true; +} + +void RawDataChartView::setPayload(const QVariant& payload) { + if (!payload.canConvert()) { + // 坏/空 variant:保持空态(不渲染、不崩)。E2+ 可在此显式提示「渲染数据格式错误」。 + return; + } + setData(payload.value()); +} + +void RawDataChartView::setData(const geopro::core::ScatterPayload& p) { + data_ = p; + if (hoverTip_) hoverTip_->setField(&data_.scatter); // 显式重绑(地址稳定,消除隐式依赖) + + // measurement 载荷(toolbar 非空):首次到来时建并替换工具条(视觉 1:1)。反演留空 → 不动。 + if (!p.toolbar.empty() && !measurementToolbar_) buildMeasurementToolbar(p.toolbar); + + if (p.scale.empty()) return; + + // 重建 ColorMapService(旧的 scatterItem 已被 QwtPlot detach/delete) + delete colorSvc_; + colorSvc_ = new ColorMapService(p.scale); + + // 散点颜色归一化对齐原版 Plotly(cmin/cmax 未设 → cauto=数据 min/max),按 vlist 有限值 + // 的 min/max 设数据范围,使整段色阶铺满数据实际范围。measurement 与反演原数据同此路径: + // v 含负异常值(如 -1066),故 cmin<0,中段视电阻率归一化到色阶中部(深品红/紫)。 + { + double vMin = std::numeric_limits::max(); + double vMax = std::numeric_limits::lowest(); + for (double v : p.scatter.v) { + if (!std::isfinite(v)) continue; // 跳过 NaN/±Inf(脏数据),否则数据范围被污染→全图 NaN 取色 + if (v < vMin) vMin = v; + if (v > vMax) vMax = v; + } + if (vMin <= vMax) colorSvc_->setDataRange(vMin, vMax); + } + + // 卸载旧散点项:QwtPlot 默认 autoDelete=true(析构时 delete 仍在 dict 的 item)。 + // 必须先 detach()(从 dict 移除)再 delete,否则 QwtPlot 析构时会 double-free。 + if (scatterItem_) { + scatterItem_->detach(); + delete scatterItem_; + scatterItem_ = nullptr; + } + + // 新建散点项并挂到 plot + scatterItem_ = new ScatterPlotItem(); + scatterItem_->setData(p.scatter, colorSvc_); + scatterItem_->attach(plot_); + + // 按数据包围盒设置轴范围 + QRectF bbox = scatterItem_->boundingRect(); + if (!bbox.isEmpty()) { + plot_->setAxisScale(QwtPlot::xTop, bbox.left(), bbox.right()); + plot_->setAxisScale(QwtPlot::yLeft, bbox.top(), bbox.bottom()); + } + plot_->updateAxes(); + if (rescaler_) rescaler_->rescale(); // 应用真实比尺 + plot_->replot(); + + // 更新色阶条:measurement 用右侧竖条,反演原数据用底部横条(二选一显示)。 + if (p.verticalLegend) { + colorBarV_->setColorScale(p.scale); + colorBarV_->setVisible(true); + colorBar_->setVisible(false); + } else { + colorBar_->setColorScale(p.scale); + colorBar_->setVisible(true); + colorBarV_->setVisible(false); + } +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/RawDataChartView.hpp b/src/app/panels/chart/RawDataChartView.hpp new file mode 100644 index 0000000..38b2c51 --- /dev/null +++ b/src/app/panels/chart/RawDataChartView.hpp @@ -0,0 +1,63 @@ +#pragma once +#include +#include "model/detail/DetailPayloads.hpp" +#include "panels/chart/ColorMapService.hpp" +#include "panels/chart/IDetailView.hpp" + +class QComboBox; +class QVBoxLayout; +class QwtPlot; +class QwtPlotRescaler; + +namespace geopro::app { + +class ColorBarWidget; +class ScatterPlotItem; +class ScatterHoverTip; + +// 原数据图表视图:工具条 + QwtPlot(x 轴顶部、Panner/Magnifier)+ 独立色阶条。 +class RawDataChartView : public QWidget, public IDetailView { + Q_OBJECT +public: + explicit RawDataChartView(QWidget* parent = nullptr); + ~RawDataChartView() override; + + // 渲染散点载荷(解包后调内部渲染;渲染代码不变)。 + void setData(const geopro::core::ScatterPayload& p); + + // IDetailView:壳/工厂统一入口。坏/空 variant → 保持空态不崩。 + QWidget* widget() override { return this; } + void setPayload(const QVariant& payload) override; + + // 供外部访问(已不再是占位,保留兼容接口返回 plot_) + QWidget* plotArea() const; + +private: + // 工具条按载荷类型二选一:反演原数据 = ctor 默认建的 inversion 工具条;measurement = + // 首个非空 ScatterToolbarConf 到来时建一次并替换(视觉 1:1)。建好后缓存,后续 setData 复用。 + void buildMeasurementToolbar(const geopro::core::ScatterToolbarConf& conf); + // 按当前 x/y 下拉选择,从备选列重绘散点(本地,无网络)。code 为下拉项 fieldCode。 + void replotForAxis(); + // “暂未实现”轻提示(占位按钮/下拉点击)。 + void showNotImplemented(QWidget* anchor); + + geopro::core::ScatterPayload data_; + QwtPlot* plot_; + QwtPlotRescaler* rescaler_ = nullptr; // 锁定 x:y 真实比尺 + ColorBarWidget* colorBar_; // 底部横条(反演原数据) + ColorBarWidget* colorBarV_; // 右侧竖条(measurement) + QComboBox* chartTypeCombo_ = nullptr; + + QVBoxLayout* rootLay_ = nullptr; // 顶层竖向布局(用于替换工具条) + QWidget* toolbar_ = nullptr; // 当前工具条(inversion 或 measurement) + bool measurementToolbar_ = false; // 已建 measurement 工具条 + QComboBox* xCombo_ = nullptr; // measurement x 下拉 + QComboBox* yCombo_ = nullptr; // measurement y 下拉 + + // 使用 unique_ptr 管理生命周期;attach 后 QwtPlot 接管绘制,但我们仍持有指针 + ColorMapService* colorSvc_ = nullptr; // heap,由 setData 重建 + ScatterPlotItem* scatterItem_ = nullptr; + ScatterHoverTip* hoverTip_ = nullptr; // 散点 hover 提示(QObject,this 持有) +}; + +} // namespace geopro::app diff --git a/src/app/panels/chart/ScatterHoverTip.cpp b/src/app/panels/chart/ScatterHoverTip.cpp new file mode 100644 index 0000000..1150190 --- /dev/null +++ b/src/app/panels/chart/ScatterHoverTip.cpp @@ -0,0 +1,110 @@ +#include "panels/chart/ScatterHoverTip.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace geopro::app { + +ScatterHoverTip::ScatterHoverTip(QwtPlot* plot, int xAxis, int yAxis, QObject* parent) + : QObject(parent), plot_(plot), xAxis_(xAxis), yAxis_(yAxis) { + if (plot_ && plot_->canvas()) { + // 默认 widget 仅在按键按下时才收到 MouseMove;hover(无按键)需开启鼠标跟踪。 + plot_->canvas()->setMouseTracking(true); + plot_->canvas()->installEventFilter(this); + } +} + +bool ScatterHoverTip::eventFilter(QObject* obj, QEvent* ev) { + if (!plot_ || obj != plot_->canvas()) return QObject::eventFilter(obj, ev); + + if (ev->type() == QEvent::Leave) { + QToolTip::hideText(); + return false; + } + if (ev->type() != QEvent::MouseMove) return QObject::eventFilter(obj, ev); + + auto* me = static_cast(ev); + // 拖动平移中(有按键)不弹提示——交给 LivePanner;无数据则跳过。 + if (me->buttons() != Qt::NoButton || !field_) return false; + + const auto& xs = field_->x; + const auto& ys = field_->y; + const auto& vs = field_->v; + const std::size_t n = std::min(xs.size(), ys.size()); + + // 在像素空间找最近散点(与 ScatterPlotItem 同用 canvasMap)。 + const QwtScaleMap xMap = plot_->canvasMap(xAxis_); + const QwtScaleMap yMap = plot_->canvasMap(yAxis_); + const QPointF mp = me->position(); + + // 电极 hover(y=0 行,与数据点 hover 分开):电极 marker 略大于数据方块, + // 命中半径稍大。命中则显示 x/y/num 浮动框(measurement 专有,反演 electrodeX 空→跳过)。 + { + const auto& ex = field_->electrodeX; + const auto& eno = field_->electrodeNo; + const double ey0 = yMap.transform(0.0); + double eBestD2 = std::numeric_limits::max(); + std::size_t eBestI = 0; + for (std::size_t i = 0; i < ex.size(); ++i) { + const double dx = xMap.transform(ex[i]) - mp.x(); + const double dy = ey0 - mp.y(); + const double d2 = dx * dx + dy * dy; + if (d2 < eBestD2) { + eBestD2 = d2; + eBestI = i; + } + } + if (eBestD2 <= kElectrodeHitRadiusPx * kElectrodeHitRadiusPx) { + const int num = (eBestI < eno.size()) + ? static_cast(eno[eBestI]) + : static_cast(eBestI) + 1; + QToolTip::showText(me->globalPosition().toPoint(), + electrodeHoverText(ex[eBestI], num), plot_->canvas()); + return false; + } + } + + if (n == 0) { + QToolTip::hideText(); + return false; + } + + double bestD2 = std::numeric_limits::max(); + std::size_t bestI = 0; + for (std::size_t i = 0; i < n; ++i) { + const double dx = xMap.transform(xs[i]) - mp.x(); + const double dy = yMap.transform(ys[i]) - mp.y(); + const double d2 = dx * dx + dy * dy; + if (d2 < bestD2) { + bestD2 = d2; + bestI = i; + } + } + + if (bestD2 <= kHitRadiusPx * kHitRadiusPx) { + const double v = (bestI < vs.size()) ? vs[bestI] : 0.0; + // 有 A/B/M/N 元数据(measurement)→ 浮动框含 X/Y/Value/a/b/m/n;否则回退 X/Y/值(反演原数据)。 + const auto& a = field_->a; + QString text; + if (bestI < a.size() && bestI < field_->b.size() && bestI < field_->m.size() && + bestI < field_->n.size()) { + text = measurementHoverText(xs[bestI], ys[bestI], v, a[bestI], field_->b[bestI], + field_->m[bestI], field_->n[bestI]); + } else { + text = scatterHoverText(xs[bestI], ys[bestI], v); + } + QToolTip::showText(me->globalPosition().toPoint(), text, plot_->canvas()); + } else { + QToolTip::hideText(); + } + return false; // 不消费,保留其它过滤器(LivePanner)链路 +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/ScatterHoverTip.hpp b/src/app/panels/chart/ScatterHoverTip.hpp new file mode 100644 index 0000000..ac1916e --- /dev/null +++ b/src/app/panels/chart/ScatterHoverTip.hpp @@ -0,0 +1,69 @@ +#pragma once +#include +#include +#include "model/Field.hpp" // core::ScatterField + +class QwtPlot; + +namespace geopro::app { + +// 格式化散点 hover 提示文本,对齐原版 Plotly hovertemplate: +// X: {x:.3f}
Y: {y:.3f}
值: {v:.3f} +// inline 纯函数:无 qwt 依赖,可独立单测。 +inline QString scatterHoverText(double x, double y, double v) { + return QStringLiteral("X: %1
Y: %2
值: %3") + .arg(x, 0, 'f', 3) + .arg(y, 0, 'f', 3) + .arg(v, 0, 'f', 3); +} + +// measurement 散点 hover 浮动框:对齐原版 Plotly 浮动框多行 +// X / Y / Value(视电阻率 col16) / a / b / m / n。 +inline QString measurementHoverText(double x, double y, double v, + double a, double b, double m, double n) { + return QStringLiteral( + "X: %1
Y: %2
Value: %3" + "
a: %4
b: %5
m: %6
n: %7") + .arg(x, 0, 'f', 3) + .arg(y, 0, 'f', 3) + .arg(v, 0, 'f', 3) + .arg(a, 0, 'g', 6) + .arg(b, 0, 'g', 6) + .arg(m, 0, 'g', 6) + .arg(n, 0, 'g', 6); +} + +// 电极 hover 浮动框:对齐原版 Plotly 电极 trace 浮动框(与数据点 hover 分开), +// x: {slopeDistance}
y: 0
num: {electrodeNo} +// x 用 6 位有效数字(对齐原版,如 5.123837),num 为电极编号(1-based 整数)。 +inline QString electrodeHoverText(double x, int num) { + return QStringLiteral("x: %1
y: 0
num: %2") + .arg(x, 0, 'g', 7) + .arg(num); +} + +// 散点 hover 提示:监听画布鼠标移动(无按键时),找最近散点, +// 命中半径内用 QToolTip 显示 X/Y/值(对齐原版 Plotly 悬浮)。 +// 不消费事件,与 LivePanner(左键平移/滚轮缩放)共存。 +class ScatterHoverTip : public QObject { + Q_OBJECT +public: + ScatterHoverTip(QwtPlot* plot, int xAxis, int yAxis, QObject* parent = nullptr); + + // 数据由 RawDataChartView 持有(其成员 data_.scatter,地址稳定);本类只读不拥有。 + void setField(const core::ScatterField* field) { field_ = field; } + +protected: + bool eventFilter(QObject* obj, QEvent* ev) override; + +private: + QwtPlot* plot_; + int xAxis_; + int yAxis_; + const core::ScatterField* field_ = nullptr; + + static constexpr double kHitRadiusPx = 6.0; // 命中半径(像素),对齐方块 marker 尺寸 + static constexpr double kElectrodeHitRadiusPx = 7.0; // 电极菱形略大,命中半径稍大 +}; + +} // namespace geopro::app diff --git a/src/app/panels/chart/ScatterPlotItem.cpp b/src/app/panels/chart/ScatterPlotItem.cpp new file mode 100644 index 0000000..85a051a --- /dev/null +++ b/src/app/panels/chart/ScatterPlotItem.cpp @@ -0,0 +1,101 @@ +#include "panels/chart/ScatterPlotItem.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace geopro::app { + +ScatterPlotItem::ScatterPlotItem() + : QwtPlotItem() { + setTitle("Scatter"); + setRenderHint(QwtPlotItem::RenderAntialiased, false); + // 关联到 x 顶轴(原数据 x 轴在顶部;与 RawDataChartView 的 setAxisScale(xTop,...) 一致, + // 否则项默认用 xBottom 的(未设置/自动)刻度 → 散点被错误压缩)。 + setXAxis(QwtPlot::xTop); + setYAxis(QwtPlot::yLeft); +} + +void ScatterPlotItem::setData(const core::ScatterField& field, ColorMapService* svc) { + field_ = field; + colorSvc_ = svc; + + // 计算数据包围盒 + if (field_.x.empty() || field_.y.empty()) { + bounding_ = QRectF(); + return; + } + double xMin = *std::min_element(field_.x.begin(), field_.x.end()); + double xMax = *std::max_element(field_.x.begin(), field_.x.end()); + double yMin = *std::min_element(field_.y.begin(), field_.y.end()); + double yMax = *std::max_element(field_.y.begin(), field_.y.end()); + // 电极 marker 在 y=0、x=electrodeX 处:纳入包围盒,避免电极被截。 + for (double ex : field_.electrodeX) { xMin = std::min(xMin, ex); xMax = std::max(xMax, ex); } + if (!field_.electrodeX.empty()) { yMin = std::min(yMin, 0.0); yMax = std::max(yMax, 0.0); } + // 加少量 margin,避免边界点被截 + double xM = (xMax - xMin) * 0.03 + 0.1; + double yM = (yMax - yMin) * 0.03 + 0.1; + bounding_ = QRectF(xMin - xM, yMin - yM, + (xMax - xMin) + 2.0 * xM, + (yMax - yMin) + 2.0 * yM); +} + +QRectF ScatterPlotItem::boundingRect() const { + return bounding_; +} + +void ScatterPlotItem::draw(QPainter* painter, + const QwtScaleMap& xMap, + const QwtScaleMap& yMap, + const QRectF& /*canvasRect*/) const { + if (!colorSvc_) return; + const auto& xs = field_.x; + const auto& ys = field_.y; + const auto& vs = field_.v; + // x/y/v 来自服务端独立数组,长度可能不一致——取 min 防越界。 + const std::size_t n = std::min(xs.size(), ys.size()); + if (n == 0) return; + + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, false); + + // 电极位置:y=0 处灰色菱形(先画,置于散点下层)。 + // 原版 Plotly 电极 marker:实心 #BEBEBE(190,190,190) 填充 + 白色 2px 描边, + // 菱形比数据方块略大(size 16 vs 12)。 + if (!field_.electrodeX.empty()) { + const double y0 = yMap.transform(0.0); + painter->setPen(QPen(Qt::white, kElectrodePenWidth)); + painter->setBrush(QColor(190, 190, 190)); + const double h = kElectrodeHalfSide; + for (double ex : field_.electrodeX) { + const double px = xMap.transform(ex); + QPolygonF dia; + dia << QPointF(px, y0 - h) << QPointF(px + h, y0) + << QPointF(px, y0 + h) << QPointF(px - h, y0); + painter->drawPolygon(dia); + } + } + + // 隐藏数据方块:电极已绘,直接收尾(保留电极菱形)。 + if (!scatterVisible_) { painter->restore(); return; } + + painter->setPen(QPen(Qt::white, kPenWidth)); + for (std::size_t i = 0; i < n; ++i) { + double px = xMap.transform(xs[i]); + double py = yMap.transform(ys[i]); + double val = (i < vs.size()) ? vs[i] : 0.0; + // 非有限值(NaN/±Inf,可能来自降级后端的脏数据):跳过该点,不绘制。 + // 与 RawDataChartView 的 setDataRange 的 isfinite 跳过一致。 + if (!std::isfinite(val)) continue; + auto c = colorSvc_->colorAtContinuous(val); + painter->setBrush(QColor(c.r, c.g, c.b, c.a)); + painter->drawRect(QRectF(px - kHalfSide, py - kHalfSide, + kHalfSide * 2.0, kHalfSide * 2.0)); + } + painter->restore(); +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/ScatterPlotItem.hpp b/src/app/panels/chart/ScatterPlotItem.hpp new file mode 100644 index 0000000..3b39b32 --- /dev/null +++ b/src/app/panels/chart/ScatterPlotItem.hpp @@ -0,0 +1,49 @@ +#pragma once +#include "model/Field.hpp" +#include "panels/chart/ColorMapService.hpp" +#include +#include + +class QPainter; +class QwtScaleMap; + +namespace geopro::app { + +// QwtPlotItem:把 ScatterField 数据渲染为彩色方块散点。 +// 每个点用固定像素边长方块绘制(不随缩放变大),白色描边, +// 颜色由 ColorMapService 连续插值决定(cauto,与原版 Plotly 一致;measurement/反演同路径)。 +class ScatterPlotItem : public QwtPlotItem { +public: + ScatterPlotItem(); + + // 按 ColorMapService 连续插值上色(cauto);数据范围由调用方在 svc 上 setDataRange。 + void setData(const core::ScatterField& field, ColorMapService* svc); + + // 显示/隐藏数据方块(measurement 工具条“显示/隐藏”)。电极菱形不受影响,始终绘制。 + void setScatterVisible(bool on) { scatterVisible_ = on; } + + int rtti() const override { return QwtPlotItem::Rtti_PlotUserItem; } + + QRectF boundingRect() const override; + + void draw(QPainter* painter, + const QwtScaleMap& xMap, + const QwtScaleMap& yMap, + const QRectF& canvasRect) const override; + +private: + core::ScatterField field_; + ColorMapService* colorSvc_ = nullptr; // 不拥有,由 RawDataChartView 持有 + QRectF bounding_; + bool scatterVisible_ = true; // false:仅画电极,隐藏数据方块 + + // 数据方块:原版 Plotly marker.size 12px(绝对像素直径)→ 半边长 6px,真 1:1。 + static constexpr double kHalfSide = 6.0; // 方块半边长(像素,全宽 12px) + static constexpr double kPenWidth = 1.0; // 白色描边宽度(像素) + // 电极菱形:原版 Plotly marker.size 16px(绝对像素直径)→ 半对角 8px,真 1:1。 + // 相对数据方块 ≈1.33×(16 vs 12),实心 #BEBEBE 填充 + 白色 2px 描边。 + static constexpr double kElectrodeHalfSide = 8.0; // 半对角(像素,全宽 16px) + static constexpr double kElectrodePenWidth = 2.0; // 电极白色描边宽度(像素) +}; + +} // namespace geopro::app diff --git a/src/app/panels/chart/TablePager.cpp b/src/app/panels/chart/TablePager.cpp new file mode 100644 index 0000000..3656647 --- /dev/null +++ b/src/app/panels/chart/TablePager.cpp @@ -0,0 +1,188 @@ +#include "panels/chart/TablePager.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace geopro::app { + +namespace { +// 每页条数选项(实测原版 vxe-pager 下拉:50/100/500/1000,默认 50)。 +const int kPageSizes[] = {50, 100, 500, 1000}; +constexpr int kPagerCount = 7; // 页码窗口(>此值用 … 折叠,对齐 vxe 默认) +constexpr int kJumpStep = 5; // 点击 … 的跳页步长 + +// 分页器样式:常态不设字色(跟随主题/全局样式表);边框用半透明灰(两主题通用); +// hover/选中页用强调蓝 #409EFF(与 DataTableView 开关同色)。 +const char* kPagerQss = R"( +QToolButton { + border: 1px solid rgba(128,128,128,0.35); + border-radius: 3px; + padding: 1px 6px; + min-width: 16px; + min-height: 20px; + background: transparent; +} +QToolButton:hover:enabled { color: #409EFF; border-color: #409EFF; } +QToolButton[active="true"] { color: #409EFF; border-color: #409EFF; font-weight: bold; } +QToolButton:disabled { color: rgba(128,128,128,0.55); } +)"; +} // namespace + +TablePager::TablePager(QWidget* parent) : QWidget(parent) { + setStyleSheet(QString::fromUtf8(kPagerQss)); + + auto* lay = new QHBoxLayout(this); + lay->setContentsMargins(8, 6, 8, 6); + lay->setSpacing(6); + lay->addStretch(1); // 右对齐 + + prevBtn_ = new QToolButton(this); + prevBtn_->setText(QStringLiteral("‹")); + prevBtn_->setCursor(Qt::PointingHandCursor); + connect(prevBtn_, &QToolButton::clicked, this, + [this] { emit pageRequested(pageNo_ - 1, pageSize_); }); + lay->addWidget(prevBtn_); + + numHost_ = new QWidget(this); + numLay_ = new QHBoxLayout(numHost_); + numLay_->setContentsMargins(0, 0, 0, 0); + numLay_->setSpacing(6); + lay->addWidget(numHost_); + + nextBtn_ = new QToolButton(this); + nextBtn_->setText(QStringLiteral("›")); + nextBtn_->setCursor(Qt::PointingHandCursor); + connect(nextBtn_, &QToolButton::clicked, this, + [this] { emit pageRequested(pageNo_ + 1, pageSize_); }); + lay->addWidget(nextBtn_); + + auto* gotoLabel = new QLabel(QStringLiteral("前往"), this); + lay->addWidget(gotoLabel); + + jumpEdit_ = new QLineEdit(this); + jumpEdit_->setFixedWidth(40); + jumpEdit_->setAlignment(Qt::AlignCenter); + jumpValidator_ = new QIntValidator(1, 1, jumpEdit_); + jumpEdit_->setValidator(jumpValidator_); + connect(jumpEdit_, &QLineEdit::returnPressed, this, [this] { + bool ok = false; + int p = jumpEdit_->text().toInt(&ok); + const int pc = pageCount(); + if (!ok) { + jumpEdit_->setText(QString::number(pageNo_)); + return; + } + p = std::min(std::max(1, p), std::max(1, pc)); + if (p == pageNo_) { + jumpEdit_->setText(QString::number(pageNo_)); + return; + } + emit pageRequested(p, pageSize_); + }); + lay->addWidget(jumpEdit_); + + lay->addWidget(new QLabel(QStringLiteral("页"), this)); + + sizeCombo_ = new QComboBox(this); + for (int s : kPageSizes) + sizeCombo_->addItem(QStringLiteral("%1条/页").arg(s), s); + connect(sizeCombo_, &QComboBox::activated, this, [this](int i) { + emit pageRequested(1, sizeCombo_->itemData(i).toInt()); // 改每页条数 → 回第 1 页 + }); + lay->addWidget(sizeCombo_); + + totalLabel_ = new QLabel(this); + lay->addWidget(totalLabel_); + + setState(0, 1, pageSize_); +} + +int TablePager::pageCount() const { + if (pageSize_ <= 0) return 1; + return std::max(1, (total_ + pageSize_ - 1) / pageSize_); +} + +void TablePager::setState(int total, int pageNo, int pageSize) { + total_ = std::max(0, total); + if (pageSize > 0) pageSize_ = pageSize; + const int pc = pageCount(); + pageNo_ = std::min(std::max(1, pageNo), pc); + + prevBtn_->setEnabled(pageNo_ > 1); + nextBtn_->setEnabled(pageNo_ < pc); + + jumpValidator_->setTop(pc); + { + QSignalBlocker b(jumpEdit_); + jumpEdit_->setText(QString::number(pageNo_)); + } + { + QSignalBlocker b(sizeCombo_); + int idx = sizeCombo_->findData(pageSize_); + if (idx < 0) { // 非预设条数(兜底):补一项再选中 + sizeCombo_->addItem(QStringLiteral("%1条/页").arg(pageSize_), pageSize_); + idx = sizeCombo_->findData(pageSize_); + } + sizeCombo_->setCurrentIndex(idx); + } + totalLabel_->setText(QStringLiteral("共 %1 条记录").arg(total_)); + + rebuildNumbers(); +} + +void TablePager::rebuildNumbers() { + // 清空旧页码按钮。 + while (QLayoutItem* it = numLay_->takeAt(0)) { + if (QWidget* w = it->widget()) w->deleteLater(); + delete it; + } + + const int pc = pageCount(); + auto addNum = [this](int p, bool active) { + auto* b = new QToolButton(numHost_); + b->setText(QString::number(p)); + b->setCursor(Qt::PointingHandCursor); + b->setProperty("active", active); + b->setEnabled(!active); // 当前页不可再点 + connect(b, &QToolButton::clicked, this, [this, p] { emit pageRequested(p, pageSize_); }); + numLay_->addWidget(b); + }; + auto addDots = [this](int target) { + auto* b = new QToolButton(numHost_); + b->setText(QStringLiteral("...")); + b->setCursor(Qt::PointingHandCursor); + connect(b, &QToolButton::clicked, this, + [this, target] { emit pageRequested(target, pageSize_); }); + numLay_->addWidget(b); + }; + + if (pc <= kPagerCount) { + for (int p = 1; p <= pc; ++p) addNum(p, p == pageNo_); + return; + } + // 折叠窗口:首页 [ … ] 中段(当前±2) [ … ] 末页。 + addNum(1, pageNo_ == 1); + int left = pageNo_ - 2; + int right = pageNo_ + 2; + if (left < 2) { + left = 2; + right = 5; + } + if (right > pc - 1) { + right = pc - 1; + left = pc - 4; + } + if (left > 2) addDots(std::max(1, pageNo_ - kJumpStep)); + for (int p = left; p <= right; ++p) addNum(p, p == pageNo_); + if (right < pc - 1) addDots(std::min(pc, pageNo_ + kJumpStep)); + addNum(pc, pageNo_ == pc); +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/TablePager.hpp b/src/app/panels/chart/TablePager.hpp new file mode 100644 index 0000000..7509f9f --- /dev/null +++ b/src/app/panels/chart/TablePager.hpp @@ -0,0 +1,48 @@ +#pragma once +#include + +class QToolButton; +class QLineEdit; +class QComboBox; +class QLabel; +class QHBoxLayout; +class QIntValidator; + +namespace geopro::app { + +// 分页器(对齐原版 vxe-pager,size--mini):上一页 / 页码(多页带 … 省略跳页)/ 下一页 + +// 「前往 [n] 页」跳页框 + 每页条数下拉 [50/100/500/1000] + 「共 N 条记录」。右对齐。 +// 数据驱动:setState(total,pageNo,pageSize) 重建页码并同步各控件。任何翻页/改每页条数都发 +// pageRequested(改每页条数时回到第 1 页,镜像原版)。仅展示与请求,不持有数据。 +// 配色:常态随主题(不显式设色,跟随全局样式表);hover/选中页用强调蓝 #409EFF(与表格开关同色)。 +class TablePager : public QWidget { + Q_OBJECT +public: + explicit TablePager(QWidget* parent = nullptr); + + // 用总数/当前页/每页条数刷新分页器(重建页码按钮、同步跳页框/下拉/总数文案)。 + void setState(int total, int pageNo, int pageSize); + +signals: + // 请求加载某页(翻页/跳页/改每页条数)。改每页条数时 pageNo=1。 + void pageRequested(int pageNo, int pageSize); + +private: + int pageCount() const; // ceil(total/pageSize),至少 1 + void rebuildNumbers(); // 按当前页重建页码按钮(含 … 跳页) + + int total_ = 0; + int pageNo_ = 1; + int pageSize_ = 50; + + QToolButton* prevBtn_ = nullptr; + QToolButton* nextBtn_ = nullptr; + QWidget* numHost_ = nullptr; + QHBoxLayout* numLay_ = nullptr; + QLineEdit* jumpEdit_ = nullptr; + QIntValidator* jumpValidator_ = nullptr; + QComboBox* sizeCombo_ = nullptr; + QLabel* totalLabel_ = nullptr; +}; + +} // namespace geopro::app diff --git a/src/app/panels/chart/TrajectoryMapView.cpp b/src/app/panels/chart/TrajectoryMapView.cpp new file mode 100644 index 0000000..8c09deb --- /dev/null +++ b/src/app/panels/chart/TrajectoryMapView.cpp @@ -0,0 +1,58 @@ +#include "panels/chart/TrajectoryMapView.hpp" + +#include +#include +#include +#include +#include +#include + +namespace geopro::app { + +namespace { + +// MapPayload → 紧凑 JSON 数组串 "[{\"no\":1,\"lon\":..,\"lat\":..}, ...]"(供页面 setPoints)。 +QString pointsToJson(const geopro::core::MapPayload& m) { + QJsonArray arr; + for (const auto& p : m.points) { + QJsonObject o; + o.insert(QStringLiteral("no"), p.electrodeNo); + o.insert(QStringLiteral("lon"), p.lon); + o.insert(QStringLiteral("lat"), p.lat); + arr.append(o); + } + return QString::fromUtf8(QJsonDocument(arr).toJson(QJsonDocument::Compact)); +} + +} // namespace + +TrajectoryMapView::TrajectoryMapView(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); + lay->setContentsMargins(0, 0, 0, 0); + lay->setSpacing(0); + + view_ = new QWebEngineView(this); + lay->addWidget(view_, 1); + + // 页面加载完成 → 标记就绪并注入待推送的点(载荷可能先到)。 + QObject::connect(view_, &QWebEngineView::loadFinished, this, [this](bool ok) { + pageLoaded_ = ok; + if (ok) injectPoints(); + }); + + view_->load(QUrl(QStringLiteral("qrc:/map/trajectory_map.html"))); +} + +void TrajectoryMapView::setPayload(const QVariant& payload) { + if (!payload.canConvert()) return; // 坏/空 → 忽略 + pendingJson_ = pointsToJson(payload.value()); + if (pageLoaded_) injectPoints(); // 页面已就绪则立即注入,否则待 loadFinished +} + +void TrajectoryMapView::injectPoints() { + if (!pageLoaded_ || pendingJson_.isEmpty()) return; + // 无 QWebChannel:单向经 runJavaScript 调页面侧 setPoints(arr)。 + view_->page()->runJavaScript(QStringLiteral("setPoints(%1)").arg(pendingJson_)); +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/TrajectoryMapView.hpp b/src/app/panels/chart/TrajectoryMapView.hpp new file mode 100644 index 0000000..6fa6d2c --- /dev/null +++ b/src/app/panels/chart/TrajectoryMapView.hpp @@ -0,0 +1,35 @@ +#pragma once +#include +#include + +#include "model/detail/DetailPayloads.hpp" +#include "panels/chart/IDetailView.hpp" + +class QWebEngineView; + +namespace geopro::app { + +// 轨迹「地图」页签视图(dd_trajectory_data 地图页签): +// QWebEngineView 加载本地 MapLibre GL 地图页(qrc:/map/trajectory_map.html),WebGL 渲染, +// 底图用天地图 WMTS 栅格瓦片 + 注记瓦片,含街道/卫星/地形三套底图切换(仅瓦片联网, +// MapLibre 库本地内置于 qrc)。 +// setPayload 解包 MapPayload(电极经纬点),构造 JSON 数组后经 page()->runJavaScript() +// 调页面侧 setPoints(arr) 画实心橙点 + 连线 + 起点标记 + 取景;无 QWebChannel(未安装),单向推数据。 +// 载荷可能早于页面 loadFinished 到达 → 暂存,待 loadFinished 后再注入(幂等)。 +class TrajectoryMapView : public QWidget, public IDetailView { + Q_OBJECT +public: + explicit TrajectoryMapView(QWidget* parent = nullptr); + + QWidget* widget() override { return this; } + void setPayload(const QVariant& payload) override; // 坏/空 variant → 忽略 + +private: + void injectPoints(); // 把 pendingJson_ 推入页面(仅页面就绪时) + + QWebEngineView* view_ = nullptr; + QString pendingJson_; // 最近一次载荷的 JS 数组串("[]" 表示无点) + bool pageLoaded_ = false; +}; + +} // namespace geopro::app diff --git a/src/app/panels/chart/TrajectoryStrategy.hpp b/src/app/panels/chart/TrajectoryStrategy.hpp new file mode 100644 index 0000000..8b63b9a --- /dev/null +++ b/src/app/panels/chart/TrajectoryStrategy.hpp @@ -0,0 +1,25 @@ +#pragma once +#include +#include "IDatasetChartStrategy.hpp" // geopro::controller +namespace geopro::app { + +// ERT 电极坐标轨迹(dd_trajectory_data)策略:地图 / 列表 / 高程 三页签(页签序对齐原版)。 +// 地图(默认/首位)= WebMap(QWebEngineView + Leaflet + 天地图瓦片;traj.map 产 MapPayload, +// 端点 trajectory/line,electrodeCoordinate → 经纬点)。 +// 列表 = Table(懒加载,gridHeaderDisplay+rowList → TablePayload)。 +// 高程 = LineProfile(平滑折线,electrodeNo→elevation)。 +// 列表与高程同端点 trajectory/rows,loaderKey 不同(traj.rows 产 TablePayload / traj.elev 产 LinePayload)。 +struct TrajectoryStrategy : controller::IDatasetChartStrategy { + std::string ddCode() const override { return "dd_trajectory_data"; } + std::vector tabs() const override { + return { + {QStringLiteral("地图"), controller::ViewKind::WebMap, + QStringLiteral("traj.map"), /*lazy*/ false, /*paginated*/ false}, + {QStringLiteral("列表"), controller::ViewKind::Table, + QStringLiteral("traj.rows"), /*lazy*/ true, /*paginated*/ false}, + {QStringLiteral("高程"), controller::ViewKind::LineProfile, + QStringLiteral("traj.elev"), /*lazy*/ false, /*paginated*/ false}, + }; + } +}; +} // namespace geopro::app diff --git a/src/app/resources/map/map.qrc b/src/app/resources/map/map.qrc new file mode 100644 index 0000000..a89ab2d --- /dev/null +++ b/src/app/resources/map/map.qrc @@ -0,0 +1,8 @@ + + + + trajectory_map.html + maplibre-gl.js + maplibre-gl.css + + diff --git a/src/app/resources/map/maplibre-gl.css b/src/app/resources/map/maplibre-gl.css new file mode 100644 index 0000000..6ceb5de --- /dev/null +++ b/src/app/resources/map/maplibre-gl.css @@ -0,0 +1 @@ +.maplibregl-map{font:12px/20px Helvetica Neue,Arial,Helvetica,sans-serif;overflow:hidden;position:relative;-webkit-tap-highlight-color:rgb(0 0 0/0)}.maplibregl-canvas{left:0;position:absolute;top:0}.maplibregl-map:fullscreen{height:100%;width:100%}.maplibregl-ctrl-group button.maplibregl-ctrl-compass{touch-action:none}.maplibregl-canvas-container.maplibregl-interactive,.maplibregl-ctrl-group button.maplibregl-ctrl-compass{cursor:grab;-webkit-user-select:none;-moz-user-select:none;user-select:none}.maplibregl-canvas-container.maplibregl-interactive.maplibregl-track-pointer{cursor:pointer}.maplibregl-canvas-container.maplibregl-interactive:active,.maplibregl-ctrl-group button.maplibregl-ctrl-compass:active{cursor:grabbing}.maplibregl-canvas-container.maplibregl-touch-zoom-rotate,.maplibregl-canvas-container.maplibregl-touch-zoom-rotate .maplibregl-canvas{touch-action:pan-x pan-y}.maplibregl-canvas-container.maplibregl-touch-drag-pan,.maplibregl-canvas-container.maplibregl-touch-drag-pan .maplibregl-canvas{touch-action:pinch-zoom}.maplibregl-canvas-container.maplibregl-touch-zoom-rotate.maplibregl-touch-drag-pan,.maplibregl-canvas-container.maplibregl-touch-zoom-rotate.maplibregl-touch-drag-pan .maplibregl-canvas{touch-action:none}.maplibregl-canvas-container.maplibregl-touch-drag-pan.maplibregl-cooperative-gestures,.maplibregl-canvas-container.maplibregl-touch-drag-pan.maplibregl-cooperative-gestures .maplibregl-canvas{touch-action:pan-x pan-y}.maplibregl-ctrl-bottom-left,.maplibregl-ctrl-bottom-right,.maplibregl-ctrl-top-left,.maplibregl-ctrl-top-right{pointer-events:none;position:absolute;z-index:2}.maplibregl-ctrl-top-left{left:0;top:0}.maplibregl-ctrl-top-right{right:0;top:0}.maplibregl-ctrl-bottom-left{bottom:0;left:0}.maplibregl-ctrl-bottom-right{bottom:0;right:0}.maplibregl-ctrl{clear:both;pointer-events:auto;transform:translate(0)}.maplibregl-ctrl-top-left .maplibregl-ctrl{float:left;margin:10px 0 0 10px}.maplibregl-ctrl-top-right .maplibregl-ctrl{float:right;margin:10px 10px 0 0}.maplibregl-ctrl-bottom-left .maplibregl-ctrl{float:left;margin:0 0 10px 10px}.maplibregl-ctrl-bottom-right .maplibregl-ctrl{float:right;margin:0 10px 10px 0}.maplibregl-ctrl-group{background:#fff;border-radius:4px}.maplibregl-ctrl-group:not(:empty){box-shadow:0 0 0 2px rgba(0,0,0,.1)}@media (forced-colors:active){.maplibregl-ctrl-group:not(:empty){box-shadow:0 0 0 2px ButtonText}}.maplibregl-ctrl-group button{background-color:transparent;border:0;box-sizing:border-box;cursor:pointer;display:block;height:29px;outline:none;padding:0;width:29px}.maplibregl-ctrl-group button+button{border-top:1px solid #ddd}.maplibregl-ctrl button .maplibregl-ctrl-icon{background-position:50%;background-repeat:no-repeat;display:block;height:100%;width:100%}@media (forced-colors:active){.maplibregl-ctrl-icon{background-color:transparent}.maplibregl-ctrl-group button+button{border-top:1px solid ButtonText}}.maplibregl-ctrl button::-moz-focus-inner{border:0;padding:0}.maplibregl-ctrl-attrib-button:focus,.maplibregl-ctrl-group button:focus{box-shadow:0 0 2px 2px #0096ff}.maplibregl-ctrl button:disabled{cursor:not-allowed}.maplibregl-ctrl button:disabled .maplibregl-ctrl-icon{opacity:.25}.maplibregl-ctrl button:not(:disabled):hover{background-color:rgb(0 0 0/5%)}.maplibregl-ctrl-group button:focus:focus-visible{box-shadow:0 0 2px 2px #0096ff}.maplibregl-ctrl-group button:focus:not(:focus-visible){box-shadow:none}.maplibregl-ctrl-group button:focus:first-child{border-radius:4px 4px 0 0}.maplibregl-ctrl-group button:focus:last-child{border-radius:0 0 4px 4px}.maplibregl-ctrl-group button:focus:only-child{border-radius:inherit}.maplibregl-ctrl button.maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5'/%3E%3C/svg%3E")}@media (forced-colors:active){.maplibregl-ctrl button.maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5'/%3E%3C/svg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl button.maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 29 29'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 29 29'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5'/%3E%3C/svg%3E")}}.maplibregl-ctrl button.maplibregl-ctrl-fullscreen .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-shrink .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 29 29'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1z'/%3E%3C/svg%3E")}@media (forced-colors:active){.maplibregl-ctrl button.maplibregl-ctrl-fullscreen .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-shrink .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1z'/%3E%3C/svg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl button.maplibregl-ctrl-fullscreen .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 29 29'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-shrink .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 29 29'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1z'/%3E%3C/svg%3E")}}.maplibregl-ctrl button.maplibregl-ctrl-compass .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='m10.5 14 4-8 4 8z'/%3E%3Cpath fill='%23ccc' d='m10.5 16 4 8 4-8z'/%3E%3C/svg%3E")}@media (forced-colors:active){.maplibregl-ctrl button.maplibregl-ctrl-compass .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='m10.5 14 4-8 4 8z'/%3E%3Cpath fill='%23ccc' d='m10.5 16 4 8 4-8z'/%3E%3C/svg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl button.maplibregl-ctrl-compass .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 29 29'%3E%3Cpath d='m10.5 14 4-8 4 8z'/%3E%3Cpath fill='%23ccc' d='m10.5 16 4 8 4-8z'/%3E%3C/svg%3E")}}.maplibregl-ctrl button.maplibregl-ctrl-terrain .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' fill='%23333' viewBox='0 0 22 22'%3E%3Cpath d='m1.754 13.406 4.453-4.851 3.09 3.09 3.281 3.277.969-.969-3.309-3.312 3.844-4.121 6.148 6.886h1.082v-.855l-7.207-8.07-4.84 5.187L6.169 6.57l-5.48 5.965v.871ZM.688 16.844h20.625v1.375H.688Zm0 0'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-terrain-enabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' fill='%2333b5e5' viewBox='0 0 22 22'%3E%3Cpath d='m1.754 13.406 4.453-4.851 3.09 3.09 3.281 3.277.969-.969-3.309-3.312 3.844-4.121 6.148 6.886h1.082v-.855l-7.207-8.07-4.84 5.187L6.169 6.57l-5.48 5.965v.871ZM.688 16.844h20.625v1.375H.688Zm0 0'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23333' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate:disabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23aaa' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3Cpath fill='red' d='m14 5 1 1-9 9-1-1z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%2333b5e5' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active-error .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23e58978' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%2333b5e5' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background-error .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23e54e33' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-waiting .maplibregl-ctrl-icon{animation:maplibregl-spin 2s linear infinite}@media (forced-colors:active){.maplibregl-ctrl button.maplibregl-ctrl-geolocate .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate:disabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23999' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3Cpath fill='red' d='m14 5 1 1-9 9-1-1z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%2333b5e5' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active-error .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23e58978' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%2333b5e5' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background-error .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23e54e33' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3C/svg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl button.maplibregl-ctrl-geolocate .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate:disabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23666' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3Cpath fill='red' d='m14 5 1 1-9 9-1-1z'/%3E%3C/svg%3E")}}@keyframes maplibregl-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}a.maplibregl-ctrl-logo{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='23' fill='none'%3E%3Cpath fill='%23000' fill-opacity='.4' fill-rule='evenodd' d='M17.408 16.796h-1.827l2.501-12.095h.198l3.324 6.533.988 2.19.988-2.19 3.258-6.533h.181l2.6 12.095h-1.81l-1.218-5.644-.362-1.71-.658 1.71-2.929 5.644h-.098l-2.914-5.644-.757-1.71-.345 1.71zm1.958-3.42-.726 3.663a1.255 1.255 0 0 1-1.232 1.011h-1.827a1.255 1.255 0 0 1-1.229-1.509l2.501-12.095a1.255 1.255 0 0 1 1.23-1.001h.197a1.25 1.25 0 0 1 1.12.685l3.19 6.273 3.125-6.263a1.25 1.25 0 0 1 1.123-.695h.181a1.255 1.255 0 0 1 1.227.991l1.443 6.71a5 5 0 0 1 .314-.787l.009-.016a4.6 4.6 0 0 1 1.777-1.887c.782-.46 1.668-.667 2.611-.667a4.6 4.6 0 0 1 1.7.32l.306.134c.21-.16.474-.256.759-.256h1.694a1.255 1.255 0 0 1 1.212.925 1.255 1.255 0 0 1 1.212-.925h1.711c.284 0 .545.094.755.252.613-.3 1.312-.45 2.075-.45 1.356 0 2.557.445 3.482 1.4q.47.48.763 1.064V4.701a1.255 1.255 0 0 1 1.255-1.255h1.86A1.255 1.255 0 0 1 54.44 4.7v9.194h2.217c.19 0 .37.043.532.118v-4.77c0-.356.147-.678.385-.906a2.42 2.42 0 0 1-.682-1.71c0-.665.267-1.253.735-1.7a2.45 2.45 0 0 1 1.722-.674 2.43 2.43 0 0 1 1.705.675q.318.302.504.683V4.7a1.255 1.255 0 0 1 1.255-1.255h1.744A1.255 1.255 0 0 1 65.812 4.7v3.335a4.8 4.8 0 0 1 1.526-.246c.938 0 1.817.214 2.59.69a4.47 4.47 0 0 1 1.67 1.743v-.98a1.255 1.255 0 0 1 1.256-1.256h1.777c.233 0 .451.064.639.174a3.4 3.4 0 0 1 1.567-.372c.346 0 .861.02 1.285.232a1.25 1.25 0 0 1 .689 1.004 4.7 4.7 0 0 1 .853-.588c.795-.44 1.675-.647 2.61-.647 1.385 0 2.65.39 3.525 1.396.836.938 1.168 2.173 1.168 3.528q-.001.515-.056 1.051a1.255 1.255 0 0 1-.947 1.09l.408.952a1.255 1.255 0 0 1-.477 1.552c-.418.268-.92.463-1.458.612-.613.171-1.304.244-2.049.244-1.06 0-2.043-.207-2.886-.698l-.015-.008c-.798-.48-1.419-1.135-1.818-1.963l-.004-.008a5.8 5.8 0 0 1-.548-2.512q0-.429.053-.843a1.3 1.3 0 0 1-.333-.086l-.166-.004c-.223 0-.426.062-.643.228-.03.024-.142.139-.142.59v3.883a1.255 1.255 0 0 1-1.256 1.256h-1.777a1.255 1.255 0 0 1-1.256-1.256V15.69l-.032.057a4.8 4.8 0 0 1-1.86 1.833 5.04 5.04 0 0 1-2.484.634 4.5 4.5 0 0 1-1.935-.424 1.25 1.25 0 0 1-.764.258h-1.71a1.255 1.255 0 0 1-1.256-1.255V7.687a2.4 2.4 0 0 1-.428.625c.253.23.412.561.412.93v7.553a1.255 1.255 0 0 1-1.256 1.255h-1.843a1.25 1.25 0 0 1-.894-.373c-.228.23-.544.373-.894.373H51.32a1.255 1.255 0 0 1-1.256-1.255v-1.251l-.061.117a4.7 4.7 0 0 1-1.782 1.884 4.77 4.77 0 0 1-2.485.67 5.6 5.6 0 0 1-1.485-.188l.009 2.764a1.255 1.255 0 0 1-1.255 1.259h-1.729a1.255 1.255 0 0 1-1.255-1.255v-3.537a1.255 1.255 0 0 1-1.167.793h-1.679a1.25 1.25 0 0 1-.77-.263 4.5 4.5 0 0 1-1.945.429c-.885 0-1.724-.21-2.495-.632l-.017-.01a5 5 0 0 1-1.081-.836 1.255 1.255 0 0 1-1.254 1.312h-1.81a1.255 1.255 0 0 1-1.228-.99l-.782-3.625-2.044 3.939a1.25 1.25 0 0 1-1.115.676h-.098a1.25 1.25 0 0 1-1.116-.68l-2.061-3.994zM35.92 16.63l.207-.114.223-.15q.493-.356.735-.785l.061-.118.033 1.332h1.678V9.242h-1.694l-.033 1.267q-.133-.329-.526-.658l-.032-.028a3.2 3.2 0 0 0-.668-.428l-.27-.12a3.3 3.3 0 0 0-1.235-.23q-1.136-.001-1.974.493a3.36 3.36 0 0 0-1.3 1.382q-.445.89-.444 2.074 0 1.2.51 2.107a3.8 3.8 0 0 0 1.382 1.381 3.9 3.9 0 0 0 1.893.477q.795 0 1.455-.33zm-2.789-5.38q-.576.675-.575 1.762 0 1.102.559 1.794.576.675 1.645.675a2.25 2.25 0 0 0 .934-.19 2.2 2.2 0 0 0 .468-.29l.178-.161a2.2 2.2 0 0 0 .397-.561q.244-.5.244-1.15v-.115q0-.708-.296-1.267l-.043-.077a2.2 2.2 0 0 0-.633-.709l-.13-.086-.047-.028a2.1 2.1 0 0 0-1.073-.285q-1.052 0-1.629.692zm2.316 2.706c.163-.17.28-.407.28-.83v-.114c0-.292-.06-.508-.15-.68a.96.96 0 0 0-.353-.389.85.85 0 0 0-.464-.127c-.4 0-.56.114-.664.239l-.01.012c-.148.174-.275.45-.275.945 0 .506.122.801.27.99.097.11.266.224.68.224.303 0 .504-.09.687-.269zm7.545 1.705a2.6 2.6 0 0 0 .331.423q.319.33.755.548l.173.074q.65.255 1.49.255 1.02 0 1.844-.493a3.45 3.45 0 0 0 1.316-1.4q.493-.904.493-2.089 0-1.909-.988-2.913-.988-1.02-2.584-1.02-.898 0-1.575.347a3 3 0 0 0-.415.262l-.199.166a3.4 3.4 0 0 0-.64.82V9.242h-1.712v11.553h1.729l-.017-5.134zm.53-1.138q.206.29.48.5l.155.11.053.034q.51.296 1.119.297 1.07 0 1.645-.675.577-.69.576-1.762 0-1.119-.576-1.777-.558-.675-1.645-.675-.435 0-.835.16a2 2 0 0 0-.284.136 2 2 0 0 0-.363.254 2.2 2.2 0 0 0-.46.569l-.082.162a2.6 2.6 0 0 0-.213 1.072v.115q0 .707.296 1.267l.135.211zm.964-.818a1.1 1.1 0 0 0 .367.385.94.94 0 0 0 .476.118c.423 0 .59-.117.687-.23.159-.194.28-.478.28-.95 0-.53-.133-.8-.266-.952l-.021-.025c-.078-.094-.231-.221-.68-.221a1 1 0 0 0-.503.135l-.012.007a.86.86 0 0 0-.335.343c-.073.133-.132.324-.132.614v.115a1.4 1.4 0 0 0 .14.66zm15.7-6.222q.347-.346.346-.856a1.05 1.05 0 0 0-.345-.79 1.18 1.18 0 0 0-.84-.329q-.51 0-.855.33a1.05 1.05 0 0 0-.346.79q0 .51.346.855.345.346.856.346.51 0 .839-.346zm4.337 9.314.033-1.332q.191.403.59.747l.098.081a4 4 0 0 0 .316.224l.223.122a3.2 3.2 0 0 0 1.44.322 3.8 3.8 0 0 0 1.875-.477 3.5 3.5 0 0 0 1.382-1.366q.527-.89.526-2.09 0-1.184-.444-2.073a3.24 3.24 0 0 0-1.283-1.399q-.823-.51-1.942-.51a3.5 3.5 0 0 0-1.527.344l-.086.043-.165.09a3 3 0 0 0-.33.214q-.432.315-.656.707a2 2 0 0 0-.099.198l.082-1.283V4.701h-1.744v12.095zm.473-2.509a2.5 2.5 0 0 0 .566.7q.117.098.245.18l.144.08a2.1 2.1 0 0 0 .975.232q1.07 0 1.645-.675.576-.69.576-1.778 0-1.102-.576-1.777-.56-.691-1.645-.692a2.2 2.2 0 0 0-1.015.235q-.22.113-.415.282l-.15.142a2.1 2.1 0 0 0-.42.594q-.223.479-.223 1.1v.115q0 .705.293 1.26zm2.616-.293c.157-.191.28-.479.28-.967 0-.51-.13-.79-.276-.961l-.021-.026c-.082-.1-.232-.225-.67-.225a.87.87 0 0 0-.681.279l-.012.011c-.154.155-.274.38-.274.807v.115c0 .285.057.499.144.669a1.1 1.1 0 0 0 .367.405c.137.082.28.123.455.123.423 0 .59-.118.686-.23zm8.266-3.013q.345-.13.724-.14l.069-.002q.493 0 .642.099l.247-1.794q-.196-.099-.717-.099a2.3 2.3 0 0 0-.545.063 2 2 0 0 0-.411.148 2.2 2.2 0 0 0-.4.249 2.5 2.5 0 0 0-.485.499 2.7 2.7 0 0 0-.32.581l-.05.137v-1.48h-1.778v7.553h1.777v-3.884q0-.546.159-.943a1.5 1.5 0 0 1 .466-.636 2.5 2.5 0 0 1 .399-.253 2 2 0 0 1 .224-.099zm9.784 2.656.05-.922q0-1.743-.856-2.698-.838-.97-2.584-.97-1.119-.001-2.007.493a3.46 3.46 0 0 0-1.4 1.382q-.493.906-.493 2.106 0 1.07.428 1.975.428.89 1.332 1.432.906.526 2.255.526.973 0 1.668-.185l.044-.012.135-.04q.613-.184.984-.421l-.542-1.267q-.3.162-.642.274l-.297.087q-.51.131-1.3.131-.954 0-1.497-.444a1.6 1.6 0 0 1-.192-.193q-.366-.44-.512-1.234l-.004-.021zm-5.427-1.256-.003.022h3.752v-.138q-.011-.727-.288-1.118a1 1 0 0 0-.156-.176q-.46-.428-1.316-.428-.986 0-1.494.604-.379.45-.494 1.234zm-27.053 2.77V4.7h-1.86v12.095h5.333V15.15zm7.103-5.908v7.553h-1.843V9.242h1.843z'/%3E%3Cpath fill='%23fff' d='m19.63 11.151-.757-1.71-.345 1.71-1.12 5.644h-1.827L18.083 4.7h.197l3.325 6.533.988 2.19.988-2.19L26.839 4.7h.181l2.6 12.095h-1.81l-1.218-5.644-.362-1.71-.658 1.71-2.93 5.644h-.098l-2.913-5.644zm14.836 5.81q-1.02 0-1.893-.478a3.8 3.8 0 0 1-1.381-1.382q-.51-.906-.51-2.106 0-1.185.444-2.074a3.36 3.36 0 0 1 1.3-1.382q.839-.494 1.974-.494a3.3 3.3 0 0 1 1.234.231 3.3 3.3 0 0 1 .97.575q.396.33.527.659l.033-1.267h1.694v7.553H37.18l-.033-1.332q-.279.593-1.02 1.053a3.17 3.17 0 0 1-1.662.444zm.296-1.482q.938 0 1.58-.642.642-.66.642-1.711v-.115q0-.708-.296-1.267a2.2 2.2 0 0 0-.807-.872 2.1 2.1 0 0 0-1.119-.313q-1.053 0-1.629.692-.575.675-.575 1.76 0 1.103.559 1.795.577.675 1.645.675zm6.521-6.237h1.711v1.4q.906-1.597 2.83-1.597 1.596 0 2.584 1.02.988 1.005.988 2.914 0 1.185-.493 2.09a3.46 3.46 0 0 1-1.316 1.399 3.5 3.5 0 0 1-1.844.493q-.954 0-1.662-.329a2.67 2.67 0 0 1-1.086-.97l.017 5.134h-1.728zm4.048 6.22q1.07 0 1.645-.674.577-.69.576-1.762 0-1.119-.576-1.777-.558-.675-1.645-.675-.592 0-1.12.296-.51.28-.822.823-.296.527-.296 1.234v.115q0 .708.296 1.267.313.543.823.855.51.296 1.119.297z'/%3E%3Cpath fill='%23e1e3e9' d='M51.325 4.7h1.86v10.45h3.473v1.646h-5.333zm7.12 4.542h1.843v7.553h-1.843zm.905-1.415a1.16 1.16 0 0 1-.856-.346 1.17 1.17 0 0 1-.346-.856 1.05 1.05 0 0 1 .346-.79q.346-.329.856-.329.494 0 .839.33a1.05 1.05 0 0 1 .345.79 1.16 1.16 0 0 1-.345.855q-.33.346-.84.346zm7.875 9.133a3.17 3.17 0 0 1-1.662-.444q-.723-.46-1.004-1.053l-.033 1.332h-1.71V4.701h1.743v4.657l-.082 1.283q.279-.658 1.086-1.119a3.5 3.5 0 0 1 1.778-.477q1.119 0 1.942.51a3.24 3.24 0 0 1 1.283 1.4q.445.888.444 2.072 0 1.201-.526 2.09a3.5 3.5 0 0 1-1.382 1.366 3.8 3.8 0 0 1-1.876.477zm-.296-1.481q1.069 0 1.645-.675.577-.69.577-1.778 0-1.102-.577-1.776-.56-.691-1.645-.692a2.12 2.12 0 0 0-1.58.659q-.642.641-.642 1.694v.115q0 .71.296 1.267a2.4 2.4 0 0 0 .807.872 2.1 2.1 0 0 0 1.119.313zm5.927-6.237h1.777v1.481q.263-.757.856-1.217a2.14 2.14 0 0 1 1.349-.46q.527 0 .724.098l-.247 1.794q-.149-.099-.642-.099-.774 0-1.416.494-.626.493-.626 1.58v3.883h-1.777V9.242zm9.534 7.718q-1.35 0-2.255-.526-.904-.543-1.332-1.432a4.6 4.6 0 0 1-.428-1.975q0-1.2.493-2.106a3.46 3.46 0 0 1 1.4-1.382q.889-.495 2.007-.494 1.744 0 2.584.97.855.956.856 2.7 0 .444-.05.92h-5.43q.18 1.005.708 1.45.542.443 1.497.443.79 0 1.3-.131a4 4 0 0 0 .938-.362l.542 1.267q-.411.263-1.119.46-.708.198-1.711.197zm1.596-4.558q.016-1.02-.444-1.432-.46-.428-1.316-.428-1.728 0-1.991 1.86z'/%3E%3Cpath d='M5.074 15.948a.484.657 0 0 0-.486.659v1.84a.484.657 0 0 0 .486.659h4.101a.484.657 0 0 0 .486-.659v-1.84a.484.657 0 0 0-.486-.659zm3.56 1.16H5.617v.838h3.017z' style='fill:%23fff;fill-rule:evenodd;stroke-width:1.03600001'/%3E%3Cg style='stroke-width:1.12603545'%3E%3Cpath d='M-9.408-1.416c-3.833-.025-7.056 2.912-7.08 6.615-.02 3.08 1.653 4.832 3.107 6.268.903.892 1.721 1.74 2.32 2.902l-.525-.004c-.543-.003-.992.304-1.24.639a1.87 1.87 0 0 0-.362 1.121l-.011 1.877c-.003.402.104.787.347 1.125.244.338.688.653 1.23.656l4.142.028c.542.003.99-.306 1.238-.641a1.87 1.87 0 0 0 .363-1.121l.012-1.875a1.87 1.87 0 0 0-.348-1.127c-.243-.338-.688-.653-1.23-.656l-.518-.004c.597-1.145 1.425-1.983 2.348-2.87 1.473-1.414 3.18-3.149 3.2-6.226-.016-3.59-2.923-6.684-6.993-6.707m-.006 1.1v.002c3.274.02 5.92 2.532 5.9 5.6-.017 2.706-1.39 4.026-2.863 5.44-1.034.994-2.118 2.033-2.814 3.633-.018.041-.052.055-.075.065q-.013.004-.02.01a.34.34 0 0 1-.226.084.34.34 0 0 1-.224-.086l-.092-.077c-.699-1.615-1.768-2.669-2.781-3.67-1.454-1.435-2.797-2.762-2.78-5.478.02-3.067 2.7-5.545 5.975-5.523m-.02 2.826c-1.62-.01-2.944 1.315-2.955 2.96-.01 1.646 1.295 2.988 2.916 2.999h.002c1.621.01 2.943-1.316 2.953-2.961.011-1.646-1.294-2.988-2.916-2.998m-.005 1.1c1.017.006 1.829.83 1.822 1.89s-.83 1.874-1.848 1.867c-1.018-.006-1.829-.83-1.822-1.89s.83-1.874 1.848-1.868m-2.155 11.857 4.14.025c.271.002.49.305.487.676l-.013 1.875c-.003.37-.224.67-.495.668l-4.14-.025c-.27-.002-.487-.306-.485-.676l.012-1.875c.003-.37.224-.67.494-.668' style='color:%23000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:evenodd;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:%23000;solid-opacity:1;vector-effect:none;fill:%23000;fill-opacity:.4;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto' transform='translate(15.553 2.85)scale(.88807)'/%3E%3Cpath d='M-9.415-.316C-12.69-.338-15.37 2.14-15.39 5.207c-.017 2.716 1.326 4.041 2.78 5.477 1.013 1 2.081 2.055 2.78 3.67l.092.076a.34.34 0 0 0 .225.086.34.34 0 0 0 .227-.083l.019-.01c.022-.009.057-.024.074-.064.697-1.6 1.78-2.64 2.814-3.634 1.473-1.414 2.847-2.733 2.864-5.44.02-3.067-2.627-5.58-5.901-5.601m-.057 8.784c1.621.011 2.944-1.315 2.955-2.96.01-1.646-1.295-2.988-2.916-2.999-1.622-.01-2.945 1.315-2.955 2.96s1.295 2.989 2.916 3' style='clip-rule:evenodd;fill:%23e1e3e9;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:.4' transform='translate(15.553 2.85)scale(.88807)'/%3E%3Cpath d='M-11.594 15.465c-.27-.002-.492.297-.494.668l-.012 1.876c-.003.371.214.673.485.675l4.14.027c.271.002.492-.298.495-.668l.012-1.877c.003-.37-.215-.672-.485-.674z' style='clip-rule:evenodd;fill:%23fff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:.4' transform='translate(15.553 2.85)scale(.88807)'/%3E%3C/g%3E%3C/svg%3E");background-repeat:no-repeat;cursor:pointer;display:block;height:23px;margin:0 0 -4px -4px;overflow:hidden;width:88px}a.maplibregl-ctrl-logo.maplibregl-compact{width:14px}@media (forced-colors:active){a.maplibregl-ctrl-logo{background-color:transparent;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='23' fill='none'%3E%3Cpath fill='%23000' fill-opacity='.4' fill-rule='evenodd' d='M17.408 16.796h-1.827l2.501-12.095h.198l3.324 6.533.988 2.19.988-2.19 3.258-6.533h.181l2.6 12.095h-1.81l-1.218-5.644-.362-1.71-.658 1.71-2.929 5.644h-.098l-2.914-5.644-.757-1.71-.345 1.71zm1.958-3.42-.726 3.663a1.255 1.255 0 0 1-1.232 1.011h-1.827a1.255 1.255 0 0 1-1.229-1.509l2.501-12.095a1.255 1.255 0 0 1 1.23-1.001h.197a1.25 1.25 0 0 1 1.12.685l3.19 6.273 3.125-6.263a1.25 1.25 0 0 1 1.123-.695h.181a1.255 1.255 0 0 1 1.227.991l1.443 6.71a5 5 0 0 1 .314-.787l.009-.016a4.6 4.6 0 0 1 1.777-1.887c.782-.46 1.668-.667 2.611-.667a4.6 4.6 0 0 1 1.7.32l.306.134c.21-.16.474-.256.759-.256h1.694a1.255 1.255 0 0 1 1.212.925 1.255 1.255 0 0 1 1.212-.925h1.711c.284 0 .545.094.755.252.613-.3 1.312-.45 2.075-.45 1.356 0 2.557.445 3.482 1.4q.47.48.763 1.064V4.701a1.255 1.255 0 0 1 1.255-1.255h1.86A1.255 1.255 0 0 1 54.44 4.7v9.194h2.217c.19 0 .37.043.532.118v-4.77c0-.356.147-.678.385-.906a2.42 2.42 0 0 1-.682-1.71c0-.665.267-1.253.735-1.7a2.45 2.45 0 0 1 1.722-.674 2.43 2.43 0 0 1 1.705.675q.318.302.504.683V4.7a1.255 1.255 0 0 1 1.255-1.255h1.744A1.255 1.255 0 0 1 65.812 4.7v3.335a4.8 4.8 0 0 1 1.526-.246c.938 0 1.817.214 2.59.69a4.47 4.47 0 0 1 1.67 1.743v-.98a1.255 1.255 0 0 1 1.256-1.256h1.777c.233 0 .451.064.639.174a3.4 3.4 0 0 1 1.567-.372c.346 0 .861.02 1.285.232a1.25 1.25 0 0 1 .689 1.004 4.7 4.7 0 0 1 .853-.588c.795-.44 1.675-.647 2.61-.647 1.385 0 2.65.39 3.525 1.396.836.938 1.168 2.173 1.168 3.528q-.001.515-.056 1.051a1.255 1.255 0 0 1-.947 1.09l.408.952a1.255 1.255 0 0 1-.477 1.552c-.418.268-.92.463-1.458.612-.613.171-1.304.244-2.049.244-1.06 0-2.043-.207-2.886-.698l-.015-.008c-.798-.48-1.419-1.135-1.818-1.963l-.004-.008a5.8 5.8 0 0 1-.548-2.512q0-.429.053-.843a1.3 1.3 0 0 1-.333-.086l-.166-.004c-.223 0-.426.062-.643.228-.03.024-.142.139-.142.59v3.883a1.255 1.255 0 0 1-1.256 1.256h-1.777a1.255 1.255 0 0 1-1.256-1.256V15.69l-.032.057a4.8 4.8 0 0 1-1.86 1.833 5.04 5.04 0 0 1-2.484.634 4.5 4.5 0 0 1-1.935-.424 1.25 1.25 0 0 1-.764.258h-1.71a1.255 1.255 0 0 1-1.256-1.255V7.687a2.4 2.4 0 0 1-.428.625c.253.23.412.561.412.93v7.553a1.255 1.255 0 0 1-1.256 1.255h-1.843a1.25 1.25 0 0 1-.894-.373c-.228.23-.544.373-.894.373H51.32a1.255 1.255 0 0 1-1.256-1.255v-1.251l-.061.117a4.7 4.7 0 0 1-1.782 1.884 4.77 4.77 0 0 1-2.485.67 5.6 5.6 0 0 1-1.485-.188l.009 2.764a1.255 1.255 0 0 1-1.255 1.259h-1.729a1.255 1.255 0 0 1-1.255-1.255v-3.537a1.255 1.255 0 0 1-1.167.793h-1.679a1.25 1.25 0 0 1-.77-.263 4.5 4.5 0 0 1-1.945.429c-.885 0-1.724-.21-2.495-.632l-.017-.01a5 5 0 0 1-1.081-.836 1.255 1.255 0 0 1-1.254 1.312h-1.81a1.255 1.255 0 0 1-1.228-.99l-.782-3.625-2.044 3.939a1.25 1.25 0 0 1-1.115.676h-.098a1.25 1.25 0 0 1-1.116-.68l-2.061-3.994zM35.92 16.63l.207-.114.223-.15q.493-.356.735-.785l.061-.118.033 1.332h1.678V9.242h-1.694l-.033 1.267q-.133-.329-.526-.658l-.032-.028a3.2 3.2 0 0 0-.668-.428l-.27-.12a3.3 3.3 0 0 0-1.235-.23q-1.136-.001-1.974.493a3.36 3.36 0 0 0-1.3 1.382q-.445.89-.444 2.074 0 1.2.51 2.107a3.8 3.8 0 0 0 1.382 1.381 3.9 3.9 0 0 0 1.893.477q.795 0 1.455-.33zm-2.789-5.38q-.576.675-.575 1.762 0 1.102.559 1.794.576.675 1.645.675a2.25 2.25 0 0 0 .934-.19 2.2 2.2 0 0 0 .468-.29l.178-.161a2.2 2.2 0 0 0 .397-.561q.244-.5.244-1.15v-.115q0-.708-.296-1.267l-.043-.077a2.2 2.2 0 0 0-.633-.709l-.13-.086-.047-.028a2.1 2.1 0 0 0-1.073-.285q-1.052 0-1.629.692zm2.316 2.706c.163-.17.28-.407.28-.83v-.114c0-.292-.06-.508-.15-.68a.96.96 0 0 0-.353-.389.85.85 0 0 0-.464-.127c-.4 0-.56.114-.664.239l-.01.012c-.148.174-.275.45-.275.945 0 .506.122.801.27.99.097.11.266.224.68.224.303 0 .504-.09.687-.269zm7.545 1.705a2.6 2.6 0 0 0 .331.423q.319.33.755.548l.173.074q.65.255 1.49.255 1.02 0 1.844-.493a3.45 3.45 0 0 0 1.316-1.4q.493-.904.493-2.089 0-1.909-.988-2.913-.988-1.02-2.584-1.02-.898 0-1.575.347a3 3 0 0 0-.415.262l-.199.166a3.4 3.4 0 0 0-.64.82V9.242h-1.712v11.553h1.729l-.017-5.134zm.53-1.138q.206.29.48.5l.155.11.053.034q.51.296 1.119.297 1.07 0 1.645-.675.577-.69.576-1.762 0-1.119-.576-1.777-.558-.675-1.645-.675-.435 0-.835.16a2 2 0 0 0-.284.136 2 2 0 0 0-.363.254 2.2 2.2 0 0 0-.46.569l-.082.162a2.6 2.6 0 0 0-.213 1.072v.115q0 .707.296 1.267l.135.211zm.964-.818a1.1 1.1 0 0 0 .367.385.94.94 0 0 0 .476.118c.423 0 .59-.117.687-.23.159-.194.28-.478.28-.95 0-.53-.133-.8-.266-.952l-.021-.025c-.078-.094-.231-.221-.68-.221a1 1 0 0 0-.503.135l-.012.007a.86.86 0 0 0-.335.343c-.073.133-.132.324-.132.614v.115a1.4 1.4 0 0 0 .14.66zm15.7-6.222q.347-.346.346-.856a1.05 1.05 0 0 0-.345-.79 1.18 1.18 0 0 0-.84-.329q-.51 0-.855.33a1.05 1.05 0 0 0-.346.79q0 .51.346.855.345.346.856.346.51 0 .839-.346zm4.337 9.314.033-1.332q.191.403.59.747l.098.081a4 4 0 0 0 .316.224l.223.122a3.2 3.2 0 0 0 1.44.322 3.8 3.8 0 0 0 1.875-.477 3.5 3.5 0 0 0 1.382-1.366q.527-.89.526-2.09 0-1.184-.444-2.073a3.24 3.24 0 0 0-1.283-1.399q-.823-.51-1.942-.51a3.5 3.5 0 0 0-1.527.344l-.086.043-.165.09a3 3 0 0 0-.33.214q-.432.315-.656.707a2 2 0 0 0-.099.198l.082-1.283V4.701h-1.744v12.095zm.473-2.509a2.5 2.5 0 0 0 .566.7q.117.098.245.18l.144.08a2.1 2.1 0 0 0 .975.232q1.07 0 1.645-.675.576-.69.576-1.778 0-1.102-.576-1.777-.56-.691-1.645-.692a2.2 2.2 0 0 0-1.015.235q-.22.113-.415.282l-.15.142a2.1 2.1 0 0 0-.42.594q-.223.479-.223 1.1v.115q0 .705.293 1.26zm2.616-.293c.157-.191.28-.479.28-.967 0-.51-.13-.79-.276-.961l-.021-.026c-.082-.1-.232-.225-.67-.225a.87.87 0 0 0-.681.279l-.012.011c-.154.155-.274.38-.274.807v.115c0 .285.057.499.144.669a1.1 1.1 0 0 0 .367.405c.137.082.28.123.455.123.423 0 .59-.118.686-.23zm8.266-3.013q.345-.13.724-.14l.069-.002q.493 0 .642.099l.247-1.794q-.196-.099-.717-.099a2.3 2.3 0 0 0-.545.063 2 2 0 0 0-.411.148 2.2 2.2 0 0 0-.4.249 2.5 2.5 0 0 0-.485.499 2.7 2.7 0 0 0-.32.581l-.05.137v-1.48h-1.778v7.553h1.777v-3.884q0-.546.159-.943a1.5 1.5 0 0 1 .466-.636 2.5 2.5 0 0 1 .399-.253 2 2 0 0 1 .224-.099zm9.784 2.656.05-.922q0-1.743-.856-2.698-.838-.97-2.584-.97-1.119-.001-2.007.493a3.46 3.46 0 0 0-1.4 1.382q-.493.906-.493 2.106 0 1.07.428 1.975.428.89 1.332 1.432.906.526 2.255.526.973 0 1.668-.185l.044-.012.135-.04q.613-.184.984-.421l-.542-1.267q-.3.162-.642.274l-.297.087q-.51.131-1.3.131-.954 0-1.497-.444a1.6 1.6 0 0 1-.192-.193q-.366-.44-.512-1.234l-.004-.021zm-5.427-1.256-.003.022h3.752v-.138q-.011-.727-.288-1.118a1 1 0 0 0-.156-.176q-.46-.428-1.316-.428-.986 0-1.494.604-.379.45-.494 1.234zm-27.053 2.77V4.7h-1.86v12.095h5.333V15.15zm7.103-5.908v7.553h-1.843V9.242h1.843z'/%3E%3Cpath fill='%23fff' d='m19.63 11.151-.757-1.71-.345 1.71-1.12 5.644h-1.827L18.083 4.7h.197l3.325 6.533.988 2.19.988-2.19L26.839 4.7h.181l2.6 12.095h-1.81l-1.218-5.644-.362-1.71-.658 1.71-2.93 5.644h-.098l-2.913-5.644zm14.836 5.81q-1.02 0-1.893-.478a3.8 3.8 0 0 1-1.381-1.382q-.51-.906-.51-2.106 0-1.185.444-2.074a3.36 3.36 0 0 1 1.3-1.382q.839-.494 1.974-.494a3.3 3.3 0 0 1 1.234.231 3.3 3.3 0 0 1 .97.575q.396.33.527.659l.033-1.267h1.694v7.553H37.18l-.033-1.332q-.279.593-1.02 1.053a3.17 3.17 0 0 1-1.662.444zm.296-1.482q.938 0 1.58-.642.642-.66.642-1.711v-.115q0-.708-.296-1.267a2.2 2.2 0 0 0-.807-.872 2.1 2.1 0 0 0-1.119-.313q-1.053 0-1.629.692-.575.675-.575 1.76 0 1.103.559 1.795.577.675 1.645.675zm6.521-6.237h1.711v1.4q.906-1.597 2.83-1.597 1.596 0 2.584 1.02.988 1.005.988 2.914 0 1.185-.493 2.09a3.46 3.46 0 0 1-1.316 1.399 3.5 3.5 0 0 1-1.844.493q-.954 0-1.662-.329a2.67 2.67 0 0 1-1.086-.97l.017 5.134h-1.728zm4.048 6.22q1.07 0 1.645-.674.577-.69.576-1.762 0-1.119-.576-1.777-.558-.675-1.645-.675-.592 0-1.12.296-.51.28-.822.823-.296.527-.296 1.234v.115q0 .708.296 1.267.313.543.823.855.51.296 1.119.297z'/%3E%3Cpath fill='%23e1e3e9' d='M51.325 4.7h1.86v10.45h3.473v1.646h-5.333zm7.12 4.542h1.843v7.553h-1.843zm.905-1.415a1.16 1.16 0 0 1-.856-.346 1.17 1.17 0 0 1-.346-.856 1.05 1.05 0 0 1 .346-.79q.346-.329.856-.329.494 0 .839.33a1.05 1.05 0 0 1 .345.79 1.16 1.16 0 0 1-.345.855q-.33.346-.84.346zm7.875 9.133a3.17 3.17 0 0 1-1.662-.444q-.723-.46-1.004-1.053l-.033 1.332h-1.71V4.701h1.743v4.657l-.082 1.283q.279-.658 1.086-1.119a3.5 3.5 0 0 1 1.778-.477q1.119 0 1.942.51a3.24 3.24 0 0 1 1.283 1.4q.445.888.444 2.072 0 1.201-.526 2.09a3.5 3.5 0 0 1-1.382 1.366 3.8 3.8 0 0 1-1.876.477zm-.296-1.481q1.069 0 1.645-.675.577-.69.577-1.778 0-1.102-.577-1.776-.56-.691-1.645-.692a2.12 2.12 0 0 0-1.58.659q-.642.641-.642 1.694v.115q0 .71.296 1.267a2.4 2.4 0 0 0 .807.872 2.1 2.1 0 0 0 1.119.313zm5.927-6.237h1.777v1.481q.263-.757.856-1.217a2.14 2.14 0 0 1 1.349-.46q.527 0 .724.098l-.247 1.794q-.149-.099-.642-.099-.774 0-1.416.494-.626.493-.626 1.58v3.883h-1.777V9.242zm9.534 7.718q-1.35 0-2.255-.526-.904-.543-1.332-1.432a4.6 4.6 0 0 1-.428-1.975q0-1.2.493-2.106a3.46 3.46 0 0 1 1.4-1.382q.889-.495 2.007-.494 1.744 0 2.584.97.855.956.856 2.7 0 .444-.05.92h-5.43q.18 1.005.708 1.45.542.443 1.497.443.79 0 1.3-.131a4 4 0 0 0 .938-.362l.542 1.267q-.411.263-1.119.46-.708.198-1.711.197zm1.596-4.558q.016-1.02-.444-1.432-.46-.428-1.316-.428-1.728 0-1.991 1.86z'/%3E%3Cpath d='M5.074 15.948a.484.657 0 0 0-.486.659v1.84a.484.657 0 0 0 .486.659h4.101a.484.657 0 0 0 .486-.659v-1.84a.484.657 0 0 0-.486-.659zm3.56 1.16H5.617v.838h3.017z' style='fill:%23fff;fill-rule:evenodd;stroke-width:1.03600001'/%3E%3Cg style='stroke-width:1.12603545'%3E%3Cpath d='M-9.408-1.416c-3.833-.025-7.056 2.912-7.08 6.615-.02 3.08 1.653 4.832 3.107 6.268.903.892 1.721 1.74 2.32 2.902l-.525-.004c-.543-.003-.992.304-1.24.639a1.87 1.87 0 0 0-.362 1.121l-.011 1.877c-.003.402.104.787.347 1.125.244.338.688.653 1.23.656l4.142.028c.542.003.99-.306 1.238-.641a1.87 1.87 0 0 0 .363-1.121l.012-1.875a1.87 1.87 0 0 0-.348-1.127c-.243-.338-.688-.653-1.23-.656l-.518-.004c.597-1.145 1.425-1.983 2.348-2.87 1.473-1.414 3.18-3.149 3.2-6.226-.016-3.59-2.923-6.684-6.993-6.707m-.006 1.1v.002c3.274.02 5.92 2.532 5.9 5.6-.017 2.706-1.39 4.026-2.863 5.44-1.034.994-2.118 2.033-2.814 3.633-.018.041-.052.055-.075.065q-.013.004-.02.01a.34.34 0 0 1-.226.084.34.34 0 0 1-.224-.086l-.092-.077c-.699-1.615-1.768-2.669-2.781-3.67-1.454-1.435-2.797-2.762-2.78-5.478.02-3.067 2.7-5.545 5.975-5.523m-.02 2.826c-1.62-.01-2.944 1.315-2.955 2.96-.01 1.646 1.295 2.988 2.916 2.999h.002c1.621.01 2.943-1.316 2.953-2.961.011-1.646-1.294-2.988-2.916-2.998m-.005 1.1c1.017.006 1.829.83 1.822 1.89s-.83 1.874-1.848 1.867c-1.018-.006-1.829-.83-1.822-1.89s.83-1.874 1.848-1.868m-2.155 11.857 4.14.025c.271.002.49.305.487.676l-.013 1.875c-.003.37-.224.67-.495.668l-4.14-.025c-.27-.002-.487-.306-.485-.676l.012-1.875c.003-.37.224-.67.494-.668' style='color:%23000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:evenodd;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:%23000;solid-opacity:1;vector-effect:none;fill:%23000;fill-opacity:.4;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto' transform='translate(15.553 2.85)scale(.88807)'/%3E%3Cpath d='M-9.415-.316C-12.69-.338-15.37 2.14-15.39 5.207c-.017 2.716 1.326 4.041 2.78 5.477 1.013 1 2.081 2.055 2.78 3.67l.092.076a.34.34 0 0 0 .225.086.34.34 0 0 0 .227-.083l.019-.01c.022-.009.057-.024.074-.064.697-1.6 1.78-2.64 2.814-3.634 1.473-1.414 2.847-2.733 2.864-5.44.02-3.067-2.627-5.58-5.901-5.601m-.057 8.784c1.621.011 2.944-1.315 2.955-2.96.01-1.646-1.295-2.988-2.916-2.999-1.622-.01-2.945 1.315-2.955 2.96s1.295 2.989 2.916 3' style='clip-rule:evenodd;fill:%23e1e3e9;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:.4' transform='translate(15.553 2.85)scale(.88807)'/%3E%3Cpath d='M-11.594 15.465c-.27-.002-.492.297-.494.668l-.012 1.876c-.003.371.214.673.485.675l4.14.027c.271.002.492-.298.495-.668l.012-1.877c.003-.37-.215-.672-.485-.674z' style='clip-rule:evenodd;fill:%23fff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:.4' transform='translate(15.553 2.85)scale(.88807)'/%3E%3C/g%3E%3C/svg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){a.maplibregl-ctrl-logo{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='23' fill='none'%3E%3Cpath fill='%23000' fill-opacity='.4' fill-rule='evenodd' d='M17.408 16.796h-1.827l2.501-12.095h.198l3.324 6.533.988 2.19.988-2.19 3.258-6.533h.181l2.6 12.095h-1.81l-1.218-5.644-.362-1.71-.658 1.71-2.929 5.644h-.098l-2.914-5.644-.757-1.71-.345 1.71zm1.958-3.42-.726 3.663a1.255 1.255 0 0 1-1.232 1.011h-1.827a1.255 1.255 0 0 1-1.229-1.509l2.501-12.095a1.255 1.255 0 0 1 1.23-1.001h.197a1.25 1.25 0 0 1 1.12.685l3.19 6.273 3.125-6.263a1.25 1.25 0 0 1 1.123-.695h.181a1.255 1.255 0 0 1 1.227.991l1.443 6.71a5 5 0 0 1 .314-.787l.009-.016a4.6 4.6 0 0 1 1.777-1.887c.782-.46 1.668-.667 2.611-.667a4.6 4.6 0 0 1 1.7.32l.306.134c.21-.16.474-.256.759-.256h1.694a1.255 1.255 0 0 1 1.212.925 1.255 1.255 0 0 1 1.212-.925h1.711c.284 0 .545.094.755.252.613-.3 1.312-.45 2.075-.45 1.356 0 2.557.445 3.482 1.4q.47.48.763 1.064V4.701a1.255 1.255 0 0 1 1.255-1.255h1.86A1.255 1.255 0 0 1 54.44 4.7v9.194h2.217c.19 0 .37.043.532.118v-4.77c0-.356.147-.678.385-.906a2.42 2.42 0 0 1-.682-1.71c0-.665.267-1.253.735-1.7a2.45 2.45 0 0 1 1.722-.674 2.43 2.43 0 0 1 1.705.675q.318.302.504.683V4.7a1.255 1.255 0 0 1 1.255-1.255h1.744A1.255 1.255 0 0 1 65.812 4.7v3.335a4.8 4.8 0 0 1 1.526-.246c.938 0 1.817.214 2.59.69a4.47 4.47 0 0 1 1.67 1.743v-.98a1.255 1.255 0 0 1 1.256-1.256h1.777c.233 0 .451.064.639.174a3.4 3.4 0 0 1 1.567-.372c.346 0 .861.02 1.285.232a1.25 1.25 0 0 1 .689 1.004 4.7 4.7 0 0 1 .853-.588c.795-.44 1.675-.647 2.61-.647 1.385 0 2.65.39 3.525 1.396.836.938 1.168 2.173 1.168 3.528q-.001.515-.056 1.051a1.255 1.255 0 0 1-.947 1.09l.408.952a1.255 1.255 0 0 1-.477 1.552c-.418.268-.92.463-1.458.612-.613.171-1.304.244-2.049.244-1.06 0-2.043-.207-2.886-.698l-.015-.008c-.798-.48-1.419-1.135-1.818-1.963l-.004-.008a5.8 5.8 0 0 1-.548-2.512q0-.429.053-.843a1.3 1.3 0 0 1-.333-.086l-.166-.004c-.223 0-.426.062-.643.228-.03.024-.142.139-.142.59v3.883a1.255 1.255 0 0 1-1.256 1.256h-1.777a1.255 1.255 0 0 1-1.256-1.256V15.69l-.032.057a4.8 4.8 0 0 1-1.86 1.833 5.04 5.04 0 0 1-2.484.634 4.5 4.5 0 0 1-1.935-.424 1.25 1.25 0 0 1-.764.258h-1.71a1.255 1.255 0 0 1-1.256-1.255V7.687a2.4 2.4 0 0 1-.428.625c.253.23.412.561.412.93v7.553a1.255 1.255 0 0 1-1.256 1.255h-1.843a1.25 1.25 0 0 1-.894-.373c-.228.23-.544.373-.894.373H51.32a1.255 1.255 0 0 1-1.256-1.255v-1.251l-.061.117a4.7 4.7 0 0 1-1.782 1.884 4.77 4.77 0 0 1-2.485.67 5.6 5.6 0 0 1-1.485-.188l.009 2.764a1.255 1.255 0 0 1-1.255 1.259h-1.729a1.255 1.255 0 0 1-1.255-1.255v-3.537a1.255 1.255 0 0 1-1.167.793h-1.679a1.25 1.25 0 0 1-.77-.263 4.5 4.5 0 0 1-1.945.429c-.885 0-1.724-.21-2.495-.632l-.017-.01a5 5 0 0 1-1.081-.836 1.255 1.255 0 0 1-1.254 1.312h-1.81a1.255 1.255 0 0 1-1.228-.99l-.782-3.625-2.044 3.939a1.25 1.25 0 0 1-1.115.676h-.098a1.25 1.25 0 0 1-1.116-.68l-2.061-3.994zM35.92 16.63l.207-.114.223-.15q.493-.356.735-.785l.061-.118.033 1.332h1.678V9.242h-1.694l-.033 1.267q-.133-.329-.526-.658l-.032-.028a3.2 3.2 0 0 0-.668-.428l-.27-.12a3.3 3.3 0 0 0-1.235-.23q-1.136-.001-1.974.493a3.36 3.36 0 0 0-1.3 1.382q-.445.89-.444 2.074 0 1.2.51 2.107a3.8 3.8 0 0 0 1.382 1.381 3.9 3.9 0 0 0 1.893.477q.795 0 1.455-.33zm-2.789-5.38q-.576.675-.575 1.762 0 1.102.559 1.794.576.675 1.645.675a2.25 2.25 0 0 0 .934-.19 2.2 2.2 0 0 0 .468-.29l.178-.161a2.2 2.2 0 0 0 .397-.561q.244-.5.244-1.15v-.115q0-.708-.296-1.267l-.043-.077a2.2 2.2 0 0 0-.633-.709l-.13-.086-.047-.028a2.1 2.1 0 0 0-1.073-.285q-1.052 0-1.629.692zm2.316 2.706c.163-.17.28-.407.28-.83v-.114c0-.292-.06-.508-.15-.68a.96.96 0 0 0-.353-.389.85.85 0 0 0-.464-.127c-.4 0-.56.114-.664.239l-.01.012c-.148.174-.275.45-.275.945 0 .506.122.801.27.99.097.11.266.224.68.224.303 0 .504-.09.687-.269zm7.545 1.705a2.6 2.6 0 0 0 .331.423q.319.33.755.548l.173.074q.65.255 1.49.255 1.02 0 1.844-.493a3.45 3.45 0 0 0 1.316-1.4q.493-.904.493-2.089 0-1.909-.988-2.913-.988-1.02-2.584-1.02-.898 0-1.575.347a3 3 0 0 0-.415.262l-.199.166a3.4 3.4 0 0 0-.64.82V9.242h-1.712v11.553h1.729l-.017-5.134zm.53-1.138q.206.29.48.5l.155.11.053.034q.51.296 1.119.297 1.07 0 1.645-.675.577-.69.576-1.762 0-1.119-.576-1.777-.558-.675-1.645-.675-.435 0-.835.16a2 2 0 0 0-.284.136 2 2 0 0 0-.363.254 2.2 2.2 0 0 0-.46.569l-.082.162a2.6 2.6 0 0 0-.213 1.072v.115q0 .707.296 1.267l.135.211zm.964-.818a1.1 1.1 0 0 0 .367.385.94.94 0 0 0 .476.118c.423 0 .59-.117.687-.23.159-.194.28-.478.28-.95 0-.53-.133-.8-.266-.952l-.021-.025c-.078-.094-.231-.221-.68-.221a1 1 0 0 0-.503.135l-.012.007a.86.86 0 0 0-.335.343c-.073.133-.132.324-.132.614v.115a1.4 1.4 0 0 0 .14.66zm15.7-6.222q.347-.346.346-.856a1.05 1.05 0 0 0-.345-.79 1.18 1.18 0 0 0-.84-.329q-.51 0-.855.33a1.05 1.05 0 0 0-.346.79q0 .51.346.855.345.346.856.346.51 0 .839-.346zm4.337 9.314.033-1.332q.191.403.59.747l.098.081a4 4 0 0 0 .316.224l.223.122a3.2 3.2 0 0 0 1.44.322 3.8 3.8 0 0 0 1.875-.477 3.5 3.5 0 0 0 1.382-1.366q.527-.89.526-2.09 0-1.184-.444-2.073a3.24 3.24 0 0 0-1.283-1.399q-.823-.51-1.942-.51a3.5 3.5 0 0 0-1.527.344l-.086.043-.165.09a3 3 0 0 0-.33.214q-.432.315-.656.707a2 2 0 0 0-.099.198l.082-1.283V4.701h-1.744v12.095zm.473-2.509a2.5 2.5 0 0 0 .566.7q.117.098.245.18l.144.08a2.1 2.1 0 0 0 .975.232q1.07 0 1.645-.675.576-.69.576-1.778 0-1.102-.576-1.777-.56-.691-1.645-.692a2.2 2.2 0 0 0-1.015.235q-.22.113-.415.282l-.15.142a2.1 2.1 0 0 0-.42.594q-.223.479-.223 1.1v.115q0 .705.293 1.26zm2.616-.293c.157-.191.28-.479.28-.967 0-.51-.13-.79-.276-.961l-.021-.026c-.082-.1-.232-.225-.67-.225a.87.87 0 0 0-.681.279l-.012.011c-.154.155-.274.38-.274.807v.115c0 .285.057.499.144.669a1.1 1.1 0 0 0 .367.405c.137.082.28.123.455.123.423 0 .59-.118.686-.23zm8.266-3.013q.345-.13.724-.14l.069-.002q.493 0 .642.099l.247-1.794q-.196-.099-.717-.099a2.3 2.3 0 0 0-.545.063 2 2 0 0 0-.411.148 2.2 2.2 0 0 0-.4.249 2.5 2.5 0 0 0-.485.499 2.7 2.7 0 0 0-.32.581l-.05.137v-1.48h-1.778v7.553h1.777v-3.884q0-.546.159-.943a1.5 1.5 0 0 1 .466-.636 2.5 2.5 0 0 1 .399-.253 2 2 0 0 1 .224-.099zm9.784 2.656.05-.922q0-1.743-.856-2.698-.838-.97-2.584-.97-1.119-.001-2.007.493a3.46 3.46 0 0 0-1.4 1.382q-.493.906-.493 2.106 0 1.07.428 1.975.428.89 1.332 1.432.906.526 2.255.526.973 0 1.668-.185l.044-.012.135-.04q.613-.184.984-.421l-.542-1.267q-.3.162-.642.274l-.297.087q-.51.131-1.3.131-.954 0-1.497-.444a1.6 1.6 0 0 1-.192-.193q-.366-.44-.512-1.234l-.004-.021zm-5.427-1.256-.003.022h3.752v-.138q-.011-.727-.288-1.118a1 1 0 0 0-.156-.176q-.46-.428-1.316-.428-.986 0-1.494.604-.379.45-.494 1.234zm-27.053 2.77V4.7h-1.86v12.095h5.333V15.15zm7.103-5.908v7.553h-1.843V9.242h1.843z'/%3E%3Cpath fill='%23fff' d='m19.63 11.151-.757-1.71-.345 1.71-1.12 5.644h-1.827L18.083 4.7h.197l3.325 6.533.988 2.19.988-2.19L26.839 4.7h.181l2.6 12.095h-1.81l-1.218-5.644-.362-1.71-.658 1.71-2.93 5.644h-.098l-2.913-5.644zm14.836 5.81q-1.02 0-1.893-.478a3.8 3.8 0 0 1-1.381-1.382q-.51-.906-.51-2.106 0-1.185.444-2.074a3.36 3.36 0 0 1 1.3-1.382q.839-.494 1.974-.494a3.3 3.3 0 0 1 1.234.231 3.3 3.3 0 0 1 .97.575q.396.33.527.659l.033-1.267h1.694v7.553H37.18l-.033-1.332q-.279.593-1.02 1.053a3.17 3.17 0 0 1-1.662.444zm.296-1.482q.938 0 1.58-.642.642-.66.642-1.711v-.115q0-.708-.296-1.267a2.2 2.2 0 0 0-.807-.872 2.1 2.1 0 0 0-1.119-.313q-1.053 0-1.629.692-.575.675-.575 1.76 0 1.103.559 1.795.577.675 1.645.675zm6.521-6.237h1.711v1.4q.906-1.597 2.83-1.597 1.596 0 2.584 1.02.988 1.005.988 2.914 0 1.185-.493 2.09a3.46 3.46 0 0 1-1.316 1.399 3.5 3.5 0 0 1-1.844.493q-.954 0-1.662-.329a2.67 2.67 0 0 1-1.086-.97l.017 5.134h-1.728zm4.048 6.22q1.07 0 1.645-.674.577-.69.576-1.762 0-1.119-.576-1.777-.558-.675-1.645-.675-.592 0-1.12.296-.51.28-.822.823-.296.527-.296 1.234v.115q0 .708.296 1.267.313.543.823.855.51.296 1.119.297z'/%3E%3Cpath fill='%23e1e3e9' d='M51.325 4.7h1.86v10.45h3.473v1.646h-5.333zm7.12 4.542h1.843v7.553h-1.843zm.905-1.415a1.16 1.16 0 0 1-.856-.346 1.17 1.17 0 0 1-.346-.856 1.05 1.05 0 0 1 .346-.79q.346-.329.856-.329.494 0 .839.33a1.05 1.05 0 0 1 .345.79 1.16 1.16 0 0 1-.345.855q-.33.346-.84.346zm7.875 9.133a3.17 3.17 0 0 1-1.662-.444q-.723-.46-1.004-1.053l-.033 1.332h-1.71V4.701h1.743v4.657l-.082 1.283q.279-.658 1.086-1.119a3.5 3.5 0 0 1 1.778-.477q1.119 0 1.942.51a3.24 3.24 0 0 1 1.283 1.4q.445.888.444 2.072 0 1.201-.526 2.09a3.5 3.5 0 0 1-1.382 1.366 3.8 3.8 0 0 1-1.876.477zm-.296-1.481q1.069 0 1.645-.675.577-.69.577-1.778 0-1.102-.577-1.776-.56-.691-1.645-.692a2.12 2.12 0 0 0-1.58.659q-.642.641-.642 1.694v.115q0 .71.296 1.267a2.4 2.4 0 0 0 .807.872 2.1 2.1 0 0 0 1.119.313zm5.927-6.237h1.777v1.481q.263-.757.856-1.217a2.14 2.14 0 0 1 1.349-.46q.527 0 .724.098l-.247 1.794q-.149-.099-.642-.099-.774 0-1.416.494-.626.493-.626 1.58v3.883h-1.777V9.242zm9.534 7.718q-1.35 0-2.255-.526-.904-.543-1.332-1.432a4.6 4.6 0 0 1-.428-1.975q0-1.2.493-2.106a3.46 3.46 0 0 1 1.4-1.382q.889-.495 2.007-.494 1.744 0 2.584.97.855.956.856 2.7 0 .444-.05.92h-5.43q.18 1.005.708 1.45.542.443 1.497.443.79 0 1.3-.131a4 4 0 0 0 .938-.362l.542 1.267q-.411.263-1.119.46-.708.198-1.711.197zm1.596-4.558q.016-1.02-.444-1.432-.46-.428-1.316-.428-1.728 0-1.991 1.86z'/%3E%3Cpath d='M5.074 15.948a.484.657 0 0 0-.486.659v1.84a.484.657 0 0 0 .486.659h4.101a.484.657 0 0 0 .486-.659v-1.84a.484.657 0 0 0-.486-.659zm3.56 1.16H5.617v.838h3.017z' style='fill:%23fff;fill-rule:evenodd;stroke-width:1.03600001'/%3E%3Cg style='stroke-width:1.12603545'%3E%3Cpath d='M-9.408-1.416c-3.833-.025-7.056 2.912-7.08 6.615-.02 3.08 1.653 4.832 3.107 6.268.903.892 1.721 1.74 2.32 2.902l-.525-.004c-.543-.003-.992.304-1.24.639a1.87 1.87 0 0 0-.362 1.121l-.011 1.877c-.003.402.104.787.347 1.125.244.338.688.653 1.23.656l4.142.028c.542.003.99-.306 1.238-.641a1.87 1.87 0 0 0 .363-1.121l.012-1.875a1.87 1.87 0 0 0-.348-1.127c-.243-.338-.688-.653-1.23-.656l-.518-.004c.597-1.145 1.425-1.983 2.348-2.87 1.473-1.414 3.18-3.149 3.2-6.226-.016-3.59-2.923-6.684-6.993-6.707m-.006 1.1v.002c3.274.02 5.92 2.532 5.9 5.6-.017 2.706-1.39 4.026-2.863 5.44-1.034.994-2.118 2.033-2.814 3.633-.018.041-.052.055-.075.065q-.013.004-.02.01a.34.34 0 0 1-.226.084.34.34 0 0 1-.224-.086l-.092-.077c-.699-1.615-1.768-2.669-2.781-3.67-1.454-1.435-2.797-2.762-2.78-5.478.02-3.067 2.7-5.545 5.975-5.523m-.02 2.826c-1.62-.01-2.944 1.315-2.955 2.96-.01 1.646 1.295 2.988 2.916 2.999h.002c1.621.01 2.943-1.316 2.953-2.961.011-1.646-1.294-2.988-2.916-2.998m-.005 1.1c1.017.006 1.829.83 1.822 1.89s-.83 1.874-1.848 1.867c-1.018-.006-1.829-.83-1.822-1.89s.83-1.874 1.848-1.868m-2.155 11.857 4.14.025c.271.002.49.305.487.676l-.013 1.875c-.003.37-.224.67-.495.668l-4.14-.025c-.27-.002-.487-.306-.485-.676l.012-1.875c.003-.37.224-.67.494-.668' style='color:%23000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:evenodd;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:%23000;solid-opacity:1;vector-effect:none;fill:%23000;fill-opacity:.4;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto' transform='translate(15.553 2.85)scale(.88807)'/%3E%3Cpath d='M-9.415-.316C-12.69-.338-15.37 2.14-15.39 5.207c-.017 2.716 1.326 4.041 2.78 5.477 1.013 1 2.081 2.055 2.78 3.67l.092.076a.34.34 0 0 0 .225.086.34.34 0 0 0 .227-.083l.019-.01c.022-.009.057-.024.074-.064.697-1.6 1.78-2.64 2.814-3.634 1.473-1.414 2.847-2.733 2.864-5.44.02-3.067-2.627-5.58-5.901-5.601m-.057 8.784c1.621.011 2.944-1.315 2.955-2.96.01-1.646-1.295-2.988-2.916-2.999-1.622-.01-2.945 1.315-2.955 2.96s1.295 2.989 2.916 3' style='clip-rule:evenodd;fill:%23e1e3e9;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:.4' transform='translate(15.553 2.85)scale(.88807)'/%3E%3Cpath d='M-11.594 15.465c-.27-.002-.492.297-.494.668l-.012 1.876c-.003.371.214.673.485.675l4.14.027c.271.002.492-.298.495-.668l.012-1.877c.003-.37-.215-.672-.485-.674z' style='clip-rule:evenodd;fill:%23fff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:.4' transform='translate(15.553 2.85)scale(.88807)'/%3E%3C/g%3E%3C/svg%3E")}}.maplibregl-ctrl.maplibregl-ctrl-attrib{background-color:hsla(0,0%,100%,.5);margin:0;padding:0 5px}@media screen{.maplibregl-ctrl-attrib.maplibregl-compact{background-color:#fff;border-radius:12px;box-sizing:content-box;color:#000;margin:10px;min-height:20px;padding:2px 24px 2px 0;position:relative}.maplibregl-ctrl-attrib.maplibregl-compact-show{padding:2px 28px 2px 8px;visibility:visible}.maplibregl-ctrl-bottom-left>.maplibregl-ctrl-attrib.maplibregl-compact-show,.maplibregl-ctrl-top-left>.maplibregl-ctrl-attrib.maplibregl-compact-show{border-radius:12px;padding:2px 8px 2px 28px}.maplibregl-ctrl-attrib.maplibregl-compact .maplibregl-ctrl-attrib-inner{display:none}.maplibregl-ctrl-attrib-button{background-color:hsla(0,0%,100%,.5);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill-rule='evenodd' viewBox='0 0 20 20'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E");border:0;border-radius:12px;box-sizing:border-box;cursor:pointer;display:none;height:24px;outline:none;position:absolute;right:0;top:0;width:24px}.maplibregl-ctrl-attrib summary.maplibregl-ctrl-attrib-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;list-style:none}.maplibregl-ctrl-attrib summary.maplibregl-ctrl-attrib-button::-webkit-details-marker{display:none}.maplibregl-ctrl-bottom-left .maplibregl-ctrl-attrib-button,.maplibregl-ctrl-top-left .maplibregl-ctrl-attrib-button{left:0}.maplibregl-ctrl-attrib.maplibregl-compact .maplibregl-ctrl-attrib-button,.maplibregl-ctrl-attrib.maplibregl-compact-show .maplibregl-ctrl-attrib-inner{display:block}.maplibregl-ctrl-attrib.maplibregl-compact-show .maplibregl-ctrl-attrib-button{background-color:rgb(0 0 0/5%)}.maplibregl-ctrl-bottom-right>.maplibregl-ctrl-attrib.maplibregl-compact:after{bottom:0;right:0}.maplibregl-ctrl-top-right>.maplibregl-ctrl-attrib.maplibregl-compact:after{right:0;top:0}.maplibregl-ctrl-top-left>.maplibregl-ctrl-attrib.maplibregl-compact:after{left:0;top:0}.maplibregl-ctrl-bottom-left>.maplibregl-ctrl-attrib.maplibregl-compact:after{bottom:0;left:0}}@media screen and (forced-colors:active){.maplibregl-ctrl-attrib.maplibregl-compact:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='%23fff' fill-rule='evenodd' viewBox='0 0 20 20'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E")}}@media screen and (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl-attrib.maplibregl-compact:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill-rule='evenodd' viewBox='0 0 20 20'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E")}}.maplibregl-ctrl-attrib a{color:rgba(0,0,0,.75);text-decoration:none}.maplibregl-ctrl-attrib a:hover{color:inherit;text-decoration:underline}.maplibregl-attrib-empty{display:none}.maplibregl-ctrl-scale{background-color:hsla(0,0%,100%,.75);border:2px solid #333;border-top:#333;box-sizing:border-box;color:#333;font-size:10px;padding:0 5px}.maplibregl-popup{display:flex;left:0;pointer-events:none;position:absolute;top:0;will-change:transform}.maplibregl-popup-anchor-top,.maplibregl-popup-anchor-top-left,.maplibregl-popup-anchor-top-right{flex-direction:column}.maplibregl-popup-anchor-bottom,.maplibregl-popup-anchor-bottom-left,.maplibregl-popup-anchor-bottom-right{flex-direction:column-reverse}.maplibregl-popup-anchor-left{flex-direction:row}.maplibregl-popup-anchor-right{flex-direction:row-reverse}.maplibregl-popup-tip{border:10px solid transparent;height:0;width:0;z-index:1}.maplibregl-popup-anchor-top .maplibregl-popup-tip{align-self:center;border-bottom-color:#fff;border-top:none}.maplibregl-popup-anchor-top-left .maplibregl-popup-tip{align-self:flex-start;border-bottom-color:#fff;border-left:none;border-top:none}.maplibregl-popup-anchor-top-right .maplibregl-popup-tip{align-self:flex-end;border-bottom-color:#fff;border-right:none;border-top:none}.maplibregl-popup-anchor-bottom .maplibregl-popup-tip{align-self:center;border-bottom:none;border-top-color:#fff}.maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip{align-self:flex-start;border-bottom:none;border-left:none;border-top-color:#fff}.maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip{align-self:flex-end;border-bottom:none;border-right:none;border-top-color:#fff}.maplibregl-popup-anchor-left .maplibregl-popup-tip{align-self:center;border-left:none;border-right-color:#fff}.maplibregl-popup-anchor-right .maplibregl-popup-tip{align-self:center;border-left-color:#fff;border-right:none}.maplibregl-popup-close-button{background-color:transparent;border:0;border-radius:0 3px 0 0;cursor:pointer;position:absolute;right:0;top:0}.maplibregl-popup-close-button:hover{background-color:rgb(0 0 0/5%)}.maplibregl-popup-content{background:#fff;border-radius:3px;box-shadow:0 1px 2px rgba(0,0,0,.1);padding:15px 10px;pointer-events:auto;position:relative}.maplibregl-popup-anchor-top-left .maplibregl-popup-content{border-top-left-radius:0}.maplibregl-popup-anchor-top-right .maplibregl-popup-content{border-top-right-radius:0}.maplibregl-popup-anchor-bottom-left .maplibregl-popup-content{border-bottom-left-radius:0}.maplibregl-popup-anchor-bottom-right .maplibregl-popup-content{border-bottom-right-radius:0}.maplibregl-popup-track-pointer{display:none}.maplibregl-popup-track-pointer *{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.maplibregl-map:hover .maplibregl-popup-track-pointer{display:flex}.maplibregl-map:active .maplibregl-popup-track-pointer{display:none}.maplibregl-marker{left:0;position:absolute;top:0;transition:opacity .2s;will-change:transform}.maplibregl-user-location-dot,.maplibregl-user-location-dot:before{background-color:#1da1f2;border-radius:50%;height:15px;width:15px}.maplibregl-user-location-dot:before{animation:maplibregl-user-location-dot-pulse 2s infinite;content:"";position:absolute}.maplibregl-user-location-dot:after{border:2px solid #fff;border-radius:50%;box-shadow:0 0 3px rgba(0,0,0,.35);box-sizing:border-box;content:"";height:19px;left:-2px;position:absolute;top:-2px;width:19px}@keyframes maplibregl-user-location-dot-pulse{0%{opacity:1;transform:scale(1)}70%{opacity:0;transform:scale(3)}to{opacity:0;transform:scale(1)}}.maplibregl-user-location-dot-stale{background-color:#aaa}.maplibregl-user-location-dot-stale:after{display:none}.maplibregl-user-location-accuracy-circle{background-color:#1da1f233;border-radius:100%;height:1px;width:1px}.maplibregl-crosshair,.maplibregl-crosshair .maplibregl-interactive,.maplibregl-crosshair .maplibregl-interactive:active{cursor:crosshair}.maplibregl-boxzoom{background:#fff;border:2px dotted #202020;height:0;left:0;opacity:.5;position:absolute;top:0;width:0}.maplibregl-cooperative-gesture-screen{align-items:center;background:rgba(0,0,0,.4);color:#fff;display:flex;font-size:1.4em;inset:0;justify-content:center;line-height:1.2;opacity:0;padding:1rem;pointer-events:none;position:absolute;transition:opacity 1s ease 1s;z-index:99999}.maplibregl-cooperative-gesture-screen.maplibregl-show{opacity:1;transition:opacity .05s}.maplibregl-cooperative-gesture-screen .maplibregl-mobile-message{display:none}@media (hover:none),(width <= 480px){.maplibregl-cooperative-gesture-screen .maplibregl-desktop-message{display:none}.maplibregl-cooperative-gesture-screen .maplibregl-mobile-message{display:block}}.maplibregl-pseudo-fullscreen{height:100%!important;left:0!important;position:fixed!important;top:0!important;width:100%!important;z-index:99999} \ No newline at end of file diff --git a/src/app/resources/map/maplibre-gl.js b/src/app/resources/map/maplibre-gl.js new file mode 100644 index 0000000..2caee97 --- /dev/null +++ b/src/app/resources/map/maplibre-gl.js @@ -0,0 +1,59 @@ +/** + * MapLibre GL JS + * @license 3-Clause BSD. Full text of license: https://github.com/maplibre/maplibre-gl-js/blob/v4.7.1/LICENSE.txt + */ +(function (global, factory) { +typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : +typeof define === 'function' && define.amd ? define(factory) : +(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.maplibregl = factory()); +})(this, (function () { 'use strict'; + +/* eslint-disable */ + +var maplibregl = {}; +var modules = {}; +function define(moduleName, _dependencies, moduleFactory) { + modules[moduleName] = moduleFactory; + + // to get the list of modules see generated dist/maplibre-gl-dev.js file (look for `define(` calls) + if (moduleName !== 'index') { + return; + } + + // we assume that when an index module is initializing then other modules are loaded already + var workerBundleString = 'var sharedModule = {}; (' + modules.shared + ')(sharedModule); (' + modules.worker + ')(sharedModule);' + + var sharedModule = {}; + // the order of arguments of a module factory depends on rollup (it decides who is whose dependency) + // to check the correct order, see dist/maplibre-gl-dev.js file (look for `define(` calls) + // we assume that for our 3 chunks it will generate 3 modules and their order is predefined like the following + modules.shared(sharedModule); + modules.index(maplibregl, sharedModule); + + if (typeof window !== 'undefined') { + maplibregl.setWorkerUrl(window.URL.createObjectURL(new Blob([workerBundleString], { type: 'text/javascript' }))); + } + + return maplibregl; +}; + + + +define("shared",["exports"],(function(t){"use strict";function e(t,e,r,n){return new(r||(r=Promise))((function(i,s){function a(t){try{l(n.next(t));}catch(t){s(t);}}function o(t){try{l(n.throw(t));}catch(t){s(t);}}function l(t){var e;t.done?i(t.value):(e=t.value,e instanceof r?e:new r((function(t){t(e);}))).then(a,o);}l((n=n.apply(t,e||[])).next());}))}function r(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}"function"==typeof SuppressedError&&SuppressedError;var n=i;function i(t,e){this.x=t,this.y=e;}i.prototype={clone:function(){return new i(this.x,this.y)},add:function(t){return this.clone()._add(t)},sub:function(t){return this.clone()._sub(t)},multByPoint:function(t){return this.clone()._multByPoint(t)},divByPoint:function(t){return this.clone()._divByPoint(t)},mult:function(t){return this.clone()._mult(t)},div:function(t){return this.clone()._div(t)},rotate:function(t){return this.clone()._rotate(t)},rotateAround:function(t,e){return this.clone()._rotateAround(t,e)},matMult:function(t){return this.clone()._matMult(t)},unit:function(){return this.clone()._unit()},perp:function(){return this.clone()._perp()},round:function(){return this.clone()._round()},mag:function(){return Math.sqrt(this.x*this.x+this.y*this.y)},equals:function(t){return this.x===t.x&&this.y===t.y},dist:function(t){return Math.sqrt(this.distSqr(t))},distSqr:function(t){var e=t.x-this.x,r=t.y-this.y;return e*e+r*r},angle:function(){return Math.atan2(this.y,this.x)},angleTo:function(t){return Math.atan2(this.y-t.y,this.x-t.x)},angleWith:function(t){return this.angleWithSep(t.x,t.y)},angleWithSep:function(t,e){return Math.atan2(this.x*e-this.y*t,this.x*t+this.y*e)},_matMult:function(t){var e=t[2]*this.x+t[3]*this.y;return this.x=t[0]*this.x+t[1]*this.y,this.y=e,this},_add:function(t){return this.x+=t.x,this.y+=t.y,this},_sub:function(t){return this.x-=t.x,this.y-=t.y,this},_mult:function(t){return this.x*=t,this.y*=t,this},_div:function(t){return this.x/=t,this.y/=t,this},_multByPoint:function(t){return this.x*=t.x,this.y*=t.y,this},_divByPoint:function(t){return this.x/=t.x,this.y/=t.y,this},_unit:function(){return this._div(this.mag()),this},_perp:function(){var t=this.y;return this.y=this.x,this.x=-t,this},_rotate:function(t){var e=Math.cos(t),r=Math.sin(t),n=r*this.x+e*this.y;return this.x=e*this.x-r*this.y,this.y=n,this},_rotateAround:function(t,e){var r=Math.cos(t),n=Math.sin(t),i=e.y+n*(this.x-e.x)+r*(this.y-e.y);return this.x=e.x+r*(this.x-e.x)-n*(this.y-e.y),this.y=i,this},_round:function(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this}},i.convert=function(t){return t instanceof i?t:Array.isArray(t)?new i(t[0],t[1]):t};var s=r(n),a=o;function o(t,e,r,n){this.cx=3*t,this.bx=3*(r-t)-this.cx,this.ax=1-this.cx-this.bx,this.cy=3*e,this.by=3*(n-e)-this.cy,this.ay=1-this.cy-this.by,this.p1x=t,this.p1y=e,this.p2x=r,this.p2y=n;}o.prototype={sampleCurveX:function(t){return ((this.ax*t+this.bx)*t+this.cx)*t},sampleCurveY:function(t){return ((this.ay*t+this.by)*t+this.cy)*t},sampleCurveDerivativeX:function(t){return (3*this.ax*t+2*this.bx)*t+this.cx},solveCurveX:function(t,e){if(void 0===e&&(e=1e-6),t<0)return 0;if(t>1)return 1;for(var r=t,n=0;n<8;n++){var i=this.sampleCurveX(r)-t;if(Math.abs(i)i?a=r:o=r,r=.5*(o-a)+a;return r},solve:function(t,e){return this.sampleCurveY(this.solveCurveX(t,e))}};var l=r(a);let u,c;function h(){return null==u&&(u="undefined"!=typeof OffscreenCanvas&&new OffscreenCanvas(1,1).getContext("2d")&&"function"==typeof createImageBitmap),u}function p(){if(null==c&&(c=!1,h())){const t=5,e=new OffscreenCanvas(t,t).getContext("2d",{willReadFrequently:!0});if(e){for(let r=0;ri.solve(t)}const d=f(.25,.1,.25,1);function y(t,e,r){return Math.min(r,Math.max(e,t))}function m(t,e,r){const n=r-e,i=((t-e)%n+n)%n+e;return i===e?r:i}function g(t,...e){for(const r of e)for(const e in r)t[e]=r[e];return t}let x=1;function v(t,e,r){const n={};for(const r in t)n[r]=e.call(this,t[r],r,t);return n}function b(t,e,r){const n={};for(const r in t)e.call(this,t[r],r,t)&&(n[r]=t[r]);return n}function w(t){return Array.isArray(t)?t.map(w):"object"==typeof t&&t?v(t,w):t}const _={};function A(t){_[t]||("undefined"!=typeof console&&console.warn(t),_[t]=!0);}function S(t,e,r){return (r.y-t.y)*(e.x-t.x)>(e.y-t.y)*(r.x-t.x)}function k(t){return "undefined"!=typeof WorkerGlobalScope&&void 0!==t&&t instanceof WorkerGlobalScope}let M=null;function I(t){return "undefined"!=typeof ImageBitmap&&t instanceof ImageBitmap}const z="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII=";function P(t,r,n,i,s){return e(this,void 0,void 0,(function*(){if("undefined"==typeof VideoFrame)throw new Error("VideoFrame not supported");const e=new VideoFrame(t,{timestamp:0});try{const a=null==e?void 0:e.format;if(!a||!a.startsWith("BGR")&&!a.startsWith("RGB"))throw new Error(`Unrecognized format ${a}`);const o=a.startsWith("BGR"),l=new Uint8ClampedArray(i*s*4);if(yield e.copyTo(l,function(t,e,r,n,i){const s=4*Math.max(-e,0),a=(Math.max(0,r)-r)*n*4+s,o=4*n,l=Math.max(0,e),u=Math.max(0,r);return {rect:{x:l,y:u,width:Math.min(t.width,e+n)-l,height:Math.min(t.height,r+i)-u},layout:[{offset:a,stride:o}]}}(t,r,n,i,s)),o)for(let t=0;tk(self)?self.worker&&self.worker.referrer:("blob:"===window.location.protocol?window.parent:window).location.href,O=function(t,r){if(/:\/\//.test(t.url)&&!/^https?:|^file:/.test(t.url)){const e=T(t.url);if(e)return e(t,r);if(k(self)&&self.worker&&self.worker.actor)return self.worker.actor.sendAsync({type:"GR",data:t,targetMapId:$},r)}if(!(/^file:/.test(n=t.url)||/^file:/.test(D())&&!/^\w+:/.test(n))){if(fetch&&Request&&AbortController&&Object.prototype.hasOwnProperty.call(Request.prototype,"signal"))return function(t,r){return e(this,void 0,void 0,(function*(){const e=new Request(t.url,{method:t.method||"GET",body:t.body,credentials:t.credentials,headers:t.headers,cache:t.cache,referrer:D(),signal:r.signal});"json"!==t.type||e.headers.has("Accept")||e.headers.set("Accept","application/json");const n=yield fetch(e);if(!n.ok){const e=yield n.blob();throw new L(n.status,n.statusText,t.url,e)}let i;i="arrayBuffer"===t.type||"image"===t.type?n.arrayBuffer():"json"===t.type?n.json():n.text();const s=yield i;if(r.signal.aborted)throw E();return {data:s,cacheControl:n.headers.get("Cache-Control"),expires:n.headers.get("Expires")}}))}(t,r);if(k(self)&&self.worker&&self.worker.actor)return self.worker.actor.sendAsync({type:"GR",data:t,mustQueue:!0,targetMapId:$},r)}var n;return function(t,e){return new Promise(((r,n)=>{var i;const s=new XMLHttpRequest;s.open(t.method||"GET",t.url,!0),"arrayBuffer"!==t.type&&"image"!==t.type||(s.responseType="arraybuffer");for(const e in t.headers)s.setRequestHeader(e,t.headers[e]);"json"===t.type&&(s.responseType="text",(null===(i=t.headers)||void 0===i?void 0:i.Accept)||s.setRequestHeader("Accept","application/json")),s.withCredentials="include"===t.credentials,s.onerror=()=>{n(new Error(s.statusText));},s.onload=()=>{if(!e.signal.aborted)if((s.status>=200&&s.status<300||0===s.status)&&null!==s.response){let e=s.response;if("json"===t.type)try{e=JSON.parse(s.response);}catch(t){return void n(t)}r({data:e,cacheControl:s.getResponseHeader("Cache-Control"),expires:s.getResponseHeader("Expires")});}else {const e=new Blob([s.response],{type:s.getResponseHeader("Content-Type")});n(new L(s.status,s.statusText,t.url,e));}},e.signal.addEventListener("abort",(()=>{s.abort(),n(E());})),s.send(t.body);}))}(t,r)};function j(t){if(!t||t.indexOf("://")<=0||0===t.indexOf("data:image/")||0===t.indexOf("blob:"))return !0;const e=new URL(t),r=window.location;return e.protocol===r.protocol&&e.host===r.host}function R(t,e,r){r[t]&&-1!==r[t].indexOf(e)||(r[t]=r[t]||[],r[t].push(e));}function U(t,e,r){if(r&&r[t]){const n=r[t].indexOf(e);-1!==n&&r[t].splice(n,1);}}class q{constructor(t,e={}){g(this,e),this.type=t;}}class N extends q{constructor(t,e={}){super("error",g({error:t},e));}}class Z{on(t,e){return this._listeners=this._listeners||{},R(t,e,this._listeners),this}off(t,e){return U(t,e,this._listeners),U(t,e,this._oneTimeListeners),this}once(t,e){return e?(this._oneTimeListeners=this._oneTimeListeners||{},R(t,e,this._oneTimeListeners),this):new Promise((e=>this.once(t,e)))}fire(t,e){"string"==typeof t&&(t=new q(t,e||{}));const r=t.type;if(this.listens(r)){t.target=this;const e=this._listeners&&this._listeners[r]?this._listeners[r].slice():[];for(const r of e)r.call(this,t);const n=this._oneTimeListeners&&this._oneTimeListeners[r]?this._oneTimeListeners[r].slice():[];for(const e of n)U(r,e,this._oneTimeListeners),e.call(this,t);const i=this._eventedParent;i&&(g(t,"function"==typeof this._eventedParentData?this._eventedParentData():this._eventedParentData),i.fire(t));}else t instanceof N&&console.error(t.error);return this}listens(t){return this._listeners&&this._listeners[t]&&this._listeners[t].length>0||this._oneTimeListeners&&this._oneTimeListeners[t]&&this._oneTimeListeners[t].length>0||this._eventedParent&&this._eventedParent.listens(t)}setEventedParent(t,e){return this._eventedParent=t,this._eventedParentData=e,this}}var G={$version:8,$root:{version:{required:!0,type:"enum",values:[8]},name:{type:"string"},metadata:{type:"*"},center:{type:"array",value:"number"},zoom:{type:"number"},bearing:{type:"number",default:0,period:360,units:"degrees"},pitch:{type:"number",default:0,units:"degrees"},light:{type:"light"},sky:{type:"sky"},projection:{type:"projection"},terrain:{type:"terrain"},sources:{required:!0,type:"sources"},sprite:{type:"sprite"},glyphs:{type:"string"},transition:{type:"transition"},layers:{required:!0,type:"array",value:"layer"}},sources:{"*":{type:"source"}},source:["source_vector","source_raster","source_raster_dem","source_geojson","source_video","source_image"],source_vector:{type:{required:!0,type:"enum",values:{vector:{}}},url:{type:"string"},tiles:{type:"array",value:"string"},bounds:{type:"array",value:"number",length:4,default:[-180,-85.051129,180,85.051129]},scheme:{type:"enum",values:{xyz:{},tms:{}},default:"xyz"},minzoom:{type:"number",default:0},maxzoom:{type:"number",default:22},attribution:{type:"string"},promoteId:{type:"promoteId"},volatile:{type:"boolean",default:!1},"*":{type:"*"}},source_raster:{type:{required:!0,type:"enum",values:{raster:{}}},url:{type:"string"},tiles:{type:"array",value:"string"},bounds:{type:"array",value:"number",length:4,default:[-180,-85.051129,180,85.051129]},minzoom:{type:"number",default:0},maxzoom:{type:"number",default:22},tileSize:{type:"number",default:512,units:"pixels"},scheme:{type:"enum",values:{xyz:{},tms:{}},default:"xyz"},attribution:{type:"string"},volatile:{type:"boolean",default:!1},"*":{type:"*"}},source_raster_dem:{type:{required:!0,type:"enum",values:{"raster-dem":{}}},url:{type:"string"},tiles:{type:"array",value:"string"},bounds:{type:"array",value:"number",length:4,default:[-180,-85.051129,180,85.051129]},minzoom:{type:"number",default:0},maxzoom:{type:"number",default:22},tileSize:{type:"number",default:512,units:"pixels"},attribution:{type:"string"},encoding:{type:"enum",values:{terrarium:{},mapbox:{},custom:{}},default:"mapbox"},redFactor:{type:"number",default:1},blueFactor:{type:"number",default:1},greenFactor:{type:"number",default:1},baseShift:{type:"number",default:0},volatile:{type:"boolean",default:!1},"*":{type:"*"}},source_geojson:{type:{required:!0,type:"enum",values:{geojson:{}}},data:{required:!0,type:"*"},maxzoom:{type:"number",default:18},attribution:{type:"string"},buffer:{type:"number",default:128,maximum:512,minimum:0},filter:{type:"*"},tolerance:{type:"number",default:.375},cluster:{type:"boolean",default:!1},clusterRadius:{type:"number",default:50,minimum:0},clusterMaxZoom:{type:"number"},clusterMinPoints:{type:"number"},clusterProperties:{type:"*"},lineMetrics:{type:"boolean",default:!1},generateId:{type:"boolean",default:!1},promoteId:{type:"promoteId"}},source_video:{type:{required:!0,type:"enum",values:{video:{}}},urls:{required:!0,type:"array",value:"string"},coordinates:{required:!0,type:"array",length:4,value:{type:"array",length:2,value:"number"}}},source_image:{type:{required:!0,type:"enum",values:{image:{}}},url:{required:!0,type:"string"},coordinates:{required:!0,type:"array",length:4,value:{type:"array",length:2,value:"number"}}},layer:{id:{type:"string",required:!0},type:{type:"enum",values:{fill:{},line:{},symbol:{},circle:{},heatmap:{},"fill-extrusion":{},raster:{},hillshade:{},background:{}},required:!0},metadata:{type:"*"},source:{type:"string"},"source-layer":{type:"string"},minzoom:{type:"number",minimum:0,maximum:24},maxzoom:{type:"number",minimum:0,maximum:24},filter:{type:"filter"},layout:{type:"layout"},paint:{type:"paint"}},layout:["layout_fill","layout_line","layout_circle","layout_heatmap","layout_fill-extrusion","layout_symbol","layout_raster","layout_hillshade","layout_background"],layout_background:{visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},layout_fill:{"fill-sort-key":{type:"number",expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},layout_circle:{"circle-sort-key":{type:"number",expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},layout_heatmap:{visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},"layout_fill-extrusion":{visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},layout_line:{"line-cap":{type:"enum",values:{butt:{},round:{},square:{}},default:"butt",expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"line-join":{type:"enum",values:{bevel:{},round:{},miter:{}},default:"miter",expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},"line-miter-limit":{type:"number",default:2,requires:[{"line-join":"miter"}],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"line-round-limit":{type:"number",default:1.05,requires:[{"line-join":"round"}],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"line-sort-key":{type:"number",expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},layout_symbol:{"symbol-placement":{type:"enum",values:{point:{},line:{},"line-center":{}},default:"point",expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"symbol-spacing":{type:"number",default:250,minimum:1,units:"pixels",requires:[{"symbol-placement":"line"}],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"symbol-avoid-edges":{type:"boolean",default:!1,expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"symbol-sort-key":{type:"number",expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},"symbol-z-order":{type:"enum",values:{auto:{},"viewport-y":{},source:{}},default:"auto",expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"icon-allow-overlap":{type:"boolean",default:!1,requires:["icon-image",{"!":"icon-overlap"}],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"icon-overlap":{type:"enum",values:{never:{},always:{},cooperative:{}},requires:["icon-image"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"icon-ignore-placement":{type:"boolean",default:!1,requires:["icon-image"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"icon-optional":{type:"boolean",default:!1,requires:["icon-image","text-field"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"icon-rotation-alignment":{type:"enum",values:{map:{},viewport:{},auto:{}},default:"auto",requires:["icon-image"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"icon-size":{type:"number",default:1,minimum:0,units:"factor of the original icon size",requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"icon-text-fit":{type:"enum",values:{none:{},width:{},height:{},both:{}},default:"none",requires:["icon-image","text-field"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"icon-text-fit-padding":{type:"array",value:"number",length:4,default:[0,0,0,0],units:"pixels",requires:["icon-image","text-field",{"icon-text-fit":["both","width","height"]}],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"icon-image":{type:"resolvedImage",tokens:!0,expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},"icon-rotate":{type:"number",default:0,period:360,units:"degrees",requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"icon-padding":{type:"padding",default:[2],units:"pixels",requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"icon-keep-upright":{type:"boolean",default:!1,requires:["icon-image",{"icon-rotation-alignment":"map"},{"symbol-placement":["line","line-center"]}],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"icon-offset":{type:"array",value:"number",length:2,default:[0,0],requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"icon-anchor":{type:"enum",values:{center:{},left:{},right:{},top:{},bottom:{},"top-left":{},"top-right":{},"bottom-left":{},"bottom-right":{}},default:"center",requires:["icon-image"],expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},"icon-pitch-alignment":{type:"enum",values:{map:{},viewport:{},auto:{}},default:"auto",requires:["icon-image"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-pitch-alignment":{type:"enum",values:{map:{},viewport:{},auto:{}},default:"auto",requires:["text-field"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-rotation-alignment":{type:"enum",values:{map:{},viewport:{},"viewport-glyph":{},auto:{}},default:"auto",requires:["text-field"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-field":{type:"formatted",default:"",tokens:!0,expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-font":{type:"array",value:"string",default:["Open Sans Regular","Arial Unicode MS Regular"],requires:["text-field"],expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-size":{type:"number",default:16,minimum:0,units:"pixels",requires:["text-field"],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-max-width":{type:"number",default:10,minimum:0,units:"ems",requires:["text-field"],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-line-height":{type:"number",default:1.2,units:"ems",requires:["text-field"],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"text-letter-spacing":{type:"number",default:0,units:"ems",requires:["text-field"],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-justify":{type:"enum",values:{auto:{},left:{},center:{},right:{}},default:"center",requires:["text-field"],expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-radial-offset":{type:"number",units:"ems",default:0,requires:["text-field"],"property-type":"data-driven",expression:{interpolated:!0,parameters:["zoom","feature"]}},"text-variable-anchor":{type:"array",value:"enum",values:{center:{},left:{},right:{},top:{},bottom:{},"top-left":{},"top-right":{},"bottom-left":{},"bottom-right":{}},requires:["text-field",{"symbol-placement":["point"]}],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-variable-anchor-offset":{type:"variableAnchorOffsetCollection",requires:["text-field",{"symbol-placement":["point"]}],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-anchor":{type:"enum",values:{center:{},left:{},right:{},top:{},bottom:{},"top-left":{},"top-right":{},"bottom-left":{},"bottom-right":{}},default:"center",requires:["text-field",{"!":"text-variable-anchor"}],expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-max-angle":{type:"number",default:45,units:"degrees",requires:["text-field",{"symbol-placement":["line","line-center"]}],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"text-writing-mode":{type:"array",value:"enum",values:{horizontal:{},vertical:{}},requires:["text-field",{"symbol-placement":["point"]}],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-rotate":{type:"number",default:0,period:360,units:"degrees",requires:["text-field"],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-padding":{type:"number",default:2,minimum:0,units:"pixels",requires:["text-field"],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"text-keep-upright":{type:"boolean",default:!0,requires:["text-field",{"text-rotation-alignment":"map"},{"symbol-placement":["line","line-center"]}],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-transform":{type:"enum",values:{none:{},uppercase:{},lowercase:{}},default:"none",requires:["text-field"],expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-offset":{type:"array",value:"number",units:"ems",length:2,default:[0,0],requires:["text-field",{"!":"text-radial-offset"}],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-allow-overlap":{type:"boolean",default:!1,requires:["text-field",{"!":"text-overlap"}],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-overlap":{type:"enum",values:{never:{},always:{},cooperative:{}},requires:["text-field"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-ignore-placement":{type:"boolean",default:!1,requires:["text-field"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-optional":{type:"boolean",default:!1,requires:["text-field","icon-image"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},layout_raster:{visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},layout_hillshade:{visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},filter:{type:"array",value:"*"},filter_operator:{type:"enum",values:{"==":{},"!=":{},">":{},">=":{},"<":{},"<=":{},in:{},"!in":{},all:{},any:{},none:{},has:{},"!has":{}}},geometry_type:{type:"enum",values:{Point:{},LineString:{},Polygon:{}}},function:{expression:{type:"expression"},stops:{type:"array",value:"function_stop"},base:{type:"number",default:1,minimum:0},property:{type:"string",default:"$zoom"},type:{type:"enum",values:{identity:{},exponential:{},interval:{},categorical:{}},default:"exponential"},colorSpace:{type:"enum",values:{rgb:{},lab:{},hcl:{}},default:"rgb"},default:{type:"*",required:!1}},function_stop:{type:"array",minimum:0,maximum:24,value:["number","color"],length:2},expression:{type:"array",value:"*",minimum:1},light:{anchor:{type:"enum",default:"viewport",values:{map:{},viewport:{}},"property-type":"data-constant",transition:!1,expression:{interpolated:!1,parameters:["zoom"]}},position:{type:"array",default:[1.15,210,30],length:3,value:"number","property-type":"data-constant",transition:!0,expression:{interpolated:!0,parameters:["zoom"]}},color:{type:"color","property-type":"data-constant",default:"#ffffff",expression:{interpolated:!0,parameters:["zoom"]},transition:!0},intensity:{type:"number","property-type":"data-constant",default:.5,minimum:0,maximum:1,expression:{interpolated:!0,parameters:["zoom"]},transition:!0}},sky:{"sky-color":{type:"color","property-type":"data-constant",default:"#88C6FC",expression:{interpolated:!0,parameters:["zoom"]},transition:!0},"horizon-color":{type:"color","property-type":"data-constant",default:"#ffffff",expression:{interpolated:!0,parameters:["zoom"]},transition:!0},"fog-color":{type:"color","property-type":"data-constant",default:"#ffffff",expression:{interpolated:!0,parameters:["zoom"]},transition:!0},"fog-ground-blend":{type:"number","property-type":"data-constant",default:.5,minimum:0,maximum:1,expression:{interpolated:!0,parameters:["zoom"]},transition:!0},"horizon-fog-blend":{type:"number","property-type":"data-constant",default:.8,minimum:0,maximum:1,expression:{interpolated:!0,parameters:["zoom"]},transition:!0},"sky-horizon-blend":{type:"number","property-type":"data-constant",default:.8,minimum:0,maximum:1,expression:{interpolated:!0,parameters:["zoom"]},transition:!0},"atmosphere-blend":{type:"number","property-type":"data-constant",default:.8,minimum:0,maximum:1,expression:{interpolated:!0,parameters:["zoom"]},transition:!0}},terrain:{source:{type:"string",required:!0},exaggeration:{type:"number",minimum:0,default:1}},projection:{type:{type:"enum",default:"mercator",values:{mercator:{},globe:{}}}},paint:["paint_fill","paint_line","paint_circle","paint_heatmap","paint_fill-extrusion","paint_symbol","paint_raster","paint_hillshade","paint_background"],paint_fill:{"fill-antialias":{type:"boolean",default:!0,expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"fill-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"fill-color":{type:"color",default:"#000000",transition:!0,requires:[{"!":"fill-pattern"}],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"fill-outline-color":{type:"color",transition:!0,requires:[{"!":"fill-pattern"},{"fill-antialias":!0}],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"fill-translate":{type:"array",value:"number",length:2,default:[0,0],transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"fill-translate-anchor":{type:"enum",values:{map:{},viewport:{}},default:"map",requires:["fill-translate"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"fill-pattern":{type:"resolvedImage",transition:!0,expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"cross-faded-data-driven"}},"paint_fill-extrusion":{"fill-extrusion-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"fill-extrusion-color":{type:"color",default:"#000000",transition:!0,requires:[{"!":"fill-extrusion-pattern"}],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"fill-extrusion-translate":{type:"array",value:"number",length:2,default:[0,0],transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"fill-extrusion-translate-anchor":{type:"enum",values:{map:{},viewport:{}},default:"map",requires:["fill-extrusion-translate"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"fill-extrusion-pattern":{type:"resolvedImage",transition:!0,expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"cross-faded-data-driven"},"fill-extrusion-height":{type:"number",default:0,minimum:0,units:"meters",transition:!0,expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"fill-extrusion-base":{type:"number",default:0,minimum:0,units:"meters",transition:!0,requires:["fill-extrusion-height"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"fill-extrusion-vertical-gradient":{type:"boolean",default:!0,transition:!1,expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"}},paint_line:{"line-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"line-color":{type:"color",default:"#000000",transition:!0,requires:[{"!":"line-pattern"}],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"line-translate":{type:"array",value:"number",length:2,default:[0,0],transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"line-translate-anchor":{type:"enum",values:{map:{},viewport:{}},default:"map",requires:["line-translate"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"line-width":{type:"number",default:1,minimum:0,transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"line-gap-width":{type:"number",default:0,minimum:0,transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"line-offset":{type:"number",default:0,transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"line-blur":{type:"number",default:0,minimum:0,transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"line-dasharray":{type:"array",value:"number",minimum:0,transition:!0,units:"line widths",requires:[{"!":"line-pattern"}],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"cross-faded"},"line-pattern":{type:"resolvedImage",transition:!0,expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"cross-faded-data-driven"},"line-gradient":{type:"color",transition:!1,requires:[{"!":"line-dasharray"},{"!":"line-pattern"},{source:"geojson",has:{lineMetrics:!0}}],expression:{interpolated:!0,parameters:["line-progress"]},"property-type":"color-ramp"}},paint_circle:{"circle-radius":{type:"number",default:5,minimum:0,transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"circle-color":{type:"color",default:"#000000",transition:!0,expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"circle-blur":{type:"number",default:0,transition:!0,expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"circle-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"circle-translate":{type:"array",value:"number",length:2,default:[0,0],transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"circle-translate-anchor":{type:"enum",values:{map:{},viewport:{}},default:"map",requires:["circle-translate"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"circle-pitch-scale":{type:"enum",values:{map:{},viewport:{}},default:"map",expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"circle-pitch-alignment":{type:"enum",values:{map:{},viewport:{}},default:"viewport",expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"circle-stroke-width":{type:"number",default:0,minimum:0,transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"circle-stroke-color":{type:"color",default:"#000000",transition:!0,expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"circle-stroke-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"}},paint_heatmap:{"heatmap-radius":{type:"number",default:30,minimum:1,transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"heatmap-weight":{type:"number",default:1,minimum:0,transition:!1,expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"heatmap-intensity":{type:"number",default:1,minimum:0,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"heatmap-color":{type:"color",default:["interpolate",["linear"],["heatmap-density"],0,"rgba(0, 0, 255, 0)",.1,"royalblue",.3,"cyan",.5,"lime",.7,"yellow",1,"red"],transition:!1,expression:{interpolated:!0,parameters:["heatmap-density"]},"property-type":"color-ramp"},"heatmap-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"}},paint_symbol:{"icon-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"icon-color":{type:"color",default:"#000000",transition:!0,requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"icon-halo-color":{type:"color",default:"rgba(0, 0, 0, 0)",transition:!0,requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"icon-halo-width":{type:"number",default:0,minimum:0,transition:!0,units:"pixels",requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"icon-halo-blur":{type:"number",default:0,minimum:0,transition:!0,units:"pixels",requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"icon-translate":{type:"array",value:"number",length:2,default:[0,0],transition:!0,units:"pixels",requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"icon-translate-anchor":{type:"enum",values:{map:{},viewport:{}},default:"map",requires:["icon-image","icon-translate"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,requires:["text-field"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"text-color":{type:"color",default:"#000000",transition:!0,overridable:!0,requires:["text-field"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"text-halo-color":{type:"color",default:"rgba(0, 0, 0, 0)",transition:!0,requires:["text-field"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"text-halo-width":{type:"number",default:0,minimum:0,transition:!0,units:"pixels",requires:["text-field"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"text-halo-blur":{type:"number",default:0,minimum:0,transition:!0,units:"pixels",requires:["text-field"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"text-translate":{type:"array",value:"number",length:2,default:[0,0],transition:!0,units:"pixels",requires:["text-field"],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"text-translate-anchor":{type:"enum",values:{map:{},viewport:{}},default:"map",requires:["text-field","text-translate"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"}},paint_raster:{"raster-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"raster-hue-rotate":{type:"number",default:0,period:360,transition:!0,units:"degrees",expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"raster-brightness-min":{type:"number",default:0,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"raster-brightness-max":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"raster-saturation":{type:"number",default:0,minimum:-1,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"raster-contrast":{type:"number",default:0,minimum:-1,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"raster-resampling":{type:"enum",values:{linear:{},nearest:{}},default:"linear",expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"raster-fade-duration":{type:"number",default:300,minimum:0,transition:!1,units:"milliseconds",expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"}},paint_hillshade:{"hillshade-illumination-direction":{type:"number",default:335,minimum:0,maximum:359,transition:!1,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"hillshade-illumination-anchor":{type:"enum",values:{map:{},viewport:{}},default:"viewport",expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"hillshade-exaggeration":{type:"number",default:.5,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"hillshade-shadow-color":{type:"color",default:"#000000",transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"hillshade-highlight-color":{type:"color",default:"#FFFFFF",transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"hillshade-accent-color":{type:"color",default:"#000000",transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"}},paint_background:{"background-color":{type:"color",default:"#000000",transition:!0,requires:[{"!":"background-pattern"}],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"background-pattern":{type:"resolvedImage",transition:!0,expression:{interpolated:!1,parameters:["zoom"]},"property-type":"cross-faded"},"background-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"}},transition:{duration:{type:"number",default:300,minimum:0,units:"milliseconds"},delay:{type:"number",default:0,minimum:0,units:"milliseconds"}},"property-type":{"data-driven":{type:"property-type"},"cross-faded":{type:"property-type"},"cross-faded-data-driven":{type:"property-type"},"color-ramp":{type:"property-type"},"data-constant":{type:"property-type"},constant:{type:"property-type"}},promoteId:{"*":{type:"string"}}};const K=["type","source","source-layer","minzoom","maxzoom","filter","layout"];function X(t,e){const r={};for(const e in t)"ref"!==e&&(r[e]=t[e]);return K.forEach((t=>{t in e&&(r[t]=e[t]);})),r}function H(t,e){if(Array.isArray(t)){if(!Array.isArray(e)||t.length!==e.length)return !1;for(let r=0;r`:"value"===t.itemType.kind?"array":`array<${e}>`}return t.kind}const _t=[lt,ut,ct,ht,pt,mt,ft,bt(dt),gt,xt,vt];function At(t,e){if("error"===e.kind)return null;if("array"===t.kind){if("array"===e.kind&&(0===e.N&&"value"===e.itemType.kind||!At(t.itemType,e.itemType))&&("number"!=typeof t.N||t.N===e.N))return null}else {if(t.kind===e.kind)return null;if("value"===t.kind)for(const t of _t)if(!At(t,e))return null}return `Expected ${wt(t)} but found ${wt(e)} instead.`}function St(t,e){return e.some((e=>e.kind===t.kind))}function kt(t,e){return e.some((e=>"null"===e?null===t:"array"===e?Array.isArray(t):"object"===e?t&&!Array.isArray(t)&&"object"==typeof t:e===typeof t))}function Mt(t,e){return "array"===t.kind&&"array"===e.kind?t.itemType.kind===e.itemType.kind&&"number"==typeof t.N:t.kind===e.kind}const It=.96422,zt=.82521,Pt=4/29,Ct=6/29,Bt=3*Ct*Ct,Vt=Ct*Ct*Ct,Et=Math.PI/180,Ft=180/Math.PI;function Tt(t){return (t%=360)<0&&(t+=360),t}function $t([t,e,r,n]){let i,s;const a=Dt((.2225045*(t=Lt(t))+.7168786*(e=Lt(e))+.0606169*(r=Lt(r)))/1);t===e&&e===r?i=s=a:(i=Dt((.4360747*t+.3850649*e+.1430804*r)/It),s=Dt((.0139322*t+.0971045*e+.7141733*r)/zt));const o=116*a-16;return [o<0?0:o,500*(i-a),200*(a-s),n]}function Lt(t){return t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Dt(t){return t>Vt?Math.pow(t,1/3):t/Bt+Pt}function Ot([t,e,r,n]){let i=(t+16)/116,s=isNaN(e)?i:i+e/500,a=isNaN(r)?i:i-r/200;return i=1*Rt(i),s=It*Rt(s),a=zt*Rt(a),[jt(3.1338561*s-1.6168667*i-.4906146*a),jt(-.9787684*s+1.9161415*i+.033454*a),jt(.0719453*s-.2289914*i+1.4052427*a),n]}function jt(t){return (t=t<=.00304?12.92*t:1.055*Math.pow(t,1/2.4)-.055)<0?0:t>1?1:t}function Rt(t){return t>Ct?t*t*t:Bt*(t-Pt)}function Ut(t){return parseInt(t.padEnd(2,t),16)/255}function qt(t,e){return Nt(e?t/100:t,0,1)}function Nt(t,e,r){return Math.min(Math.max(e,t),r)}function Zt(t){return !t.some(Number.isNaN)}const Gt={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]};class Kt{constructor(t,e,r,n=1,i=!0){this.r=t,this.g=e,this.b=r,this.a=n,i||(this.r*=n,this.g*=n,this.b*=n,n||this.overwriteGetter("rgb",[t,e,r,n]));}static parse(t){if(t instanceof Kt)return t;if("string"!=typeof t)return;const e=function(t){if("transparent"===(t=t.toLowerCase().trim()))return [0,0,0,0];const e=Gt[t];if(e){const[t,r,n]=e;return [t/255,r/255,n/255,1]}if(t.startsWith("#")&&/^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(t)){const e=t.length<6?1:2;let r=1;return [Ut(t.slice(r,r+=e)),Ut(t.slice(r,r+=e)),Ut(t.slice(r,r+=e)),Ut(t.slice(r,r+e)||"ff")]}if(t.startsWith("rgb")){const e=t.match(/^rgba?\(\s*([\de.+-]+)(%)?(?:\s+|\s*(,)\s*)([\de.+-]+)(%)?(?:\s+|\s*(,)\s*)([\de.+-]+)(%)?(?:\s*([,\/])\s*([\de.+-]+)(%)?)?\s*\)$/);if(e){const[t,r,n,i,s,a,o,l,u,c,h,p]=e,f=[i||" ",o||" ",c].join("");if(" "===f||" /"===f||",,"===f||",,,"===f){const t=[n,a,u].join(""),e="%%%"===t?100:""===t?255:0;if(e){const t=[Nt(+r/e,0,1),Nt(+s/e,0,1),Nt(+l/e,0,1),h?qt(+h,p):1];if(Zt(t))return t}}return}}const r=t.match(/^hsla?\(\s*([\de.+-]+)(?:deg)?(?:\s+|\s*(,)\s*)([\de.+-]+)%(?:\s+|\s*(,)\s*)([\de.+-]+)%(?:\s*([,\/])\s*([\de.+-]+)(%)?)?\s*\)$/);if(r){const[t,e,n,i,s,a,o,l,u]=r,c=[n||" ",s||" ",o].join("");if(" "===c||" /"===c||",,"===c||",,,"===c){const t=[+e,Nt(+i,0,100),Nt(+a,0,100),l?qt(+l,u):1];if(Zt(t))return function([t,e,r,n]){function i(n){const i=(n+t/30)%12,s=e*Math.min(r,1-r);return r-s*Math.max(-1,Math.min(i-3,9-i,1))}return t=Tt(t),e/=100,r/=100,[i(0),i(8),i(4),n]}(t)}}}(t);return e?new Kt(...e,!1):void 0}get rgb(){const{r:t,g:e,b:r,a:n}=this,i=n||1/0;return this.overwriteGetter("rgb",[t/i,e/i,r/i,n])}get hcl(){return this.overwriteGetter("hcl",function(t){const[e,r,n,i]=$t(t),s=Math.sqrt(r*r+n*n);return [Math.round(1e4*s)?Tt(Math.atan2(n,r)*Ft):NaN,s,e,i]}(this.rgb))}get lab(){return this.overwriteGetter("lab",$t(this.rgb))}overwriteGetter(t,e){return Object.defineProperty(this,t,{value:e}),e}toString(){const[t,e,r,n]=this.rgb;return `rgba(${[t,e,r].map((t=>Math.round(255*t))).join(",")},${n})`}}Kt.black=new Kt(0,0,0,1),Kt.white=new Kt(1,1,1,1),Kt.transparent=new Kt(0,0,0,0),Kt.red=new Kt(1,0,0,1);class Xt{constructor(t,e,r){this.sensitivity=t?e?"variant":"case":e?"accent":"base",this.locale=r,this.collator=new Intl.Collator(this.locale?this.locale:[],{sensitivity:this.sensitivity,usage:"search"});}compare(t,e){return this.collator.compare(t,e)}resolvedLocale(){return new Intl.Collator(this.locale?this.locale:[]).resolvedOptions().locale}}class Ht{constructor(t,e,r,n,i){this.text=t,this.image=e,this.scale=r,this.fontStack=n,this.textColor=i;}}class Yt{constructor(t){this.sections=t;}static fromString(t){return new Yt([new Ht(t,null,null,null,null)])}isEmpty(){return 0===this.sections.length||!this.sections.some((t=>0!==t.text.length||t.image&&0!==t.image.name.length))}static factory(t){return t instanceof Yt?t:Yt.fromString(t)}toString(){return 0===this.sections.length?"":this.sections.map((t=>t.text)).join("")}}class Jt{constructor(t){this.values=t.slice();}static parse(t){if(t instanceof Jt)return t;if("number"==typeof t)return new Jt([t,t,t,t]);if(Array.isArray(t)&&!(t.length<1||t.length>4)){for(const e of t)if("number"!=typeof e)return;switch(t.length){case 1:t=[t[0],t[0],t[0],t[0]];break;case 2:t=[t[0],t[1],t[0],t[1]];break;case 3:t=[t[0],t[1],t[2],t[1]];}return new Jt(t)}}toString(){return JSON.stringify(this.values)}}const Wt=new Set(["center","left","right","top","bottom","top-left","top-right","bottom-left","bottom-right"]);class Qt{constructor(t){this.values=t.slice();}static parse(t){if(t instanceof Qt)return t;if(Array.isArray(t)&&!(t.length<1)&&t.length%2==0){for(let e=0;e=0&&t<=255&&"number"==typeof e&&e>=0&&e<=255&&"number"==typeof r&&r>=0&&r<=255?void 0===n||"number"==typeof n&&n>=0&&n<=1?null:`Invalid rgba value [${[t,e,r,n].join(", ")}]: 'a' must be between 0 and 1.`:`Invalid rgba value [${("number"==typeof n?[t,e,r,n]:[t,e,r]).join(", ")}]: 'r', 'g', and 'b' must be between 0 and 255.`}function re(t){if(null===t||"string"==typeof t||"boolean"==typeof t||"number"==typeof t||t instanceof Kt||t instanceof Xt||t instanceof Yt||t instanceof Jt||t instanceof Qt||t instanceof te)return !0;if(Array.isArray(t)){for(const e of t)if(!re(e))return !1;return !0}if("object"==typeof t){for(const e in t)if(!re(t[e]))return !1;return !0}return !1}function ne(t){if(null===t)return lt;if("string"==typeof t)return ct;if("boolean"==typeof t)return ht;if("number"==typeof t)return ut;if(t instanceof Kt)return pt;if(t instanceof Xt)return yt;if(t instanceof Yt)return mt;if(t instanceof Jt)return gt;if(t instanceof Qt)return vt;if(t instanceof te)return xt;if(Array.isArray(t)){const e=t.length;let r;for(const e of t){const t=ne(e);if(r){if(r===t)continue;r=dt;break}r=t;}return bt(r||dt,e)}return ft}function ie(t){const e=typeof t;return null===t?"":"string"===e||"number"===e||"boolean"===e?String(t):t instanceof Kt||t instanceof Yt||t instanceof Jt||t instanceof Qt||t instanceof te?t.toString():JSON.stringify(t)}class se{constructor(t,e){this.type=t,this.value=e;}static parse(t,e){if(2!==t.length)return e.error(`'literal' expression requires exactly one argument, but found ${t.length-1} instead.`);if(!re(t[1]))return e.error("invalid value");const r=t[1];let n=ne(r);const i=e.expectedType;return "array"!==n.kind||0!==n.N||!i||"array"!==i.kind||"number"==typeof i.N&&0!==i.N||(n=i),new se(n,r)}evaluate(){return this.value}eachChild(){}outputDefined(){return !0}}class ae{constructor(t){this.name="ExpressionEvaluationError",this.message=t;}toJSON(){return this.message}}const oe={string:ct,number:ut,boolean:ht,object:ft};class le{constructor(t,e){this.type=t,this.args=e;}static parse(t,e){if(t.length<2)return e.error("Expected at least one argument.");let r,n=1;const i=t[0];if("array"===i){let i,s;if(t.length>2){const r=t[1];if("string"!=typeof r||!(r in oe)||"object"===r)return e.error('The item type argument of "array" must be one of string, number, boolean',1);i=oe[r],n++;}else i=dt;if(t.length>3){if(null!==t[2]&&("number"!=typeof t[2]||t[2]<0||t[2]!==Math.floor(t[2])))return e.error('The length argument to "array" must be a positive integer literal',2);s=t[2],n++;}r=bt(i,s);}else {if(!oe[i])throw new Error(`Types doesn't contain name = ${i}`);r=oe[i];}const s=[];for(;nt.outputDefined()))}}const ue={"to-boolean":ht,"to-color":pt,"to-number":ut,"to-string":ct};class ce{constructor(t,e){this.type=t,this.args=e;}static parse(t,e){if(t.length<2)return e.error("Expected at least one argument.");const r=t[0];if(!ue[r])throw new Error(`Can't parse ${r} as it is not part of the known types`);if(("to-boolean"===r||"to-string"===r)&&2!==t.length)return e.error("Expected one argument.");const n=ue[r],i=[];for(let r=1;r4?`Invalid rbga value ${JSON.stringify(e)}: expected an array containing either three or four numeric values.`:ee(e[0],e[1],e[2],e[3]),!r))return new Kt(e[0]/255,e[1]/255,e[2]/255,e[3])}throw new ae(r||`Could not parse color from value '${"string"==typeof e?e:JSON.stringify(e)}'`)}case"padding":{let e;for(const r of this.args){e=r.evaluate(t);const n=Jt.parse(e);if(n)return n}throw new ae(`Could not parse padding from value '${"string"==typeof e?e:JSON.stringify(e)}'`)}case"variableAnchorOffsetCollection":{let e;for(const r of this.args){e=r.evaluate(t);const n=Qt.parse(e);if(n)return n}throw new ae(`Could not parse variableAnchorOffsetCollection from value '${"string"==typeof e?e:JSON.stringify(e)}'`)}case"number":{let e=null;for(const r of this.args){if(e=r.evaluate(t),null===e)return 0;const n=Number(e);if(!isNaN(n))return n}throw new ae(`Could not convert ${JSON.stringify(e)} to number.`)}case"formatted":return Yt.fromString(ie(this.args[0].evaluate(t)));case"resolvedImage":return te.fromString(ie(this.args[0].evaluate(t)));default:return ie(this.args[0].evaluate(t))}}eachChild(t){this.args.forEach(t);}outputDefined(){return this.args.every((t=>t.outputDefined()))}}const he=["Unknown","Point","LineString","Polygon"];class pe{constructor(){this.globals=null,this.feature=null,this.featureState=null,this.formattedSection=null,this._parseColorCache={},this.availableImages=null,this.canonical=null;}id(){return this.feature&&"id"in this.feature?this.feature.id:null}geometryType(){return this.feature?"number"==typeof this.feature.type?he[this.feature.type]:this.feature.type:null}geometry(){return this.feature&&"geometry"in this.feature?this.feature.geometry:null}canonicalID(){return this.canonical}properties(){return this.feature&&this.feature.properties||{}}parseColor(t){let e=this._parseColorCache[t];return e||(e=this._parseColorCache[t]=Kt.parse(t)),e}}class fe{constructor(t,e,r=[],n,i=new ot,s=[]){this.registry=t,this.path=r,this.key=r.map((t=>`[${t}]`)).join(""),this.scope=i,this.errors=s,this.expectedType=n,this._isConstant=e;}parse(t,e,r,n,i={}){return e?this.concat(e,r,n)._parse(t,i):this._parse(t,i)}_parse(t,e){function r(t,e,r){return "assert"===r?new le(e,[t]):"coerce"===r?new ce(e,[t]):t}if(null!==t&&"string"!=typeof t&&"boolean"!=typeof t&&"number"!=typeof t||(t=["literal",t]),Array.isArray(t)){if(0===t.length)return this.error('Expected an array with at least one element. If you wanted a literal array, use ["literal", []].');const n=t[0];if("string"!=typeof n)return this.error(`Expression name must be a string, but found ${typeof n} instead. If you wanted a literal array, use ["literal", [...]].`,0),null;const i=this.registry[n];if(i){let n=i.parse(t,this);if(!n)return null;if(this.expectedType){const t=this.expectedType,i=n.type;if("string"!==t.kind&&"number"!==t.kind&&"boolean"!==t.kind&&"object"!==t.kind&&"array"!==t.kind||"value"!==i.kind)if("color"!==t.kind&&"formatted"!==t.kind&&"resolvedImage"!==t.kind||"value"!==i.kind&&"string"!==i.kind)if("padding"!==t.kind||"value"!==i.kind&&"number"!==i.kind&&"array"!==i.kind)if("variableAnchorOffsetCollection"!==t.kind||"value"!==i.kind&&"array"!==i.kind){if(this.checkSubtype(t,i))return null}else n=r(n,t,e.typeAnnotation||"coerce");else n=r(n,t,e.typeAnnotation||"coerce");else n=r(n,t,e.typeAnnotation||"coerce");else n=r(n,t,e.typeAnnotation||"assert");}if(!(n instanceof se)&&"resolvedImage"!==n.type.kind&&this._isConstant(n)){const t=new pe;try{n=new se(n.type,n.evaluate(t));}catch(t){return this.error(t.message),null}}return n}return this.error(`Unknown expression "${n}". If you wanted a literal array, use ["literal", [...]].`,0)}return this.error(void 0===t?"'undefined' value invalid. Use null instead.":"object"==typeof t?'Bare objects invalid. Use ["literal", {...}] instead.':`Expected an array, but found ${typeof t} instead.`)}concat(t,e,r){const n="number"==typeof t?this.path.concat(t):this.path,i=r?this.scope.concat(r):this.scope;return new fe(this.registry,this._isConstant,n,e||null,i,this.errors)}error(t,...e){const r=`${this.key}${e.map((t=>`[${t}]`)).join("")}`;this.errors.push(new at(r,t));}checkSubtype(t,e){const r=At(t,e);return r&&this.error(r),r}}class de{constructor(t,e){this.type=e.type,this.bindings=[].concat(t),this.result=e;}evaluate(t){return this.result.evaluate(t)}eachChild(t){for(const e of this.bindings)t(e[1]);t(this.result);}static parse(t,e){if(t.length<4)return e.error(`Expected at least 3 arguments, but found ${t.length-1} instead.`);const r=[];for(let n=1;n=r.length)throw new ae(`Array index out of bounds: ${e} > ${r.length-1}.`);if(e!==Math.floor(e))throw new ae(`Array index must be an integer, but found ${e} instead.`);return r[e]}eachChild(t){t(this.index),t(this.input);}outputDefined(){return !1}}class ge{constructor(t,e){this.type=ht,this.needle=t,this.haystack=e;}static parse(t,e){if(3!==t.length)return e.error(`Expected 2 arguments, but found ${t.length-1} instead.`);const r=e.parse(t[1],1,dt),n=e.parse(t[2],2,dt);return r&&n?St(r.type,[ht,ct,ut,lt,dt])?new ge(r,n):e.error(`Expected first argument to be of type boolean, string, number or null, but found ${wt(r.type)} instead`):null}evaluate(t){const e=this.needle.evaluate(t),r=this.haystack.evaluate(t);if(!r)return !1;if(!kt(e,["boolean","string","number","null"]))throw new ae(`Expected first argument to be of type boolean, string, number or null, but found ${wt(ne(e))} instead.`);if(!kt(r,["string","array"]))throw new ae(`Expected second argument to be of type array or string, but found ${wt(ne(r))} instead.`);return r.indexOf(e)>=0}eachChild(t){t(this.needle),t(this.haystack);}outputDefined(){return !0}}class xe{constructor(t,e,r){this.type=ut,this.needle=t,this.haystack=e,this.fromIndex=r;}static parse(t,e){if(t.length<=2||t.length>=5)return e.error(`Expected 3 or 4 arguments, but found ${t.length-1} instead.`);const r=e.parse(t[1],1,dt),n=e.parse(t[2],2,dt);if(!r||!n)return null;if(!St(r.type,[ht,ct,ut,lt,dt]))return e.error(`Expected first argument to be of type boolean, string, number or null, but found ${wt(r.type)} instead`);if(4===t.length){const i=e.parse(t[3],3,ut);return i?new xe(r,n,i):null}return new xe(r,n)}evaluate(t){const e=this.needle.evaluate(t),r=this.haystack.evaluate(t);if(!kt(e,["boolean","string","number","null"]))throw new ae(`Expected first argument to be of type boolean, string, number or null, but found ${wt(ne(e))} instead.`);let n;if(this.fromIndex&&(n=this.fromIndex.evaluate(t)),kt(r,["string"])){const t=r.indexOf(e,n);return -1===t?-1:[...r.slice(0,t)].length}if(kt(r,["array"]))return r.indexOf(e,n);throw new ae(`Expected second argument to be of type array or string, but found ${wt(ne(r))} instead.`)}eachChild(t){t(this.needle),t(this.haystack),this.fromIndex&&t(this.fromIndex);}outputDefined(){return !1}}class ve{constructor(t,e,r,n,i,s){this.inputType=t,this.type=e,this.input=r,this.cases=n,this.outputs=i,this.otherwise=s;}static parse(t,e){if(t.length<5)return e.error(`Expected at least 4 arguments, but found only ${t.length-1}.`);if(t.length%2!=1)return e.error("Expected an even number of arguments.");let r,n;e.expectedType&&"value"!==e.expectedType.kind&&(n=e.expectedType);const i={},s=[];for(let a=2;aNumber.MAX_SAFE_INTEGER)return u.error(`Branch labels must be integers no larger than ${Number.MAX_SAFE_INTEGER}.`);if("number"==typeof t&&Math.floor(t)!==t)return u.error("Numeric branch labels must be integer values.");if(r){if(u.checkSubtype(r,ne(t)))return null}else r=ne(t);if(void 0!==i[String(t)])return u.error("Branch labels must be unique.");i[String(t)]=s.length;}const c=e.parse(l,a,n);if(!c)return null;n=n||c.type,s.push(c);}const a=e.parse(t[1],1,dt);if(!a)return null;const o=e.parse(t[t.length-1],t.length-1,n);return o?"value"!==a.type.kind&&e.concat(1).checkSubtype(r,a.type)?null:new ve(r,n,a,i,s,o):null}evaluate(t){const e=this.input.evaluate(t);return (ne(e)===this.inputType&&this.outputs[this.cases[e]]||this.otherwise).evaluate(t)}eachChild(t){t(this.input),this.outputs.forEach(t),t(this.otherwise);}outputDefined(){return this.outputs.every((t=>t.outputDefined()))&&this.otherwise.outputDefined()}}class be{constructor(t,e,r){this.type=t,this.branches=e,this.otherwise=r;}static parse(t,e){if(t.length<4)return e.error(`Expected at least 3 arguments, but found only ${t.length-1}.`);if(t.length%2!=0)return e.error("Expected an odd number of arguments.");let r;e.expectedType&&"value"!==e.expectedType.kind&&(r=e.expectedType);const n=[];for(let i=1;ie.outputDefined()))&&this.otherwise.outputDefined()}}class we{constructor(t,e,r,n){this.type=t,this.input=e,this.beginIndex=r,this.endIndex=n;}static parse(t,e){if(t.length<=2||t.length>=5)return e.error(`Expected 3 or 4 arguments, but found ${t.length-1} instead.`);const r=e.parse(t[1],1,dt),n=e.parse(t[2],2,ut);if(!r||!n)return null;if(!St(r.type,[bt(dt),ct,dt]))return e.error(`Expected first argument to be of type array or string, but found ${wt(r.type)} instead`);if(4===t.length){const i=e.parse(t[3],3,ut);return i?new we(r.type,r,n,i):null}return new we(r.type,r,n)}evaluate(t){const e=this.input.evaluate(t),r=this.beginIndex.evaluate(t);let n;if(this.endIndex&&(n=this.endIndex.evaluate(t)),kt(e,["string"]))return [...e].slice(r,n).join("");if(kt(e,["array"]))return e.slice(r,n);throw new ae(`Expected first argument to be of type array or string, but found ${wt(ne(e))} instead.`)}eachChild(t){t(this.input),t(this.beginIndex),this.endIndex&&t(this.endIndex);}outputDefined(){return !1}}function _e(t,e){const r=t.length-1;let n,i,s=0,a=r,o=0;for(;s<=a;)if(o=Math.floor((s+a)/2),n=t[o],i=t[o+1],n<=e){if(o===r||ee))throw new ae("Input is not a number.");a=o-1;}return 0}class Ae{constructor(t,e,r){this.type=t,this.input=e,this.labels=[],this.outputs=[];for(const[t,e]of r)this.labels.push(t),this.outputs.push(e);}static parse(t,e){if(t.length-1<4)return e.error(`Expected at least 4 arguments, but found only ${t.length-1}.`);if((t.length-1)%2!=0)return e.error("Expected an even number of arguments.");const r=e.parse(t[1],1,ut);if(!r)return null;const n=[];let i=null;e.expectedType&&"value"!==e.expectedType.kind&&(i=e.expectedType);for(let r=1;r=s)return e.error('Input/output pairs for "step" expressions must be arranged with input values in strictly ascending order.',o);const u=e.parse(a,l,i);if(!u)return null;i=i||u.type,n.push([s,u]);}return new Ae(i,r,n)}evaluate(t){const e=this.labels,r=this.outputs;if(1===e.length)return r[0].evaluate(t);const n=this.input.evaluate(t);if(n<=e[0])return r[0].evaluate(t);const i=e.length;return n>=e[i-1]?r[i-1].evaluate(t):r[_e(e,n)].evaluate(t)}eachChild(t){t(this.input);for(const e of this.outputs)t(e);}outputDefined(){return this.outputs.every((t=>t.outputDefined()))}}function Se(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var ke=Me;function Me(t,e,r,n){this.cx=3*t,this.bx=3*(r-t)-this.cx,this.ax=1-this.cx-this.bx,this.cy=3*e,this.by=3*(n-e)-this.cy,this.ay=1-this.cy-this.by,this.p1x=t,this.p1y=e,this.p2x=r,this.p2y=n;}Me.prototype={sampleCurveX:function(t){return ((this.ax*t+this.bx)*t+this.cx)*t},sampleCurveY:function(t){return ((this.ay*t+this.by)*t+this.cy)*t},sampleCurveDerivativeX:function(t){return (3*this.ax*t+2*this.bx)*t+this.cx},solveCurveX:function(t,e){if(void 0===e&&(e=1e-6),t<0)return 0;if(t>1)return 1;for(var r=t,n=0;n<8;n++){var i=this.sampleCurveX(r)-t;if(Math.abs(i)i?a=r:o=r,r=.5*(o-a)+a;return r},solve:function(t,e){return this.sampleCurveY(this.solveCurveX(t,e))}};var Ie=Se(ke);function ze(t,e,r){return t+r*(e-t)}function Pe(t,e,r){return t.map(((t,n)=>ze(t,e[n],r)))}const Ce={number:ze,color:function(t,e,r,n="rgb"){switch(n){case"rgb":{const[n,i,s,a]=Pe(t.rgb,e.rgb,r);return new Kt(n,i,s,a,!1)}case"hcl":{const[n,i,s,a]=t.hcl,[o,l,u,c]=e.hcl;let h,p;if(isNaN(n)||isNaN(o))isNaN(n)?isNaN(o)?h=NaN:(h=o,1!==s&&0!==s||(p=l)):(h=n,1!==u&&0!==u||(p=i));else {let t=o-n;o>n&&t>180?t-=360:o180&&(t+=360),h=n+r*t;}const[f,d,y,m]=function([t,e,r,n]){return t=isNaN(t)?0:t*Et,Ot([r,Math.cos(t)*e,Math.sin(t)*e,n])}([h,null!=p?p:ze(i,l,r),ze(s,u,r),ze(a,c,r)]);return new Kt(f,d,y,m,!1)}case"lab":{const[n,i,s,a]=Ot(Pe(t.lab,e.lab,r));return new Kt(n,i,s,a,!1)}}},array:Pe,padding:function(t,e,r){return new Jt(Pe(t.values,e.values,r))},variableAnchorOffsetCollection:function(t,e,r){const n=t.values,i=e.values;if(n.length!==i.length)throw new ae(`Cannot interpolate values of different length. from: ${t.toString()}, to: ${e.toString()}`);const s=[];for(let t=0;t"number"!=typeof t||t<0||t>1)))return e.error("Cubic bezier interpolation requires four numeric arguments with values between 0 and 1.",1);n={name:"cubic-bezier",controlPoints:t};}}if(t.length-1<4)return e.error(`Expected at least 4 arguments, but found only ${t.length-1}.`);if((t.length-1)%2!=0)return e.error("Expected an even number of arguments.");if(i=e.parse(i,2,ut),!i)return null;const a=[];let o=null;"interpolate-hcl"===r||"interpolate-lab"===r?o=pt:e.expectedType&&"value"!==e.expectedType.kind&&(o=e.expectedType);for(let t=0;t=r)return e.error('Input/output pairs for "interpolate" expressions must be arranged with input values in strictly ascending order.',i);const u=e.parse(n,l,o);if(!u)return null;o=o||u.type,a.push([r,u]);}return Mt(o,ut)||Mt(o,pt)||Mt(o,gt)||Mt(o,vt)||Mt(o,bt(ut))?new Be(o,r,n,i,a):e.error(`Type ${wt(o)} is not interpolatable.`)}evaluate(t){const e=this.labels,r=this.outputs;if(1===e.length)return r[0].evaluate(t);const n=this.input.evaluate(t);if(n<=e[0])return r[0].evaluate(t);const i=e.length;if(n>=e[i-1])return r[i-1].evaluate(t);const s=_e(e,n),a=Be.interpolationFactor(this.interpolation,n,e[s],e[s+1]),o=r[s].evaluate(t),l=r[s+1].evaluate(t);switch(this.operator){case"interpolate":return Ce[this.type.kind](o,l,a);case"interpolate-hcl":return Ce.color(o,l,a,"hcl");case"interpolate-lab":return Ce.color(o,l,a,"lab")}}eachChild(t){t(this.input);for(const e of this.outputs)t(e);}outputDefined(){return this.outputs.every((t=>t.outputDefined()))}}function Ve(t,e,r,n){const i=n-r,s=t-r;return 0===i?0:1===e?s/i:(Math.pow(e,s)-1)/(Math.pow(e,i)-1)}class Ee{constructor(t,e){this.type=t,this.args=e;}static parse(t,e){if(t.length<2)return e.error("Expectected at least one argument.");let r=null;const n=e.expectedType;n&&"value"!==n.kind&&(r=n);const i=[];for(const n of t.slice(1)){const t=e.parse(n,1+i.length,r,void 0,{typeAnnotation:"omit"});if(!t)return null;r=r||t.type,i.push(t);}if(!r)throw new Error("No output type");const s=n&&i.some((t=>At(n,t.type)));return new Ee(s?dt:r,i)}evaluate(t){let e,r=null,n=0;for(const i of this.args)if(n++,r=i.evaluate(t),r&&r instanceof te&&!r.available&&(e||(e=r.name),r=null,n===this.args.length&&(r=e)),null!==r)break;return r}eachChild(t){this.args.forEach(t);}outputDefined(){return this.args.every((t=>t.outputDefined()))}}function Fe(t,e){return "=="===t||"!="===t?"boolean"===e.kind||"string"===e.kind||"number"===e.kind||"null"===e.kind||"value"===e.kind:"string"===e.kind||"number"===e.kind||"value"===e.kind}function Te(t,e,r,n){return 0===n.compare(e,r)}function $e(t,e,r){const n="=="!==t&&"!="!==t;return class i{constructor(t,e,r){this.type=ht,this.lhs=t,this.rhs=e,this.collator=r,this.hasUntypedArgument="value"===t.type.kind||"value"===e.type.kind;}static parse(t,e){if(3!==t.length&&4!==t.length)return e.error("Expected two or three arguments.");const r=t[0];let s=e.parse(t[1],1,dt);if(!s)return null;if(!Fe(r,s.type))return e.concat(1).error(`"${r}" comparisons are not supported for type '${wt(s.type)}'.`);let a=e.parse(t[2],2,dt);if(!a)return null;if(!Fe(r,a.type))return e.concat(2).error(`"${r}" comparisons are not supported for type '${wt(a.type)}'.`);if(s.type.kind!==a.type.kind&&"value"!==s.type.kind&&"value"!==a.type.kind)return e.error(`Cannot compare types '${wt(s.type)}' and '${wt(a.type)}'.`);n&&("value"===s.type.kind&&"value"!==a.type.kind?s=new le(a.type,[s]):"value"!==s.type.kind&&"value"===a.type.kind&&(a=new le(s.type,[a])));let o=null;if(4===t.length){if("string"!==s.type.kind&&"string"!==a.type.kind&&"value"!==s.type.kind&&"value"!==a.type.kind)return e.error("Cannot use collator to compare non-string types.");if(o=e.parse(t[3],3,yt),!o)return null}return new i(s,a,o)}evaluate(i){const s=this.lhs.evaluate(i),a=this.rhs.evaluate(i);if(n&&this.hasUntypedArgument){const e=ne(s),r=ne(a);if(e.kind!==r.kind||"string"!==e.kind&&"number"!==e.kind)throw new ae(`Expected arguments for "${t}" to be (string, string) or (number, number), but found (${e.kind}, ${r.kind}) instead.`)}if(this.collator&&!n&&this.hasUntypedArgument){const t=ne(s),r=ne(a);if("string"!==t.kind||"string"!==r.kind)return e(i,s,a)}return this.collator?r(i,s,a,this.collator.evaluate(i)):e(i,s,a)}eachChild(t){t(this.lhs),t(this.rhs),this.collator&&t(this.collator);}outputDefined(){return !0}}}const Le=$e("==",(function(t,e,r){return e===r}),Te),De=$e("!=",(function(t,e,r){return e!==r}),(function(t,e,r,n){return !Te(0,e,r,n)})),Oe=$e("<",(function(t,e,r){return e",(function(t,e,r){return e>r}),(function(t,e,r,n){return n.compare(e,r)>0})),Re=$e("<=",(function(t,e,r){return e<=r}),(function(t,e,r,n){return n.compare(e,r)<=0})),Ue=$e(">=",(function(t,e,r){return e>=r}),(function(t,e,r,n){return n.compare(e,r)>=0}));class qe{constructor(t,e,r){this.type=yt,this.locale=r,this.caseSensitive=t,this.diacriticSensitive=e;}static parse(t,e){if(2!==t.length)return e.error("Expected one argument.");const r=t[1];if("object"!=typeof r||Array.isArray(r))return e.error("Collator options argument must be an object.");const n=e.parse(void 0!==r["case-sensitive"]&&r["case-sensitive"],1,ht);if(!n)return null;const i=e.parse(void 0!==r["diacritic-sensitive"]&&r["diacritic-sensitive"],1,ht);if(!i)return null;let s=null;return r.locale&&(s=e.parse(r.locale,1,ct),!s)?null:new qe(n,i,s)}evaluate(t){return new Xt(this.caseSensitive.evaluate(t),this.diacriticSensitive.evaluate(t),this.locale?this.locale.evaluate(t):null)}eachChild(t){t(this.caseSensitive),t(this.diacriticSensitive),this.locale&&t(this.locale);}outputDefined(){return !1}}class Ne{constructor(t,e,r,n,i){this.type=ct,this.number=t,this.locale=e,this.currency=r,this.minFractionDigits=n,this.maxFractionDigits=i;}static parse(t,e){if(3!==t.length)return e.error("Expected two arguments.");const r=e.parse(t[1],1,ut);if(!r)return null;const n=t[2];if("object"!=typeof n||Array.isArray(n))return e.error("NumberFormat options argument must be an object.");let i=null;if(n.locale&&(i=e.parse(n.locale,1,ct),!i))return null;let s=null;if(n.currency&&(s=e.parse(n.currency,1,ct),!s))return null;let a=null;if(n["min-fraction-digits"]&&(a=e.parse(n["min-fraction-digits"],1,ut),!a))return null;let o=null;return n["max-fraction-digits"]&&(o=e.parse(n["max-fraction-digits"],1,ut),!o)?null:new Ne(r,i,s,a,o)}evaluate(t){return new Intl.NumberFormat(this.locale?this.locale.evaluate(t):[],{style:this.currency?"currency":"decimal",currency:this.currency?this.currency.evaluate(t):void 0,minimumFractionDigits:this.minFractionDigits?this.minFractionDigits.evaluate(t):void 0,maximumFractionDigits:this.maxFractionDigits?this.maxFractionDigits.evaluate(t):void 0}).format(this.number.evaluate(t))}eachChild(t){t(this.number),this.locale&&t(this.locale),this.currency&&t(this.currency),this.minFractionDigits&&t(this.minFractionDigits),this.maxFractionDigits&&t(this.maxFractionDigits);}outputDefined(){return !1}}class Ze{constructor(t){this.type=mt,this.sections=t;}static parse(t,e){if(t.length<2)return e.error("Expected at least one argument.");const r=t[1];if(!Array.isArray(r)&&"object"==typeof r)return e.error("First argument must be an image or text section.");const n=[];let i=!1;for(let r=1;r<=t.length-1;++r){const s=t[r];if(i&&"object"==typeof s&&!Array.isArray(s)){i=!1;let t=null;if(s["font-scale"]&&(t=e.parse(s["font-scale"],1,ut),!t))return null;let r=null;if(s["text-font"]&&(r=e.parse(s["text-font"],1,bt(ct)),!r))return null;let a=null;if(s["text-color"]&&(a=e.parse(s["text-color"],1,pt),!a))return null;const o=n[n.length-1];o.scale=t,o.font=r,o.textColor=a;}else {const s=e.parse(t[r],1,dt);if(!s)return null;const a=s.type.kind;if("string"!==a&&"value"!==a&&"null"!==a&&"resolvedImage"!==a)return e.error("Formatted text type must be 'string', 'value', 'image' or 'null'.");i=!0,n.push({content:s,scale:null,font:null,textColor:null});}}return new Ze(n)}evaluate(t){return new Yt(this.sections.map((e=>{const r=e.content.evaluate(t);return ne(r)===xt?new Ht("",r,null,null,null):new Ht(ie(r),null,e.scale?e.scale.evaluate(t):null,e.font?e.font.evaluate(t).join(","):null,e.textColor?e.textColor.evaluate(t):null)})))}eachChild(t){for(const e of this.sections)t(e.content),e.scale&&t(e.scale),e.font&&t(e.font),e.textColor&&t(e.textColor);}outputDefined(){return !1}}class Ge{constructor(t){this.type=xt,this.input=t;}static parse(t,e){if(2!==t.length)return e.error("Expected two arguments.");const r=e.parse(t[1],1,ct);return r?new Ge(r):e.error("No image name provided.")}evaluate(t){const e=this.input.evaluate(t),r=te.fromString(e);return r&&t.availableImages&&(r.available=t.availableImages.indexOf(e)>-1),r}eachChild(t){t(this.input);}outputDefined(){return !1}}class Ke{constructor(t){this.type=ut,this.input=t;}static parse(t,e){if(2!==t.length)return e.error(`Expected 1 argument, but found ${t.length-1} instead.`);const r=e.parse(t[1],1);return r?"array"!==r.type.kind&&"string"!==r.type.kind&&"value"!==r.type.kind?e.error(`Expected argument of type string or array, but found ${wt(r.type)} instead.`):new Ke(r):null}evaluate(t){const e=this.input.evaluate(t);if("string"==typeof e)return [...e].length;if(Array.isArray(e))return e.length;throw new ae(`Expected value to be of type string or array, but found ${wt(ne(e))} instead.`)}eachChild(t){t(this.input);}outputDefined(){return !1}}const Xe=8192;function He(t,e){const r=(180+t[0])/360,n=(180-180/Math.PI*Math.log(Math.tan(Math.PI/4+t[1]*Math.PI/360)))/360,i=Math.pow(2,e.z);return [Math.round(r*i*Xe),Math.round(n*i*Xe)]}function Ye(t,e){const r=Math.pow(2,e.z);return [(i=(t[0]/Xe+e.x)/r,360*i-180),(n=(t[1]/Xe+e.y)/r,360/Math.PI*Math.atan(Math.exp((180-360*n)*Math.PI/180))-90)];var n,i;}function Je(t,e){t[0]=Math.min(t[0],e[0]),t[1]=Math.min(t[1],e[1]),t[2]=Math.max(t[2],e[0]),t[3]=Math.max(t[3],e[1]);}function We(t,e){return !(t[0]<=e[0]||t[2]>=e[2]||t[1]<=e[1]||t[3]>=e[3])}function Qe(t,e,r){const n=t[0]-e[0],i=t[1]-e[1],s=t[0]-r[0],a=t[1]-r[1];return n*a-s*i==0&&n*s<=0&&i*a<=0}function tr(t,e,r,n){return 0!=(i=[n[0]-r[0],n[1]-r[1]])[0]*(s=[e[0]-t[0],e[1]-t[1]])[1]-i[1]*s[0]&&!(!ar(t,e,r,n)||!ar(r,n,t,e));var i,s;}function er(t,e,r){for(const n of r)for(let r=0;r(i=t)[1]!=(a=o[e+1])[1]>i[1]&&i[0]<(a[0]-s[0])*(i[1]-s[1])/(a[1]-s[1])+s[0]&&(n=!n);}var i,s,a;return n}function nr(t,e){for(const r of e)if(rr(t,r))return !0;return !1}function ir(t,e){for(const r of t)if(!rr(r,e))return !1;for(let r=0;r0&&o<0||a<0&&o>0}function or(t,e,r){const n=[];for(let i=0;ir[2]){const e=.5*n;let i=t[0]-r[0]>e?-n:r[0]-t[0]>e?n:0;0===i&&(i=t[0]-r[2]>e?-n:r[2]-t[0]>e?n:0),t[0]+=i;}Je(e,t);}function cr(t,e,r,n){const i=Math.pow(2,n.z)*Xe,s=[n.x*Xe,n.y*Xe],a=[];for(const n of t)for(const t of n){const n=[t.x+s[0],t.y+s[1]];ur(n,e,r,i),a.push(n);}return a}function hr(t,e,r,n){const i=Math.pow(2,n.z)*Xe,s=[n.x*Xe,n.y*Xe],a=[];for(const r of t){const t=[];for(const n of r){const r=[n.x+s[0],n.y+s[1]];Je(e,r),t.push(r);}a.push(t);}if(e[2]-e[0]<=i/2){(o=e)[0]=o[1]=1/0,o[2]=o[3]=-1/0;for(const t of a)for(const n of t)ur(n,e,r,i);}var o;return a}class pr{constructor(t,e){this.type=ht,this.geojson=t,this.geometries=e;}static parse(t,e){if(2!==t.length)return e.error(`'within' expression requires exactly one argument, but found ${t.length-1} instead.`);if(re(t[1])){const e=t[1];if("FeatureCollection"===e.type){const t=[];for(const r of e.features){const{type:e,coordinates:n}=r.geometry;"Polygon"===e&&t.push(n),"MultiPolygon"===e&&t.push(...n);}if(t.length)return new pr(e,{type:"MultiPolygon",coordinates:t})}else if("Feature"===e.type){const t=e.geometry.type;if("Polygon"===t||"MultiPolygon"===t)return new pr(e,e.geometry)}else if("Polygon"===e.type||"MultiPolygon"===e.type)return new pr(e,e)}return e.error("'within' expression requires valid geojson object that contains polygon geometry type.")}evaluate(t){if(null!=t.geometry()&&null!=t.canonicalID()){if("Point"===t.geometryType())return function(t,e){const r=[1/0,1/0,-1/0,-1/0],n=[1/0,1/0,-1/0,-1/0],i=t.canonicalID();if("Polygon"===e.type){const s=or(e.coordinates,n,i),a=cr(t.geometry(),r,n,i);if(!We(r,n))return !1;for(const t of a)if(!rr(t,s))return !1}if("MultiPolygon"===e.type){const s=lr(e.coordinates,n,i),a=cr(t.geometry(),r,n,i);if(!We(r,n))return !1;for(const t of a)if(!nr(t,s))return !1}return !0}(t,this.geometries);if("LineString"===t.geometryType())return function(t,e){const r=[1/0,1/0,-1/0,-1/0],n=[1/0,1/0,-1/0,-1/0],i=t.canonicalID();if("Polygon"===e.type){const s=or(e.coordinates,n,i),a=hr(t.geometry(),r,n,i);if(!We(r,n))return !1;for(const t of a)if(!ir(t,s))return !1}if("MultiPolygon"===e.type){const s=lr(e.coordinates,n,i),a=hr(t.geometry(),r,n,i);if(!We(r,n))return !1;for(const t of a)if(!sr(t,s))return !1}return !0}(t,this.geometries)}return !1}eachChild(){}outputDefined(){return !0}}let fr=class{constructor(t=[],e=((t,e)=>te?1:0)){if(this.data=t,this.length=this.data.length,this.compare=e,this.length>0)for(let t=(this.length>>1)-1;t>=0;t--)this._down(t);}push(t){this.data.push(t),this._up(this.length++);}pop(){if(0===this.length)return;const t=this.data[0],e=this.data.pop();return --this.length>0&&(this.data[0]=e,this._down(0)),t}peek(){return this.data[0]}_up(t){const{data:e,compare:r}=this,n=e[t];for(;t>0;){const i=t-1>>1,s=e[i];if(r(n,s)>=0)break;e[t]=s,t=i;}e[t]=n;}_down(t){const{data:e,compare:r}=this,n=this.length>>1,i=e[t];for(;t=0)break;e[t]=e[n],t=n;}e[t]=i;}};function dr(t,e,r,n,i){yr(t,e,r,n||t.length-1,i||gr);}function yr(t,e,r,n,i){for(;n>r;){if(n-r>600){var s=n-r+1,a=e-r+1,o=Math.log(s),l=.5*Math.exp(2*o/3),u=.5*Math.sqrt(o*l*(s-l)/s)*(a-s/2<0?-1:1);yr(t,e,Math.max(r,Math.floor(e-a*l/s+u)),Math.min(n,Math.floor(e+(s-a)*l/s+u)),i);}var c=t[e],h=r,p=n;for(mr(t,r,e),i(t[n],c)>0&&mr(t,r,n);h0;)p--;}0===i(t[r],c)?mr(t,r,p):mr(t,++p,n),p<=e&&(r=p+1),e<=p&&(n=p-1);}}function mr(t,e,r){var n=t[e];t[e]=t[r],t[r]=n;}function gr(t,e){return te?1:0}function xr(t,e){if(t.length<=1)return [t];const r=[];let n,i;for(const e of t){const t=br(e);0!==t&&(e.area=Math.abs(t),void 0===i&&(i=t<0),i===t<0?(n&&r.push(n),n=[e]):n.push(e));}if(n&&r.push(n),e>1)for(let t=0;t1?(l=t[o+1][0],u=t[o+1][1]):p>0&&(l+=c/this.kx*p,u+=h/this.ky*p)),c=this.wrap(e[0]-l)*this.kx,h=(e[1]-u)*this.ky;const f=c*c+h*h;f180;)t-=360;return t}}function kr(t,e){return e[0]-t[0]}function Mr(t){return t[1]-t[0]+1}function Ir(t,e){return t[1]>=t[0]&&t[1]t[1])return [null,null];const r=Mr(t);if(e){if(2===r)return [t,null];const e=Math.floor(r/2);return [[t[0],t[0]+e],[t[0]+e,t[1]]]}if(1===r)return [t,null];const n=Math.floor(r/2)-1;return [[t[0],t[0]+n],[t[0]+n+1,t[1]]]}function Pr(t,e){if(!Ir(e,t.length))return [1/0,1/0,-1/0,-1/0];const r=[1/0,1/0,-1/0,-1/0];for(let n=e[0];n<=e[1];++n)Je(r,t[n]);return r}function Cr(t){const e=[1/0,1/0,-1/0,-1/0];for(const r of t)for(const t of r)Je(e,t);return e}function Br(t){return t[0]!==-1/0&&t[1]!==-1/0&&t[2]!==1/0&&t[3]!==1/0}function Vr(t,e,r){if(!Br(t)||!Br(e))return NaN;let n=0,i=0;return t[2]e[2]&&(n=t[0]-e[2]),t[1]>e[3]&&(i=t[1]-e[3]),t[3]=n)return n;if(We(i,s)){if(Or(t,e))return 0}else if(Or(e,t))return 0;let a=1/0;for(const n of t)for(let t=0,i=n.length,s=i-1;t0;){const i=a.pop();if(i[0]>=s)continue;const l=i[1],u=e?50:100;if(Mr(l)<=u){if(!Ir(l,t.length))return NaN;if(e){const e=Dr(t,l,r,n);if(isNaN(e)||0===e)return e;s=Math.min(s,e);}else for(let e=l[0];e<=l[1];++e){const i=Lr(t[e],r,n);if(s=Math.min(s,i),0===s)return 0}}else {const r=zr(l,e);Rr(a,s,n,t,o,r[0]),Rr(a,s,n,t,o,r[1]);}}return s}function Nr(t,e,r,n,i,s=1/0){let a=Math.min(s,i.distance(t[0],r[0]));if(0===a)return a;const o=new fr([[0,[0,t.length-1],[0,r.length-1]]],kr);for(;o.length>0;){const s=o.pop();if(s[0]>=a)continue;const l=s[1],u=s[2],c=e?50:100,h=n?50:100;if(Mr(l)<=c&&Mr(u)<=h){if(!Ir(l,t.length)&&Ir(u,r.length))return NaN;let s;if(e&&n)s=Tr(t,l,r,u,i),a=Math.min(a,s);else if(e&&!n){const e=t.slice(l[0],l[1]+1);for(let t=u[0];t<=u[1];++t)if(s=Er(r[t],e,i),a=Math.min(a,s),0===a)return a}else if(!e&&n){const e=r.slice(u[0],u[1]+1);for(let r=l[0];r<=l[1];++r)if(s=Er(t[r],e,i),a=Math.min(a,s),0===a)return a}else s=$r(t,l,r,u,i),a=Math.min(a,s);}else {const s=zr(l,e),c=zr(u,n);Ur(o,a,i,t,r,s[0],c[0]),Ur(o,a,i,t,r,s[0],c[1]),Ur(o,a,i,t,r,s[1],c[0]),Ur(o,a,i,t,r,s[1],c[1]);}}return a}function Zr(t){return "MultiPolygon"===t.type?t.coordinates.map((t=>({type:"Polygon",coordinates:t}))):"MultiLineString"===t.type?t.coordinates.map((t=>({type:"LineString",coordinates:t}))):"MultiPoint"===t.type?t.coordinates.map((t=>({type:"Point",coordinates:t}))):[t]}class Gr{constructor(t,e){this.type=ut,this.geojson=t,this.geometries=e;}static parse(t,e){if(2!==t.length)return e.error(`'distance' expression requires exactly one argument, but found ${t.length-1} instead.`);if(re(t[1])){const e=t[1];if("FeatureCollection"===e.type)return new Gr(e,e.features.map((t=>Zr(t.geometry))).flat());if("Feature"===e.type)return new Gr(e,Zr(e.geometry));if("type"in e&&"coordinates"in e)return new Gr(e,Zr(e))}return e.error("'distance' expression requires valid geojson object that contains polygon geometry type.")}evaluate(t){if(null!=t.geometry()&&null!=t.canonicalID()){if("Point"===t.geometryType())return function(t,e){const r=t.geometry(),n=r.flat().map((e=>Ye([e.x,e.y],t.canonical)));if(0===r.length)return NaN;const i=new Sr(n[0][1]);let s=1/0;for(const t of e){switch(t.type){case"Point":s=Math.min(s,Nr(n,!1,[t.coordinates],!1,i,s));break;case"LineString":s=Math.min(s,Nr(n,!1,t.coordinates,!0,i,s));break;case"Polygon":s=Math.min(s,qr(n,!1,t.coordinates,i,s));}if(0===s)return s}return s}(t,this.geometries);if("LineString"===t.geometryType())return function(t,e){const r=t.geometry(),n=r.flat().map((e=>Ye([e.x,e.y],t.canonical)));if(0===r.length)return NaN;const i=new Sr(n[0][1]);let s=1/0;for(const t of e){switch(t.type){case"Point":s=Math.min(s,Nr(n,!0,[t.coordinates],!1,i,s));break;case"LineString":s=Math.min(s,Nr(n,!0,t.coordinates,!0,i,s));break;case"Polygon":s=Math.min(s,qr(n,!0,t.coordinates,i,s));}if(0===s)return s}return s}(t,this.geometries);if("Polygon"===t.geometryType())return function(t,e){const r=t.geometry();if(0===r.length||0===r[0].length)return NaN;const n=xr(r,0).map((e=>e.map((e=>e.map((e=>Ye([e.x,e.y],t.canonical))))))),i=new Sr(n[0][0][0][1]);let s=1/0;for(const t of e)for(const e of n){switch(t.type){case"Point":s=Math.min(s,qr([t.coordinates],!1,e,i,s));break;case"LineString":s=Math.min(s,qr(t.coordinates,!0,e,i,s));break;case"Polygon":s=Math.min(s,jr(e,t.coordinates,i,s));}if(0===s)return s}return s}(t,this.geometries)}return NaN}eachChild(){}outputDefined(){return !0}}const Kr={"==":Le,"!=":De,">":je,"<":Oe,">=":Ue,"<=":Re,array:le,at:me,boolean:le,case:be,coalesce:Ee,collator:qe,format:Ze,image:Ge,in:ge,"index-of":xe,interpolate:Be,"interpolate-hcl":Be,"interpolate-lab":Be,length:Ke,let:de,literal:se,match:ve,number:le,"number-format":Ne,object:le,slice:we,step:Ae,string:le,"to-boolean":ce,"to-color":ce,"to-number":ce,"to-string":ce,var:ye,within:pr,distance:Gr};class Xr{constructor(t,e,r,n){this.name=t,this.type=e,this._evaluate=r,this.args=n;}evaluate(t){return this._evaluate(t,this.args)}eachChild(t){this.args.forEach(t);}outputDefined(){return !1}static parse(t,e){const r=t[0],n=Xr.definitions[r];if(!n)return e.error(`Unknown expression "${r}". If you wanted a literal array, use ["literal", [...]].`,0);const i=Array.isArray(n)?n[0]:n.type,s=Array.isArray(n)?[[n[1],n[2]]]:n.overloads,a=s.filter((([e])=>!Array.isArray(e)||e.length===t.length-1));let o=null;for(const[n,s]of a){o=new fe(e.registry,Qr,e.path,null,e.scope);const a=[];let l=!1;for(let e=1;e{return e=t,Array.isArray(e)?`(${e.map(wt).join(", ")})`:`(${wt(e.type)}...)`;var e;})).join(" | "),n=[];for(let r=1;r{r=e?r&&Qr(t):r&&t instanceof se;})),!!r&&tn(t)&&rn(t,["zoom","heatmap-density","line-progress","accumulated","is-supported-script"])}function tn(t){if(t instanceof Xr){if("get"===t.name&&1===t.args.length)return !1;if("feature-state"===t.name)return !1;if("has"===t.name&&1===t.args.length)return !1;if("properties"===t.name||"geometry-type"===t.name||"id"===t.name)return !1;if(/^filter-/.test(t.name))return !1}if(t instanceof pr)return !1;if(t instanceof Gr)return !1;let e=!0;return t.eachChild((t=>{e&&!tn(t)&&(e=!1);})),e}function en(t){if(t instanceof Xr&&"feature-state"===t.name)return !1;let e=!0;return t.eachChild((t=>{e&&!en(t)&&(e=!1);})),e}function rn(t,e){if(t instanceof Xr&&e.indexOf(t.name)>=0)return !1;let r=!0;return t.eachChild((t=>{r&&!rn(t,e)&&(r=!1);})),r}function nn(t){return {result:"success",value:t}}function sn(t){return {result:"error",value:t}}function an(t){return "data-driven"===t["property-type"]||"cross-faded-data-driven"===t["property-type"]}function on(t){return !!t.expression&&t.expression.parameters.indexOf("zoom")>-1}function ln(t){return !!t.expression&&t.expression.interpolated}function un(t){return t instanceof Number?"number":t instanceof String?"string":t instanceof Boolean?"boolean":Array.isArray(t)?"array":null===t?"null":typeof t}function cn(t){return "object"==typeof t&&null!==t&&!Array.isArray(t)}function hn(t){return t}function pn(t,e){const r="color"===e.type,n=t.stops&&"object"==typeof t.stops[0][0],i=n||!(n||void 0!==t.property),s=t.type||(ln(e)?"exponential":"interval");if(r||"padding"===e.type){const n=r?Kt.parse:Jt.parse;(t=st({},t)).stops&&(t.stops=t.stops.map((t=>[t[0],n(t[1])]))),t.default=n(t.default?t.default:e.default);}if(t.colorSpace&&"rgb"!==(a=t.colorSpace)&&"hcl"!==a&&"lab"!==a)throw new Error(`Unknown color space: "${t.colorSpace}"`);var a;let o,l,u;if("exponential"===s)o=mn;else if("interval"===s)o=yn;else if("categorical"===s){o=dn,l=Object.create(null);for(const e of t.stops)l[e[0]]=e[1];u=typeof t.stops[0][0];}else {if("identity"!==s)throw new Error(`Unknown function type "${s}"`);o=gn;}if(n){const r={},n=[];for(let e=0;et[0])),evaluate:({zoom:r},n)=>mn({stops:i,base:t.base},e,r).evaluate(r,n)}}if(i){const r="exponential"===s?{name:"exponential",base:void 0!==t.base?t.base:1}:null;return {kind:"camera",interpolationType:r,interpolationFactor:Be.interpolationFactor.bind(void 0,r),zoomStops:t.stops.map((t=>t[0])),evaluate:({zoom:r})=>o(t,e,r,l,u)}}return {kind:"source",evaluate(r,n){const i=n&&n.properties?n.properties[t.property]:void 0;return void 0===i?fn(t.default,e.default):o(t,e,i,l,u)}}}function fn(t,e,r){return void 0!==t?t:void 0!==e?e:void 0!==r?r:void 0}function dn(t,e,r,n,i){return fn(typeof r===i?n[r]:void 0,t.default,e.default)}function yn(t,e,r){if("number"!==un(r))return fn(t.default,e.default);const n=t.stops.length;if(1===n)return t.stops[0][1];if(r<=t.stops[0][0])return t.stops[0][1];if(r>=t.stops[n-1][0])return t.stops[n-1][1];const i=_e(t.stops.map((t=>t[0])),r);return t.stops[i][1]}function mn(t,e,r){const n=void 0!==t.base?t.base:1;if("number"!==un(r))return fn(t.default,e.default);const i=t.stops.length;if(1===i)return t.stops[0][1];if(r<=t.stops[0][0])return t.stops[0][1];if(r>=t.stops[i-1][0])return t.stops[i-1][1];const s=_e(t.stops.map((t=>t[0])),r),a=function(t,e,r,n){const i=n-r,s=t-r;return 0===i?0:1===e?s/i:(Math.pow(e,s)-1)/(Math.pow(e,i)-1)}(r,n,t.stops[s][0],t.stops[s+1][0]),o=t.stops[s][1],l=t.stops[s+1][1],u=Ce[e.type]||hn;return "function"==typeof o.evaluate?{evaluate(...e){const r=o.evaluate.apply(void 0,e),n=l.evaluate.apply(void 0,e);if(void 0!==r&&void 0!==n)return u(r,n,a,t.colorSpace)}}:u(o,l,a,t.colorSpace)}function gn(t,e,r){switch(e.type){case"color":r=Kt.parse(r);break;case"formatted":r=Yt.fromString(r.toString());break;case"resolvedImage":r=te.fromString(r.toString());break;case"padding":r=Jt.parse(r);break;default:un(r)===e.type||"enum"===e.type&&e.values[r]||(r=void 0);}return fn(r,t.default,e.default)}Xr.register(Kr,{error:[{kind:"error"},[ct],(t,[e])=>{throw new ae(e.evaluate(t))}],typeof:[ct,[dt],(t,[e])=>wt(ne(e.evaluate(t)))],"to-rgba":[bt(ut,4),[pt],(t,[e])=>{const[r,n,i,s]=e.evaluate(t).rgb;return [255*r,255*n,255*i,s]}],rgb:[pt,[ut,ut,ut],Hr],rgba:[pt,[ut,ut,ut,ut],Hr],has:{type:ht,overloads:[[[ct],(t,[e])=>Yr(e.evaluate(t),t.properties())],[[ct,ft],(t,[e,r])=>Yr(e.evaluate(t),r.evaluate(t))]]},get:{type:dt,overloads:[[[ct],(t,[e])=>Jr(e.evaluate(t),t.properties())],[[ct,ft],(t,[e,r])=>Jr(e.evaluate(t),r.evaluate(t))]]},"feature-state":[dt,[ct],(t,[e])=>Jr(e.evaluate(t),t.featureState||{})],properties:[ft,[],t=>t.properties()],"geometry-type":[ct,[],t=>t.geometryType()],id:[dt,[],t=>t.id()],zoom:[ut,[],t=>t.globals.zoom],"heatmap-density":[ut,[],t=>t.globals.heatmapDensity||0],"line-progress":[ut,[],t=>t.globals.lineProgress||0],accumulated:[dt,[],t=>void 0===t.globals.accumulated?null:t.globals.accumulated],"+":[ut,Wr(ut),(t,e)=>{let r=0;for(const n of e)r+=n.evaluate(t);return r}],"*":[ut,Wr(ut),(t,e)=>{let r=1;for(const n of e)r*=n.evaluate(t);return r}],"-":{type:ut,overloads:[[[ut,ut],(t,[e,r])=>e.evaluate(t)-r.evaluate(t)],[[ut],(t,[e])=>-e.evaluate(t)]]},"/":[ut,[ut,ut],(t,[e,r])=>e.evaluate(t)/r.evaluate(t)],"%":[ut,[ut,ut],(t,[e,r])=>e.evaluate(t)%r.evaluate(t)],ln2:[ut,[],()=>Math.LN2],pi:[ut,[],()=>Math.PI],e:[ut,[],()=>Math.E],"^":[ut,[ut,ut],(t,[e,r])=>Math.pow(e.evaluate(t),r.evaluate(t))],sqrt:[ut,[ut],(t,[e])=>Math.sqrt(e.evaluate(t))],log10:[ut,[ut],(t,[e])=>Math.log(e.evaluate(t))/Math.LN10],ln:[ut,[ut],(t,[e])=>Math.log(e.evaluate(t))],log2:[ut,[ut],(t,[e])=>Math.log(e.evaluate(t))/Math.LN2],sin:[ut,[ut],(t,[e])=>Math.sin(e.evaluate(t))],cos:[ut,[ut],(t,[e])=>Math.cos(e.evaluate(t))],tan:[ut,[ut],(t,[e])=>Math.tan(e.evaluate(t))],asin:[ut,[ut],(t,[e])=>Math.asin(e.evaluate(t))],acos:[ut,[ut],(t,[e])=>Math.acos(e.evaluate(t))],atan:[ut,[ut],(t,[e])=>Math.atan(e.evaluate(t))],min:[ut,Wr(ut),(t,e)=>Math.min(...e.map((e=>e.evaluate(t))))],max:[ut,Wr(ut),(t,e)=>Math.max(...e.map((e=>e.evaluate(t))))],abs:[ut,[ut],(t,[e])=>Math.abs(e.evaluate(t))],round:[ut,[ut],(t,[e])=>{const r=e.evaluate(t);return r<0?-Math.round(-r):Math.round(r)}],floor:[ut,[ut],(t,[e])=>Math.floor(e.evaluate(t))],ceil:[ut,[ut],(t,[e])=>Math.ceil(e.evaluate(t))],"filter-==":[ht,[ct,dt],(t,[e,r])=>t.properties()[e.value]===r.value],"filter-id-==":[ht,[dt],(t,[e])=>t.id()===e.value],"filter-type-==":[ht,[ct],(t,[e])=>t.geometryType()===e.value],"filter-<":[ht,[ct,dt],(t,[e,r])=>{const n=t.properties()[e.value],i=r.value;return typeof n==typeof i&&n{const r=t.id(),n=e.value;return typeof r==typeof n&&r":[ht,[ct,dt],(t,[e,r])=>{const n=t.properties()[e.value],i=r.value;return typeof n==typeof i&&n>i}],"filter-id->":[ht,[dt],(t,[e])=>{const r=t.id(),n=e.value;return typeof r==typeof n&&r>n}],"filter-<=":[ht,[ct,dt],(t,[e,r])=>{const n=t.properties()[e.value],i=r.value;return typeof n==typeof i&&n<=i}],"filter-id-<=":[ht,[dt],(t,[e])=>{const r=t.id(),n=e.value;return typeof r==typeof n&&r<=n}],"filter->=":[ht,[ct,dt],(t,[e,r])=>{const n=t.properties()[e.value],i=r.value;return typeof n==typeof i&&n>=i}],"filter-id->=":[ht,[dt],(t,[e])=>{const r=t.id(),n=e.value;return typeof r==typeof n&&r>=n}],"filter-has":[ht,[dt],(t,[e])=>e.value in t.properties()],"filter-has-id":[ht,[],t=>null!==t.id()&&void 0!==t.id()],"filter-type-in":[ht,[bt(ct)],(t,[e])=>e.value.indexOf(t.geometryType())>=0],"filter-id-in":[ht,[bt(dt)],(t,[e])=>e.value.indexOf(t.id())>=0],"filter-in-small":[ht,[ct,bt(dt)],(t,[e,r])=>r.value.indexOf(t.properties()[e.value])>=0],"filter-in-large":[ht,[ct,bt(dt)],(t,[e,r])=>function(t,e,r,n){for(;r<=n;){const i=r+n>>1;if(e[i]===t)return !0;e[i]>t?n=i-1:r=i+1;}return !1}(t.properties()[e.value],r.value,0,r.value.length-1)],all:{type:ht,overloads:[[[ht,ht],(t,[e,r])=>e.evaluate(t)&&r.evaluate(t)],[Wr(ht),(t,e)=>{for(const r of e)if(!r.evaluate(t))return !1;return !0}]]},any:{type:ht,overloads:[[[ht,ht],(t,[e,r])=>e.evaluate(t)||r.evaluate(t)],[Wr(ht),(t,e)=>{for(const r of e)if(r.evaluate(t))return !0;return !1}]]},"!":[ht,[ht],(t,[e])=>!e.evaluate(t)],"is-supported-script":[ht,[ct],(t,[e])=>{const r=t.globals&&t.globals.isSupportedScript;return !r||r(e.evaluate(t))}],upcase:[ct,[ct],(t,[e])=>e.evaluate(t).toUpperCase()],downcase:[ct,[ct],(t,[e])=>e.evaluate(t).toLowerCase()],concat:[ct,Wr(dt),(t,e)=>e.map((e=>ie(e.evaluate(t)))).join("")],"resolved-locale":[ct,[yt],(t,[e])=>e.evaluate(t).resolvedLocale()]});class xn{constructor(t,e){var r;this.expression=t,this._warningHistory={},this._evaluator=new pe,this._defaultValue=e?"color"===(r=e).type&&cn(r.default)?new Kt(0,0,0,0):"color"===r.type?Kt.parse(r.default)||null:"padding"===r.type?Jt.parse(r.default)||null:"variableAnchorOffsetCollection"===r.type?Qt.parse(r.default)||null:void 0===r.default?null:r.default:null,this._enumValues=e&&"enum"===e.type?e.values:null;}evaluateWithoutErrorHandling(t,e,r,n,i,s){return this._evaluator.globals=t,this._evaluator.feature=e,this._evaluator.featureState=r,this._evaluator.canonical=n,this._evaluator.availableImages=i||null,this._evaluator.formattedSection=s,this.expression.evaluate(this._evaluator)}evaluate(t,e,r,n,i,s){this._evaluator.globals=t,this._evaluator.feature=e||null,this._evaluator.featureState=r||null,this._evaluator.canonical=n,this._evaluator.availableImages=i||null,this._evaluator.formattedSection=s||null;try{const t=this.expression.evaluate(this._evaluator);if(null==t||"number"==typeof t&&t!=t)return this._defaultValue;if(this._enumValues&&!(t in this._enumValues))throw new ae(`Expected value to be one of ${Object.keys(this._enumValues).map((t=>JSON.stringify(t))).join(", ")}, but found ${JSON.stringify(t)} instead.`);return t}catch(t){return this._warningHistory[t.message]||(this._warningHistory[t.message]=!0,"undefined"!=typeof console&&console.warn(t.message)),this._defaultValue}}}function vn(t){return Array.isArray(t)&&t.length>0&&"string"==typeof t[0]&&t[0]in Kr}function bn(t,e){const r=new fe(Kr,Qr,[],e?function(t){const e={color:pt,string:ct,number:ut,enum:ct,boolean:ht,formatted:mt,padding:gt,resolvedImage:xt,variableAnchorOffsetCollection:vt};return "array"===t.type?bt(e[t.value]||dt,t.length):e[t.type]}(e):void 0),n=r.parse(t,void 0,void 0,void 0,e&&"string"===e.type?{typeAnnotation:"coerce"}:void 0);return n?nn(new xn(n,e)):sn(r.errors)}class wn{constructor(t,e){this.kind=t,this._styleExpression=e,this.isStateDependent="constant"!==t&&!en(e.expression);}evaluateWithoutErrorHandling(t,e,r,n,i,s){return this._styleExpression.evaluateWithoutErrorHandling(t,e,r,n,i,s)}evaluate(t,e,r,n,i,s){return this._styleExpression.evaluate(t,e,r,n,i,s)}}class _n{constructor(t,e,r,n){this.kind=t,this.zoomStops=r,this._styleExpression=e,this.isStateDependent="camera"!==t&&!en(e.expression),this.interpolationType=n;}evaluateWithoutErrorHandling(t,e,r,n,i,s){return this._styleExpression.evaluateWithoutErrorHandling(t,e,r,n,i,s)}evaluate(t,e,r,n,i,s){return this._styleExpression.evaluate(t,e,r,n,i,s)}interpolationFactor(t,e,r){return this.interpolationType?Be.interpolationFactor(this.interpolationType,t,e,r):0}}function An(t,e){const r=bn(t,e);if("error"===r.result)return r;const n=r.value.expression,i=tn(n);if(!i&&!an(e))return sn([new at("","data expressions not supported")]);const s=rn(n,["zoom"]);if(!s&&!on(e))return sn([new at("","zoom expressions not supported")]);const a=kn(n);return a||s?a instanceof at?sn([a]):a instanceof Be&&!ln(e)?sn([new at("",'"interpolate" expressions cannot be used with this property')]):nn(a?new _n(i?"camera":"composite",r.value,a.labels,a instanceof Be?a.interpolation:void 0):new wn(i?"constant":"source",r.value)):sn([new at("",'"zoom" expression may only be used as input to a top-level "step" or "interpolate" expression.')])}class Sn{constructor(t,e){this._parameters=t,this._specification=e,st(this,pn(this._parameters,this._specification));}static deserialize(t){return new Sn(t._parameters,t._specification)}static serialize(t){return {_parameters:t._parameters,_specification:t._specification}}}function kn(t){let e=null;if(t instanceof de)e=kn(t.result);else if(t instanceof Ee){for(const r of t.args)if(e=kn(r),e)break}else (t instanceof Ae||t instanceof Be)&&t.input instanceof Xr&&"zoom"===t.input.name&&(e=t);return e instanceof at||t.eachChild((t=>{const r=kn(t);r instanceof at?e=r:!e&&r?e=new at("",'"zoom" expression may only be used as input to a top-level "step" or "interpolate" expression.'):e&&r&&e!==r&&(e=new at("",'Only one zoom-based "step" or "interpolate" subexpression may be used in an expression.'));})),e}function Mn(t){if(!0===t||!1===t)return !0;if(!Array.isArray(t)||0===t.length)return !1;switch(t[0]){case"has":return t.length>=2&&"$id"!==t[1]&&"$type"!==t[1];case"in":return t.length>=3&&("string"!=typeof t[1]||Array.isArray(t[2]));case"!in":case"!has":case"none":return !1;case"==":case"!=":case">":case">=":case"<":case"<=":return 3!==t.length||Array.isArray(t[1])||Array.isArray(t[2]);case"any":case"all":for(const e of t.slice(1))if(!Mn(e)&&"boolean"!=typeof e)return !1;return !0;default:return !0}}const In={type:"boolean",default:!1,transition:!1,"property-type":"data-driven",expression:{interpolated:!1,parameters:["zoom","feature"]}};function zn(t){if(null==t)return {filter:()=>!0,needGeometry:!1};Mn(t)||(t=Bn(t));const e=bn(t,In);if("error"===e.result)throw new Error(e.value.map((t=>`${t.key}: ${t.message}`)).join(", "));return {filter:(t,r,n)=>e.value.evaluate(t,r,{},n),needGeometry:Cn(t)}}function Pn(t,e){return te?1:0}function Cn(t){if(!Array.isArray(t))return !1;if("within"===t[0]||"distance"===t[0])return !0;for(let e=1;e"===e||"<="===e||">="===e?Vn(t[1],t[2],e):"any"===e?(r=t.slice(1),["any"].concat(r.map(Bn))):"all"===e?["all"].concat(t.slice(1).map(Bn)):"none"===e?["all"].concat(t.slice(1).map(Bn).map(Tn)):"in"===e?En(t[1],t.slice(2)):"!in"===e?Tn(En(t[1],t.slice(2))):"has"===e?Fn(t[1]):"!has"!==e||Tn(Fn(t[1]));var r;}function Vn(t,e,r){switch(t){case"$type":return [`filter-type-${r}`,e];case"$id":return [`filter-id-${r}`,e];default:return [`filter-${r}`,t,e]}}function En(t,e){if(0===e.length)return !1;switch(t){case"$type":return ["filter-type-in",["literal",e]];case"$id":return ["filter-id-in",["literal",e]];default:return e.length>200&&!e.some((t=>typeof t!=typeof e[0]))?["filter-in-large",t,["literal",e.sort(Pn)]]:["filter-in-small",t,["literal",e]]}}function Fn(t){switch(t){case"$type":return !0;case"$id":return ["filter-has-id"];default:return ["filter-has",t]}}function Tn(t){return ["!",t]}function $n(t){const e=typeof t;if("number"===e||"boolean"===e||"string"===e||null==t)return JSON.stringify(t);if(Array.isArray(t)){let e="[";for(const r of t)e+=`${$n(r)},`;return `${e}]`}const r=Object.keys(t).sort();let n="{";for(let e=0;en.maximum?[new it(e,r,`${r} is greater than the maximum value ${n.maximum}`)]:[]}function Nn(t){const e=t.valueSpec,r=On(t.value.type);let n,i,s,a={};const o="categorical"!==r&&void 0===t.value.property,l=!o,u="array"===un(t.value.stops)&&"array"===un(t.value.stops[0])&&"object"===un(t.value.stops[0][0]),c=Rn({key:t.key,value:t.value,valueSpec:t.styleSpec.function,validateSpec:t.validateSpec,style:t.style,styleSpec:t.styleSpec,objectElementValidators:{stops:function(t){if("identity"===r)return [new it(t.key,t.value,'identity function may not have a "stops" property')];let e=[];const n=t.value;return e=e.concat(Un({key:t.key,value:n,valueSpec:t.valueSpec,validateSpec:t.validateSpec,style:t.style,styleSpec:t.styleSpec,arrayElementValidator:h})),"array"===un(n)&&0===n.length&&e.push(new it(t.key,n,"array must have at least one stop")),e},default:function(t){return t.validateSpec({key:t.key,value:t.value,valueSpec:e,validateSpec:t.validateSpec,style:t.style,styleSpec:t.styleSpec})}}});return "identity"===r&&o&&c.push(new it(t.key,t.value,'missing required property "property"')),"identity"===r||t.value.stops||c.push(new it(t.key,t.value,'missing required property "stops"')),"exponential"===r&&t.valueSpec.expression&&!ln(t.valueSpec)&&c.push(new it(t.key,t.value,"exponential functions not supported")),t.styleSpec.$version>=8&&(l&&!an(t.valueSpec)?c.push(new it(t.key,t.value,"property functions not supported")):o&&!on(t.valueSpec)&&c.push(new it(t.key,t.value,"zoom functions not supported"))),"categorical"!==r&&!u||void 0!==t.value.property||c.push(new it(t.key,t.value,'"property" property is required')),c;function h(t){let r=[];const n=t.value,o=t.key;if("array"!==un(n))return [new it(o,n,`array expected, ${un(n)} found`)];if(2!==n.length)return [new it(o,n,`array length 2 expected, length ${n.length} found`)];if(u){if("object"!==un(n[0]))return [new it(o,n,`object expected, ${un(n[0])} found`)];if(void 0===n[0].zoom)return [new it(o,n,"object stop key must have zoom")];if(void 0===n[0].value)return [new it(o,n,"object stop key must have value")];if(s&&s>On(n[0].zoom))return [new it(o,n[0].zoom,"stop zoom values must appear in ascending order")];On(n[0].zoom)!==s&&(s=On(n[0].zoom),i=void 0,a={}),r=r.concat(Rn({key:`${o}[0]`,value:n[0],valueSpec:{zoom:{}},validateSpec:t.validateSpec,style:t.style,styleSpec:t.styleSpec,objectElementValidators:{zoom:qn,value:p}}));}else r=r.concat(p({key:`${o}[0]`,value:n[0],valueSpec:{},validateSpec:t.validateSpec,style:t.style,styleSpec:t.styleSpec},n));return vn(jn(n[1]))?r.concat([new it(`${o}[1]`,n[1],"expressions are not allowed in function stops.")]):r.concat(t.validateSpec({key:`${o}[1]`,value:n[1],valueSpec:e,validateSpec:t.validateSpec,style:t.style,styleSpec:t.styleSpec}))}function p(t,s){const o=un(t.value),l=On(t.value),u=null!==t.value?t.value:s;if(n){if(o!==n)return [new it(t.key,u,`${o} stop domain type must match previous stop domain type ${n}`)]}else n=o;if("number"!==o&&"string"!==o&&"boolean"!==o)return [new it(t.key,u,"stop domain value must be a number, string, or boolean")];if("number"!==o&&"categorical"!==r){let n=`number expected, ${o} found`;return an(e)&&void 0===r&&(n+='\nIf you intended to use a categorical function, specify `"type": "categorical"`.'),[new it(t.key,u,n)]}return "categorical"!==r||"number"!==o||isFinite(l)&&Math.floor(l)===l?"categorical"!==r&&"number"===o&&void 0!==i&&lnew it(`${t.key}${e.key}`,t.value,e.message)));const r=e.value.expression||e.value._styleExpression.expression;if("property"===t.expressionContext&&"text-font"===t.propertyKey&&!r.outputDefined())return [new it(t.key,t.value,`Invalid data expression for "${t.propertyKey}". Output values must be contained as literals within the expression.`)];if("property"===t.expressionContext&&"layout"===t.propertyType&&!en(r))return [new it(t.key,t.value,'"feature-state" data expressions are not supported with layout properties.')];if("filter"===t.expressionContext&&!en(r))return [new it(t.key,t.value,'"feature-state" data expressions are not supported with filters.')];if(t.expressionContext&&0===t.expressionContext.indexOf("cluster")){if(!rn(r,["zoom","feature-state"]))return [new it(t.key,t.value,'"zoom" and "feature-state" expressions are not supported with cluster properties.')];if("cluster-initial"===t.expressionContext&&!tn(r))return [new it(t.key,t.value,"Feature data expressions are not supported with initial expression part of cluster properties.")]}return []}function Gn(t){const e=t.key,r=t.value,n=t.valueSpec,i=[];return Array.isArray(n.values)?-1===n.values.indexOf(On(r))&&i.push(new it(e,r,`expected one of [${n.values.join(", ")}], ${JSON.stringify(r)} found`)):-1===Object.keys(n.values).indexOf(On(r))&&i.push(new it(e,r,`expected one of [${Object.keys(n.values).join(", ")}], ${JSON.stringify(r)} found`)),i}function Kn(t){return Mn(jn(t.value))?Zn(st({},t,{expressionContext:"filter",valueSpec:{value:"boolean"}})):Xn(t)}function Xn(t){const e=t.value,r=t.key;if("array"!==un(e))return [new it(r,e,`array expected, ${un(e)} found`)];const n=t.styleSpec;let i,s=[];if(e.length<1)return [new it(r,e,"filter array must have at least 1 element")];switch(s=s.concat(Gn({key:`${r}[0]`,value:e[0],valueSpec:n.filter_operator,style:t.style,styleSpec:t.styleSpec})),On(e[0])){case"<":case"<=":case">":case">=":e.length>=2&&"$type"===On(e[1])&&s.push(new it(r,e,`"$type" cannot be use with operator "${e[0]}"`));case"==":case"!=":3!==e.length&&s.push(new it(r,e,`filter array for operator "${e[0]}" must have 3 elements`));case"in":case"!in":e.length>=2&&(i=un(e[1]),"string"!==i&&s.push(new it(`${r}[1]`,e[1],`string expected, ${i} found`)));for(let a=2;a{t in r&&e.push(new it(n,r[t],`"${t}" is prohibited for ref layers`));})),i.layers.forEach((e=>{On(e.id)===o&&(t=e);})),t?t.ref?e.push(new it(n,r.ref,"ref cannot reference another ref layer")):a=On(t.type):e.push(new it(n,r.ref,`ref layer "${o}" not found`));}else if("background"!==a)if(r.source){const t=i.sources&&i.sources[r.source],s=t&&On(t.type);t?"vector"===s&&"raster"===a?e.push(new it(n,r.source,`layer "${r.id}" requires a raster source`)):"raster-dem"!==s&&"hillshade"===a?e.push(new it(n,r.source,`layer "${r.id}" requires a raster-dem source`)):"raster"===s&&"raster"!==a?e.push(new it(n,r.source,`layer "${r.id}" requires a vector source`)):"vector"!==s||r["source-layer"]?"raster-dem"===s&&"hillshade"!==a?e.push(new it(n,r.source,"raster-dem source can only be used with layer type 'hillshade'.")):"line"!==a||!r.paint||!r.paint["line-gradient"]||"geojson"===s&&t.lineMetrics||e.push(new it(n,r,`layer "${r.id}" specifies a line-gradient, which requires a GeoJSON source with \`lineMetrics\` enabled.`)):e.push(new it(n,r,`layer "${r.id}" must specify a "source-layer"`)):e.push(new it(n,r.source,`source "${r.source}" not found`));}else e.push(new it(n,r,'missing required property "source"'));return e=e.concat(Rn({key:n,value:r,valueSpec:s.layer,style:t.style,styleSpec:t.styleSpec,validateSpec:t.validateSpec,objectElementValidators:{"*":()=>[],type:()=>t.validateSpec({key:`${n}.type`,value:r.type,valueSpec:s.layer.type,style:t.style,styleSpec:t.styleSpec,validateSpec:t.validateSpec,object:r,objectKey:"type"}),filter:Kn,layout:t=>Rn({layer:r,key:t.key,value:t.value,style:t.style,styleSpec:t.styleSpec,validateSpec:t.validateSpec,objectElementValidators:{"*":t=>Jn(st({layerType:a},t))}}),paint:t=>Rn({layer:r,key:t.key,value:t.value,style:t.style,styleSpec:t.styleSpec,validateSpec:t.validateSpec,objectElementValidators:{"*":t=>Yn(st({layerType:a},t))}})}})),e}function Qn(t){const e=t.value,r=t.key,n=un(e);return "string"!==n?[new it(r,e,`string expected, ${n} found`)]:[]}const ti={promoteId:function({key:t,value:e}){if("string"===un(e))return Qn({key:t,value:e});{const r=[];for(const n in e)r.push(...Qn({key:`${t}.${n}`,value:e[n]}));return r}}};function ei(t){const e=t.value,r=t.key,n=t.styleSpec,i=t.style,s=t.validateSpec;if(!e.type)return [new it(r,e,'"type" is required')];const a=On(e.type);let o;switch(a){case"vector":case"raster":return o=Rn({key:r,value:e,valueSpec:n[`source_${a.replace("-","_")}`],style:t.style,styleSpec:n,objectElementValidators:ti,validateSpec:s}),o;case"raster-dem":return o=function(t){var e;const r=null!==(e=t.sourceName)&&void 0!==e?e:"",n=t.value,i=t.styleSpec,s=i.source_raster_dem,a=t.style;let o=[];const l=un(n);if(void 0===n)return o;if("object"!==l)return o.push(new it("source_raster_dem",n,`object expected, ${l} found`)),o;const u="custom"===On(n.encoding),c=["redFactor","greenFactor","blueFactor","baseShift"],h=t.value.encoding?`"${t.value.encoding}"`:"Default";for(const e in n)!u&&c.includes(e)?o.push(new it(e,n[e],`In "${r}": "${e}" is only valid when "encoding" is set to "custom". ${h} encoding found`)):s[e]?o=o.concat(t.validateSpec({key:e,value:n[e],valueSpec:s[e],validateSpec:t.validateSpec,style:a,styleSpec:i})):o.push(new it(e,n[e],`unknown property "${e}"`));return o}({sourceName:r,value:e,style:t.style,styleSpec:n,validateSpec:s}),o;case"geojson":if(o=Rn({key:r,value:e,valueSpec:n.source_geojson,style:i,styleSpec:n,validateSpec:s,objectElementValidators:ti}),e.cluster)for(const t in e.clusterProperties){const[n,i]=e.clusterProperties[t],a="string"==typeof n?[n,["accumulated"],["get",t]]:n;o.push(...Zn({key:`${r}.${t}.map`,value:i,validateSpec:s,expressionContext:"cluster-map"})),o.push(...Zn({key:`${r}.${t}.reduce`,value:a,validateSpec:s,expressionContext:"cluster-reduce"}));}return o;case"video":return Rn({key:r,value:e,valueSpec:n.source_video,style:i,validateSpec:s,styleSpec:n});case"image":return Rn({key:r,value:e,valueSpec:n.source_image,style:i,validateSpec:s,styleSpec:n});case"canvas":return [new it(r,null,"Please use runtime APIs to add canvas sources, rather than including them in stylesheets.","source.canvas")];default:return Gn({key:`${r}.type`,value:e.type,valueSpec:{values:["vector","raster","raster-dem","geojson","video","image"]},style:i,validateSpec:s,styleSpec:n})}}function ri(t){const e=t.value,r=t.styleSpec,n=r.light,i=t.style;let s=[];const a=un(e);if(void 0===e)return s;if("object"!==a)return s=s.concat([new it("light",e,`object expected, ${a} found`)]),s;for(const a in e){const o=a.match(/^(.*)-transition$/);s=s.concat(o&&n[o[1]]&&n[o[1]].transition?t.validateSpec({key:a,value:e[a],valueSpec:r.transition,validateSpec:t.validateSpec,style:i,styleSpec:r}):n[a]?t.validateSpec({key:a,value:e[a],valueSpec:n[a],validateSpec:t.validateSpec,style:i,styleSpec:r}):[new it(a,e[a],`unknown property "${a}"`)]);}return s}function ni(t){const e=t.value,r=t.styleSpec,n=r.sky,i=t.style,s=un(e);if(void 0===e)return [];if("object"!==s)return [new it("sky",e,`object expected, ${s} found`)];let a=[];for(const s in e)a=a.concat(n[s]?t.validateSpec({key:s,value:e[s],valueSpec:n[s],style:i,styleSpec:r}):[new it(s,e[s],`unknown property "${s}"`)]);return a}function ii(t){const e=t.value,r=t.styleSpec,n=r.terrain,i=t.style;let s=[];const a=un(e);if(void 0===e)return s;if("object"!==a)return s=s.concat([new it("terrain",e,`object expected, ${a} found`)]),s;for(const a in e)s=s.concat(n[a]?t.validateSpec({key:a,value:e[a],valueSpec:n[a],validateSpec:t.validateSpec,style:i,styleSpec:r}):[new it(a,e[a],`unknown property "${a}"`)]);return s}function si(t){let e=[];const r=t.value,n=t.key;if(Array.isArray(r)){const i=[],s=[];for(const a in r)r[a].id&&i.includes(r[a].id)&&e.push(new it(n,r,`all the sprites' ids must be unique, but ${r[a].id} is duplicated`)),i.push(r[a].id),r[a].url&&s.includes(r[a].url)&&e.push(new it(n,r,`all the sprites' URLs must be unique, but ${r[a].url} is duplicated`)),s.push(r[a].url),e=e.concat(Rn({key:`${n}[${a}]`,value:r[a],valueSpec:{id:{type:"string",required:!0},url:{type:"string",required:!0}},validateSpec:t.validateSpec}));return e}return Qn({key:n,value:r})}const ai={"*":()=>[],array:Un,boolean:function(t){const e=t.value,r=t.key,n=un(e);return "boolean"!==n?[new it(r,e,`boolean expected, ${n} found`)]:[]},number:qn,color:function(t){const e=t.key,r=t.value,n=un(r);return "string"!==n?[new it(e,r,`color expected, ${n} found`)]:Kt.parse(String(r))?[]:[new it(e,r,`color expected, "${r}" found`)]},constants:Dn,enum:Gn,filter:Kn,function:Nn,layer:Wn,object:Rn,source:ei,light:ri,sky:ni,terrain:ii,projection:function(t){const e=t.value,r=t.styleSpec,n=r.projection,i=t.style,s=un(e);if(void 0===e)return [];if("object"!==s)return [new it("projection",e,`object expected, ${s} found`)];let a=[];for(const s in e)a=a.concat(n[s]?t.validateSpec({key:s,value:e[s],valueSpec:n[s],style:i,styleSpec:r}):[new it(s,e[s],`unknown property "${s}"`)]);return a},string:Qn,formatted:function(t){return 0===Qn(t).length?[]:Zn(t)},resolvedImage:function(t){return 0===Qn(t).length?[]:Zn(t)},padding:function(t){const e=t.key,r=t.value;if("array"===un(r)){if(r.length<1||r.length>4)return [new it(e,r,`padding requires 1 to 4 values; ${r.length} values found`)];const n={type:"number"};let i=[];for(let s=0;s[]}})),t.constants&&(r=r.concat(Dn({key:"constants",value:t.constants,style:t,styleSpec:e,validateSpec:oi}))),hi(r)}function ci(t){return function(e){return t({...e,validateSpec:oi})}}function hi(t){return [].concat(t).sort(((t,e)=>t.line-e.line))}function pi(t){return function(...e){return hi(t.apply(this,e))}}ui.source=pi(ci(ei)),ui.sprite=pi(ci(si)),ui.glyphs=pi(ci(li)),ui.light=pi(ci(ri)),ui.sky=pi(ci(ni)),ui.terrain=pi(ci(ii)),ui.layer=pi(ci(Wn)),ui.filter=pi(ci(Kn)),ui.paintProperty=pi(ci(Yn)),ui.layoutProperty=pi(ci(Jn));const fi=ui,di=fi.light,yi=fi.sky,mi=fi.paintProperty,gi=fi.layoutProperty;function xi(t,e){let r=!1;if(e&&e.length)for(const n of e)t.fire(new N(new Error(n.message))),r=!0;return r}class vi{constructor(t,e,r){const n=this.cells=[];if(t instanceof ArrayBuffer){this.arrayBuffer=t;const i=new Int32Array(this.arrayBuffer);t=i[0],this.d=(e=i[1])+2*(r=i[2]);for(let t=0;t=u[l+0]&&n>=u[l+1])?(a[h]=!0,s.push(i[h])):a[h]=!1;}}}}_forEachCell(t,e,r,n,i,s,a,o){const l=this._convertToCellCoord(t),u=this._convertToCellCoord(e),c=this._convertToCellCoord(r),h=this._convertToCellCoord(n);for(let p=l;p<=c;p++)for(let l=u;l<=h;l++){const u=this.d*l+p;if((!o||o(this._convertFromCellCoord(p),this._convertFromCellCoord(l),this._convertFromCellCoord(p+1),this._convertFromCellCoord(l+1)))&&i.call(this,t,e,r,n,u,s,a,o))return}}_convertFromCellCoord(t){return (t-this.padding)/this.scale}_convertToCellCoord(t){return Math.max(0,Math.min(this.d-1,Math.floor(t*this.scale)+this.padding))}toArrayBuffer(){if(this.arrayBuffer)return this.arrayBuffer;const t=this.cells,e=3+this.cells.length+1+1;let r=0;for(let t=0;t=0)continue;const s=t[n];i[n]=bi[r].shallow.indexOf(n)>=0?s:ki(s,e);}t instanceof Error&&(i.message=t.message);}if(i.$name)throw new Error("$name property is reserved for worker serialization logic.");return "Object"!==r&&(i.$name=r),i}function Mi(t){if(Si(t))return t;if(Array.isArray(t))return t.map(Mi);if("object"!=typeof t)throw new Error("can't deserialize object of type "+typeof t);const e=Ai(t)||"Object";if(!bi[e])throw new Error(`can't deserialize unregistered class ${e}`);const{klass:r}=bi[e];if(!r)throw new Error(`can't deserialize unregistered class ${e}`);if(r.deserialize)return r.deserialize(t);const n=Object.create(r.prototype);for(const r of Object.keys(t)){if("$name"===r)continue;const i=t[r];n[r]=bi[e].shallow.indexOf(r)>=0?i:Mi(i);}return n}class Ii{constructor(){this.first=!0;}update(t,e){const r=Math.floor(t);return this.first?(this.first=!1,this.lastIntegerZoom=r,this.lastIntegerZoomTime=0,this.lastZoom=t,this.lastFloorZoom=r,!0):(this.lastFloorZoom>r?(this.lastIntegerZoom=r+1,this.lastIntegerZoomTime=e):this.lastFloorZoomt>=128&&t<=255,"Hangul Jamo":t=>t>=4352&&t<=4607,Khmer:t=>t>=6016&&t<=6143,"General Punctuation":t=>t>=8192&&t<=8303,"Letterlike Symbols":t=>t>=8448&&t<=8527,"Number Forms":t=>t>=8528&&t<=8591,"Miscellaneous Technical":t=>t>=8960&&t<=9215,"Control Pictures":t=>t>=9216&&t<=9279,"Optical Character Recognition":t=>t>=9280&&t<=9311,"Enclosed Alphanumerics":t=>t>=9312&&t<=9471,"Geometric Shapes":t=>t>=9632&&t<=9727,"Miscellaneous Symbols":t=>t>=9728&&t<=9983,"Miscellaneous Symbols and Arrows":t=>t>=11008&&t<=11263,"Ideographic Description Characters":t=>t>=12272&&t<=12287,"CJK Symbols and Punctuation":t=>t>=12288&&t<=12351,Katakana:t=>t>=12448&&t<=12543,Kanbun:t=>t>=12688&&t<=12703,"CJK Strokes":t=>t>=12736&&t<=12783,"Enclosed CJK Letters and Months":t=>t>=12800&&t<=13055,"CJK Compatibility":t=>t>=13056&&t<=13311,"Yijing Hexagram Symbols":t=>t>=19904&&t<=19967,"Private Use Area":t=>t>=57344&&t<=63743,"Vertical Forms":t=>t>=65040&&t<=65055,"CJK Compatibility Forms":t=>t>=65072&&t<=65103,"Small Form Variants":t=>t>=65104&&t<=65135,"Halfwidth and Fullwidth Forms":t=>t>=65280&&t<=65519};function Pi(t){for(const e of t)if(Ti(e.charCodeAt(0)))return !0;return !1}function Ci(t){for(const e of t)if(!Ei(e.charCodeAt(0)))return !1;return !0}function Bi(t){const e=t.map((t=>{try{return new RegExp(`\\p{sc=${t}}`,"u").source}catch(t){return null}})).filter((t=>t));return new RegExp(e.join("|"),"u")}const Vi=Bi(["Arab","Dupl","Mong","Ougr","Syrc"]);function Ei(t){return !Vi.test(String.fromCodePoint(t))}const Fi=Bi(["Bopo","Hani","Hira","Kana","Kits","Nshu","Tang","Yiii"]);function Ti(t){return !(746!==t&&747!==t&&(t<4352||!(zi["CJK Compatibility Forms"](t)&&!(t>=65097&&t<=65103)||zi["CJK Compatibility"](t)||zi["CJK Strokes"](t)||!(!zi["CJK Symbols and Punctuation"](t)||t>=12296&&t<=12305||t>=12308&&t<=12319||12336===t)||zi["Enclosed CJK Letters and Months"](t)||zi["Ideographic Description Characters"](t)||zi.Kanbun(t)||zi.Katakana(t)&&12540!==t||!(!zi["Halfwidth and Fullwidth Forms"](t)||65288===t||65289===t||65293===t||t>=65306&&t<=65310||65339===t||65341===t||65343===t||t>=65371&&t<=65503||65507===t||t>=65512&&t<=65519)||!(!zi["Small Form Variants"](t)||t>=65112&&t<=65118||t>=65123&&t<=65126)||zi["Vertical Forms"](t)||zi["Yijing Hexagram Symbols"](t)||/\p{sc=Cans}/u.test(String.fromCodePoint(t))||/\p{sc=Hang}/u.test(String.fromCodePoint(t))||Fi.test(String.fromCodePoint(t)))))}function $i(t){return !(Ti(t)||function(t){return !!(zi["Latin-1 Supplement"](t)&&(167===t||169===t||174===t||177===t||188===t||189===t||190===t||215===t||247===t)||zi["General Punctuation"](t)&&(8214===t||8224===t||8225===t||8240===t||8241===t||8251===t||8252===t||8258===t||8263===t||8264===t||8265===t||8273===t)||zi["Letterlike Symbols"](t)||zi["Number Forms"](t)||zi["Miscellaneous Technical"](t)&&(t>=8960&&t<=8967||t>=8972&&t<=8991||t>=8996&&t<=9e3||9003===t||t>=9085&&t<=9114||t>=9150&&t<=9165||9167===t||t>=9169&&t<=9179||t>=9186&&t<=9215)||zi["Control Pictures"](t)&&9251!==t||zi["Optical Character Recognition"](t)||zi["Enclosed Alphanumerics"](t)||zi["Geometric Shapes"](t)||zi["Miscellaneous Symbols"](t)&&!(t>=9754&&t<=9759)||zi["Miscellaneous Symbols and Arrows"](t)&&(t>=11026&&t<=11055||t>=11088&&t<=11097||t>=11192&&t<=11243)||zi["CJK Symbols and Punctuation"](t)||zi.Katakana(t)||zi["Private Use Area"](t)||zi["CJK Compatibility Forms"](t)||zi["Small Form Variants"](t)||zi["Halfwidth and Fullwidth Forms"](t)||8734===t||8756===t||8757===t||t>=9984&&t<=10087||t>=10102&&t<=10131||65532===t||65533===t)}(t))}const Li=Bi(["Adlm","Arab","Armi","Avst","Chrs","Cprt","Egyp","Elym","Gara","Hatr","Hebr","Hung","Khar","Lydi","Mand","Mani","Mend","Merc","Mero","Narb","Nbat","Nkoo","Orkh","Palm","Phli","Phlp","Phnx","Prti","Rohg","Samr","Sarb","Sogo","Syrc","Thaa","Todr","Yezi"]);function Di(t){return Li.test(String.fromCodePoint(t))}function Oi(t,e){return !(!e&&Di(t)||t>=2304&&t<=3583||t>=3840&&t<=4255||zi.Khmer(t))}function ji(t){for(const e of t)if(Di(e.charCodeAt(0)))return !0;return !1}const Ri=new class{constructor(){this.applyArabicShaping=null,this.processBidirectionalText=null,this.processStyledBidirectionalText=null,this.pluginStatus="unavailable",this.pluginURL=null;}setState(t){this.pluginStatus=t.pluginStatus,this.pluginURL=t.pluginURL;}getState(){return {pluginStatus:this.pluginStatus,pluginURL:this.pluginURL}}setMethods(t){this.applyArabicShaping=t.applyArabicShaping,this.processBidirectionalText=t.processBidirectionalText,this.processStyledBidirectionalText=t.processStyledBidirectionalText;}isParsed(){return null!=this.applyArabicShaping&&null!=this.processBidirectionalText&&null!=this.processStyledBidirectionalText}getPluginURL(){return this.pluginURL}getRTLTextPluginStatus(){return this.pluginStatus}};class Ui{constructor(t,e){this.zoom=t,e?(this.now=e.now,this.fadeDuration=e.fadeDuration,this.zoomHistory=e.zoomHistory,this.transition=e.transition):(this.now=0,this.fadeDuration=0,this.zoomHistory=new Ii,this.transition={});}isSupportedScript(t){return function(t,e){for(const r of t)if(!Oi(r.charCodeAt(0),e))return !1;return !0}(t,"loaded"===Ri.getRTLTextPluginStatus())}crossFadingFactor(){return 0===this.fadeDuration?1:Math.min((this.now-this.zoomHistory.lastIntegerZoomTime)/this.fadeDuration,1)}getCrossfadeParameters(){const t=this.zoom,e=t-Math.floor(t),r=this.crossFadingFactor();return t>this.zoomHistory.lastIntegerZoom?{fromScale:2,toScale:1,t:e+(1-e)*r}:{fromScale:.5,toScale:1,t:1-(1-r)*e}}}class qi{constructor(t,e){this.property=t,this.value=e,this.expression=function(t,e){if(cn(t))return new Sn(t,e);if(vn(t)){const r=An(t,e);if("error"===r.result)throw new Error(r.value.map((t=>`${t.key}: ${t.message}`)).join(", "));return r.value}{let r=t;return "color"===e.type&&"string"==typeof t?r=Kt.parse(t):"padding"!==e.type||"number"!=typeof t&&!Array.isArray(t)?"variableAnchorOffsetCollection"===e.type&&Array.isArray(t)&&(r=Qt.parse(t)):r=Jt.parse(t),{kind:"constant",evaluate:()=>r}}}(void 0===e?t.specification.default:e,t.specification);}isDataDriven(){return "source"===this.expression.kind||"composite"===this.expression.kind}possiblyEvaluate(t,e,r){return this.property.possiblyEvaluate(this,t,e,r)}}class Ni{constructor(t){this.property=t,this.value=new qi(t,void 0);}transitioned(t,e){return new Gi(this.property,this.value,e,g({},t.transition,this.transition),t.now)}untransitioned(){return new Gi(this.property,this.value,null,{},0)}}class Zi{constructor(t){this._properties=t,this._values=Object.create(t.defaultTransitionablePropertyValues);}getValue(t){return w(this._values[t].value.value)}setValue(t,e){Object.prototype.hasOwnProperty.call(this._values,t)||(this._values[t]=new Ni(this._values[t].property)),this._values[t].value=new qi(this._values[t].property,null===e?void 0:w(e));}getTransition(t){return w(this._values[t].transition)}setTransition(t,e){Object.prototype.hasOwnProperty.call(this._values,t)||(this._values[t]=new Ni(this._values[t].property)),this._values[t].transition=w(e)||void 0;}serialize(){const t={};for(const e of Object.keys(this._values)){const r=this.getValue(e);void 0!==r&&(t[e]=r);const n=this.getTransition(e);void 0!==n&&(t[`${e}-transition`]=n);}return t}transitioned(t,e){const r=new Ki(this._properties);for(const n of Object.keys(this._values))r._values[n]=this._values[n].transitioned(t,e._values[n]);return r}untransitioned(){const t=new Ki(this._properties);for(const e of Object.keys(this._values))t._values[e]=this._values[e].untransitioned();return t}}class Gi{constructor(t,e,r,n,i){this.property=t,this.value=e,this.begin=i+n.delay||0,this.end=this.begin+n.duration||0,t.specification.transition&&(n.delay||n.duration)&&(this.prior=r);}possiblyEvaluate(t,e,r){const n=t.now||0,i=this.value.possiblyEvaluate(t,e,r),s=this.prior;if(s){if(n>this.end)return this.prior=null,i;if(this.value.isDataDriven())return this.prior=null,i;if(n=1)return 1;const e=t*t,r=e*t;return 4*(t<.5?r:3*(t-e)+r-.75)}(a))}}return i}}class Ki{constructor(t){this._properties=t,this._values=Object.create(t.defaultTransitioningPropertyValues);}possiblyEvaluate(t,e,r){const n=new Yi(this._properties);for(const i of Object.keys(this._values))n._values[i]=this._values[i].possiblyEvaluate(t,e,r);return n}hasTransition(){for(const t of Object.keys(this._values))if(this._values[t].prior)return !0;return !1}}class Xi{constructor(t){this._properties=t,this._values=Object.create(t.defaultPropertyValues);}hasValue(t){return void 0!==this._values[t].value}getValue(t){return w(this._values[t].value)}setValue(t,e){this._values[t]=new qi(this._values[t].property,null===e?void 0:w(e));}serialize(){const t={};for(const e of Object.keys(this._values)){const r=this.getValue(e);void 0!==r&&(t[e]=r);}return t}possiblyEvaluate(t,e,r){const n=new Yi(this._properties);for(const i of Object.keys(this._values))n._values[i]=this._values[i].possiblyEvaluate(t,e,r);return n}}class Hi{constructor(t,e,r){this.property=t,this.value=e,this.parameters=r;}isConstant(){return "constant"===this.value.kind}constantOr(t){return "constant"===this.value.kind?this.value.value:t}evaluate(t,e,r,n){return this.property.evaluate(this.value,this.parameters,t,e,r,n)}}class Yi{constructor(t){this._properties=t,this._values=Object.create(t.defaultPossiblyEvaluatedValues);}get(t){return this._values[t]}}class Ji{constructor(t){this.specification=t;}possiblyEvaluate(t,e){if(t.isDataDriven())throw new Error("Value should not be data driven");return t.expression.evaluate(e)}interpolate(t,e,r){const n=Ce[this.specification.type];return n?n(t,e,r):t}}class Wi{constructor(t,e){this.specification=t,this.overrides=e;}possiblyEvaluate(t,e,r,n){return new Hi(this,"constant"===t.expression.kind||"camera"===t.expression.kind?{kind:"constant",value:t.expression.evaluate(e,null,{},r,n)}:t.expression,e)}interpolate(t,e,r){if("constant"!==t.value.kind||"constant"!==e.value.kind)return t;if(void 0===t.value.value||void 0===e.value.value)return new Hi(this,{kind:"constant",value:void 0},t.parameters);const n=Ce[this.specification.type];if(n){const i=n(t.value.value,e.value.value,r);return new Hi(this,{kind:"constant",value:i},t.parameters)}return t}evaluate(t,e,r,n,i,s){return "constant"===t.kind?t.value:t.evaluate(e,r,n,i,s)}}class Qi extends Wi{possiblyEvaluate(t,e,r,n){if(void 0===t.value)return new Hi(this,{kind:"constant",value:void 0},e);if("constant"===t.expression.kind){const i=t.expression.evaluate(e,null,{},r,n),s="resolvedImage"===t.property.specification.type&&"string"!=typeof i?i.name:i,a=this._calculate(s,s,s,e);return new Hi(this,{kind:"constant",value:a},e)}if("camera"===t.expression.kind){const r=this._calculate(t.expression.evaluate({zoom:e.zoom-1}),t.expression.evaluate({zoom:e.zoom}),t.expression.evaluate({zoom:e.zoom+1}),e);return new Hi(this,{kind:"constant",value:r},e)}return new Hi(this,t.expression,e)}evaluate(t,e,r,n,i,s){if("source"===t.kind){const a=t.evaluate(e,r,n,i,s);return this._calculate(a,a,a,e)}return "composite"===t.kind?this._calculate(t.evaluate({zoom:Math.floor(e.zoom)-1},r,n),t.evaluate({zoom:Math.floor(e.zoom)},r,n),t.evaluate({zoom:Math.floor(e.zoom)+1},r,n),e):t.value}_calculate(t,e,r,n){return n.zoom>n.zoomHistory.lastIntegerZoom?{from:t,to:e}:{from:r,to:e}}interpolate(t){return t}}class ts{constructor(t){this.specification=t;}possiblyEvaluate(t,e,r,n){if(void 0!==t.value){if("constant"===t.expression.kind){const i=t.expression.evaluate(e,null,{},r,n);return this._calculate(i,i,i,e)}return this._calculate(t.expression.evaluate(new Ui(Math.floor(e.zoom-1),e)),t.expression.evaluate(new Ui(Math.floor(e.zoom),e)),t.expression.evaluate(new Ui(Math.floor(e.zoom+1),e)),e)}}_calculate(t,e,r,n){return n.zoom>n.zoomHistory.lastIntegerZoom?{from:t,to:e}:{from:r,to:e}}interpolate(t){return t}}class es{constructor(t){this.specification=t;}possiblyEvaluate(t,e,r,n){return !!t.expression.evaluate(e,null,{},r,n)}interpolate(){return !1}}class rs{constructor(t){this.properties=t,this.defaultPropertyValues={},this.defaultTransitionablePropertyValues={},this.defaultTransitioningPropertyValues={},this.defaultPossiblyEvaluatedValues={},this.overridableProperties=[];for(const e in t){const r=t[e];r.specification.overridable&&this.overridableProperties.push(e);const n=this.defaultPropertyValues[e]=new qi(r,void 0),i=this.defaultTransitionablePropertyValues[e]=new Ni(r);this.defaultTransitioningPropertyValues[e]=i.untransitioned(),this.defaultPossiblyEvaluatedValues[e]=n.possiblyEvaluate({});}}}wi("DataDrivenProperty",Wi),wi("DataConstantProperty",Ji),wi("CrossFadedDataDrivenProperty",Qi),wi("CrossFadedProperty",ts),wi("ColorRampProperty",es);const ns="-transition";class is extends Z{constructor(t,e){if(super(),this.id=t.id,this.type=t.type,this._featureFilter={filter:()=>!0,needGeometry:!1},"custom"!==t.type&&(this.metadata=t.metadata,this.minzoom=t.minzoom,this.maxzoom=t.maxzoom,"background"!==t.type&&(this.source=t.source,this.sourceLayer=t["source-layer"],this.filter=t.filter),e.layout&&(this._unevaluatedLayout=new Xi(e.layout)),e.paint)){this._transitionablePaint=new Zi(e.paint);for(const e in t.paint)this.setPaintProperty(e,t.paint[e],{validate:!1});for(const e in t.layout)this.setLayoutProperty(e,t.layout[e],{validate:!1});this._transitioningPaint=this._transitionablePaint.untransitioned(),this.paint=new Yi(e.paint);}}getCrossfadeParameters(){return this._crossfadeParameters}getLayoutProperty(t){return "visibility"===t?this.visibility:this._unevaluatedLayout.getValue(t)}setLayoutProperty(t,e,r={}){null!=e&&this._validate(gi,`layers.${this.id}.layout.${t}`,t,e,r)||("visibility"!==t?this._unevaluatedLayout.setValue(t,e):this.visibility=e);}getPaintProperty(t){return t.endsWith(ns)?this._transitionablePaint.getTransition(t.slice(0,-11)):this._transitionablePaint.getValue(t)}setPaintProperty(t,e,r={}){if(null!=e&&this._validate(mi,`layers.${this.id}.paint.${t}`,t,e,r))return !1;if(t.endsWith(ns))return this._transitionablePaint.setTransition(t.slice(0,-11),e||void 0),!1;{const r=this._transitionablePaint._values[t],n="cross-faded-data-driven"===r.property.specification["property-type"],i=r.value.isDataDriven(),s=r.value;this._transitionablePaint.setValue(t,e),this._handleSpecialPaintPropertyUpdate(t);const a=this._transitionablePaint._values[t].value;return a.isDataDriven()||i||n||this._handleOverridablePaintPropertyUpdate(t,s,a)}}_handleSpecialPaintPropertyUpdate(t){}_handleOverridablePaintPropertyUpdate(t,e,r){return !1}isHidden(t){return !!(this.minzoom&&t=this.maxzoom)||"none"===this.visibility}updateTransitions(t){this._transitioningPaint=this._transitionablePaint.transitioned(t,this._transitioningPaint);}hasTransition(){return this._transitioningPaint.hasTransition()}recalculate(t,e){t.getCrossfadeParameters&&(this._crossfadeParameters=t.getCrossfadeParameters()),this._unevaluatedLayout&&(this.layout=this._unevaluatedLayout.possiblyEvaluate(t,void 0,e)),this.paint=this._transitioningPaint.possiblyEvaluate(t,void 0,e);}serialize(){const t={id:this.id,type:this.type,source:this.source,"source-layer":this.sourceLayer,metadata:this.metadata,minzoom:this.minzoom,maxzoom:this.maxzoom,filter:this.filter,layout:this._unevaluatedLayout&&this._unevaluatedLayout.serialize(),paint:this._transitionablePaint&&this._transitionablePaint.serialize()};return this.visibility&&(t.layout=t.layout||{},t.layout.visibility=this.visibility),b(t,((t,e)=>!(void 0===t||"layout"===e&&!Object.keys(t).length||"paint"===e&&!Object.keys(t).length)))}_validate(t,e,r,n,i={}){return (!i||!1!==i.validate)&&xi(this,t.call(fi,{key:e,layerType:this.type,objectKey:r,value:n,styleSpec:G,style:{glyphs:!0,sprite:!0}}))}is3D(){return !1}isTileClipped(){return !1}hasOffscreenPass(){return !1}resize(){}isStateDependent(){for(const t in this.paint._values){const e=this.paint.get(t);if(e instanceof Hi&&an(e.property.specification)&&("source"===e.value.kind||"composite"===e.value.kind)&&e.value.isStateDependent)return !0}return !1}}const ss={Int8:Int8Array,Uint8:Uint8Array,Int16:Int16Array,Uint16:Uint16Array,Int32:Int32Array,Uint32:Uint32Array,Float32:Float32Array};class as{constructor(t,e){this._structArray=t,this._pos1=e*this.size,this._pos2=this._pos1/2,this._pos4=this._pos1/4,this._pos8=this._pos1/8;}}class os{constructor(){this.isTransferred=!1,this.capacity=-1,this.resize(0);}static serialize(t,e){return t._trim(),e&&(t.isTransferred=!0,e.push(t.arrayBuffer)),{length:t.length,arrayBuffer:t.arrayBuffer}}static deserialize(t){const e=Object.create(this.prototype);return e.arrayBuffer=t.arrayBuffer,e.length=t.length,e.capacity=t.arrayBuffer.byteLength/e.bytesPerElement,e._refreshViews(),e}_trim(){this.length!==this.capacity&&(this.capacity=this.length,this.arrayBuffer=this.arrayBuffer.slice(0,this.length*this.bytesPerElement),this._refreshViews());}clear(){this.length=0;}resize(t){this.reserve(t),this.length=t;}reserve(t){if(t>this.capacity){this.capacity=Math.max(t,Math.floor(5*this.capacity),128),this.arrayBuffer=new ArrayBuffer(this.capacity*this.bytesPerElement);const e=this.uint8;this._refreshViews(),e&&this.uint8.set(e);}}_refreshViews(){throw new Error("_refreshViews() must be implemented by each concrete StructArray layout")}}function ls(t,e=1){let r=0,n=0;return {members:t.map((t=>{const i=ss[t.type].BYTES_PER_ELEMENT,s=r=us(r,Math.max(e,i)),a=t.components||1;return n=Math.max(n,i),r+=i*a,{name:t.name,type:t.type,components:a,offset:s}})),size:us(r,Math.max(n,e)),alignment:e}}function us(t,e){return Math.ceil(t/e)*e}class cs extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer);}emplaceBack(t,e){const r=this.length;return this.resize(r+1),this.emplace(r,t,e)}emplace(t,e,r){const n=2*t;return this.int16[n+0]=e,this.int16[n+1]=r,t}}cs.prototype.bytesPerElement=4,wi("StructArrayLayout2i4",cs);class hs extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer);}emplaceBack(t,e,r){const n=this.length;return this.resize(n+1),this.emplace(n,t,e,r)}emplace(t,e,r,n){const i=3*t;return this.int16[i+0]=e,this.int16[i+1]=r,this.int16[i+2]=n,t}}hs.prototype.bytesPerElement=6,wi("StructArrayLayout3i6",hs);class ps extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer);}emplaceBack(t,e,r,n){const i=this.length;return this.resize(i+1),this.emplace(i,t,e,r,n)}emplace(t,e,r,n,i){const s=4*t;return this.int16[s+0]=e,this.int16[s+1]=r,this.int16[s+2]=n,this.int16[s+3]=i,t}}ps.prototype.bytesPerElement=8,wi("StructArrayLayout4i8",ps);class fs extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i,s){const a=this.length;return this.resize(a+1),this.emplace(a,t,e,r,n,i,s)}emplace(t,e,r,n,i,s,a){const o=6*t;return this.int16[o+0]=e,this.int16[o+1]=r,this.int16[o+2]=n,this.int16[o+3]=i,this.int16[o+4]=s,this.int16[o+5]=a,t}}fs.prototype.bytesPerElement=12,wi("StructArrayLayout2i4i12",fs);class ds extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i,s){const a=this.length;return this.resize(a+1),this.emplace(a,t,e,r,n,i,s)}emplace(t,e,r,n,i,s,a){const o=4*t,l=8*t;return this.int16[o+0]=e,this.int16[o+1]=r,this.uint8[l+4]=n,this.uint8[l+5]=i,this.uint8[l+6]=s,this.uint8[l+7]=a,t}}ds.prototype.bytesPerElement=8,wi("StructArrayLayout2i4ub8",ds);class ys extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.float32=new Float32Array(this.arrayBuffer);}emplaceBack(t,e){const r=this.length;return this.resize(r+1),this.emplace(r,t,e)}emplace(t,e,r){const n=2*t;return this.float32[n+0]=e,this.float32[n+1]=r,t}}ys.prototype.bytesPerElement=8,wi("StructArrayLayout2f8",ys);class ms extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i,s,a,o,l,u){const c=this.length;return this.resize(c+1),this.emplace(c,t,e,r,n,i,s,a,o,l,u)}emplace(t,e,r,n,i,s,a,o,l,u,c){const h=10*t;return this.uint16[h+0]=e,this.uint16[h+1]=r,this.uint16[h+2]=n,this.uint16[h+3]=i,this.uint16[h+4]=s,this.uint16[h+5]=a,this.uint16[h+6]=o,this.uint16[h+7]=l,this.uint16[h+8]=u,this.uint16[h+9]=c,t}}ms.prototype.bytesPerElement=20,wi("StructArrayLayout10ui20",ms);class gs extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i,s,a,o,l,u,c,h){const p=this.length;return this.resize(p+1),this.emplace(p,t,e,r,n,i,s,a,o,l,u,c,h)}emplace(t,e,r,n,i,s,a,o,l,u,c,h,p){const f=12*t;return this.int16[f+0]=e,this.int16[f+1]=r,this.int16[f+2]=n,this.int16[f+3]=i,this.uint16[f+4]=s,this.uint16[f+5]=a,this.uint16[f+6]=o,this.uint16[f+7]=l,this.int16[f+8]=u,this.int16[f+9]=c,this.int16[f+10]=h,this.int16[f+11]=p,t}}gs.prototype.bytesPerElement=24,wi("StructArrayLayout4i4ui4i24",gs);class xs extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.float32=new Float32Array(this.arrayBuffer);}emplaceBack(t,e,r){const n=this.length;return this.resize(n+1),this.emplace(n,t,e,r)}emplace(t,e,r,n){const i=3*t;return this.float32[i+0]=e,this.float32[i+1]=r,this.float32[i+2]=n,t}}xs.prototype.bytesPerElement=12,wi("StructArrayLayout3f12",xs);class vs extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.uint32=new Uint32Array(this.arrayBuffer);}emplaceBack(t){const e=this.length;return this.resize(e+1),this.emplace(e,t)}emplace(t,e){return this.uint32[1*t+0]=e,t}}vs.prototype.bytesPerElement=4,wi("StructArrayLayout1ul4",vs);class bs extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer),this.uint32=new Uint32Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i,s,a,o,l){const u=this.length;return this.resize(u+1),this.emplace(u,t,e,r,n,i,s,a,o,l)}emplace(t,e,r,n,i,s,a,o,l,u){const c=10*t,h=5*t;return this.int16[c+0]=e,this.int16[c+1]=r,this.int16[c+2]=n,this.int16[c+3]=i,this.int16[c+4]=s,this.int16[c+5]=a,this.uint32[h+3]=o,this.uint16[c+8]=l,this.uint16[c+9]=u,t}}bs.prototype.bytesPerElement=20,wi("StructArrayLayout6i1ul2ui20",bs);class ws extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i,s){const a=this.length;return this.resize(a+1),this.emplace(a,t,e,r,n,i,s)}emplace(t,e,r,n,i,s,a){const o=6*t;return this.int16[o+0]=e,this.int16[o+1]=r,this.int16[o+2]=n,this.int16[o+3]=i,this.int16[o+4]=s,this.int16[o+5]=a,t}}ws.prototype.bytesPerElement=12,wi("StructArrayLayout2i2i2i12",ws);class _s extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.float32=new Float32Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i){const s=this.length;return this.resize(s+1),this.emplace(s,t,e,r,n,i)}emplace(t,e,r,n,i,s){const a=4*t,o=8*t;return this.float32[a+0]=e,this.float32[a+1]=r,this.float32[a+2]=n,this.int16[o+6]=i,this.int16[o+7]=s,t}}_s.prototype.bytesPerElement=16,wi("StructArrayLayout2f1f2i16",_s);class As extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.float32=new Float32Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i,s){const a=this.length;return this.resize(a+1),this.emplace(a,t,e,r,n,i,s)}emplace(t,e,r,n,i,s,a){const o=16*t,l=4*t,u=8*t;return this.uint8[o+0]=e,this.uint8[o+1]=r,this.float32[l+1]=n,this.float32[l+2]=i,this.int16[u+6]=s,this.int16[u+7]=a,t}}As.prototype.bytesPerElement=16,wi("StructArrayLayout2ub2f2i16",As);class Ss extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer);}emplaceBack(t,e,r){const n=this.length;return this.resize(n+1),this.emplace(n,t,e,r)}emplace(t,e,r,n){const i=3*t;return this.uint16[i+0]=e,this.uint16[i+1]=r,this.uint16[i+2]=n,t}}Ss.prototype.bytesPerElement=6,wi("StructArrayLayout3ui6",Ss);class ks extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer),this.uint32=new Uint32Array(this.arrayBuffer),this.float32=new Float32Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i,s,a,o,l,u,c,h,p,f,d,y,m){const g=this.length;return this.resize(g+1),this.emplace(g,t,e,r,n,i,s,a,o,l,u,c,h,p,f,d,y,m)}emplace(t,e,r,n,i,s,a,o,l,u,c,h,p,f,d,y,m,g){const x=24*t,v=12*t,b=48*t;return this.int16[x+0]=e,this.int16[x+1]=r,this.uint16[x+2]=n,this.uint16[x+3]=i,this.uint32[v+2]=s,this.uint32[v+3]=a,this.uint32[v+4]=o,this.uint16[x+10]=l,this.uint16[x+11]=u,this.uint16[x+12]=c,this.float32[v+7]=h,this.float32[v+8]=p,this.uint8[b+36]=f,this.uint8[b+37]=d,this.uint8[b+38]=y,this.uint32[v+10]=m,this.int16[x+22]=g,t}}ks.prototype.bytesPerElement=48,wi("StructArrayLayout2i2ui3ul3ui2f3ub1ul1i48",ks);class Ms extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer),this.uint32=new Uint32Array(this.arrayBuffer),this.float32=new Float32Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i,s,a,o,l,u,c,h,p,f,d,y,m,g,x,v,b,w,_,A,S,k,M,I){const z=this.length;return this.resize(z+1),this.emplace(z,t,e,r,n,i,s,a,o,l,u,c,h,p,f,d,y,m,g,x,v,b,w,_,A,S,k,M,I)}emplace(t,e,r,n,i,s,a,o,l,u,c,h,p,f,d,y,m,g,x,v,b,w,_,A,S,k,M,I,z){const P=32*t,C=16*t;return this.int16[P+0]=e,this.int16[P+1]=r,this.int16[P+2]=n,this.int16[P+3]=i,this.int16[P+4]=s,this.int16[P+5]=a,this.int16[P+6]=o,this.int16[P+7]=l,this.uint16[P+8]=u,this.uint16[P+9]=c,this.uint16[P+10]=h,this.uint16[P+11]=p,this.uint16[P+12]=f,this.uint16[P+13]=d,this.uint16[P+14]=y,this.uint16[P+15]=m,this.uint16[P+16]=g,this.uint16[P+17]=x,this.uint16[P+18]=v,this.uint16[P+19]=b,this.uint16[P+20]=w,this.uint16[P+21]=_,this.uint16[P+22]=A,this.uint32[C+12]=S,this.float32[C+13]=k,this.float32[C+14]=M,this.uint16[P+30]=I,this.uint16[P+31]=z,t}}Ms.prototype.bytesPerElement=64,wi("StructArrayLayout8i15ui1ul2f2ui64",Ms);class Is extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.float32=new Float32Array(this.arrayBuffer);}emplaceBack(t){const e=this.length;return this.resize(e+1),this.emplace(e,t)}emplace(t,e){return this.float32[1*t+0]=e,t}}Is.prototype.bytesPerElement=4,wi("StructArrayLayout1f4",Is);class zs extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer),this.float32=new Float32Array(this.arrayBuffer);}emplaceBack(t,e,r){const n=this.length;return this.resize(n+1),this.emplace(n,t,e,r)}emplace(t,e,r,n){const i=3*t;return this.uint16[6*t+0]=e,this.float32[i+1]=r,this.float32[i+2]=n,t}}zs.prototype.bytesPerElement=12,wi("StructArrayLayout1ui2f12",zs);class Ps extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.uint32=new Uint32Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer);}emplaceBack(t,e,r){const n=this.length;return this.resize(n+1),this.emplace(n,t,e,r)}emplace(t,e,r,n){const i=4*t;return this.uint32[2*t+0]=e,this.uint16[i+2]=r,this.uint16[i+3]=n,t}}Ps.prototype.bytesPerElement=8,wi("StructArrayLayout1ul2ui8",Ps);class Cs extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer);}emplaceBack(t,e){const r=this.length;return this.resize(r+1),this.emplace(r,t,e)}emplace(t,e,r){const n=2*t;return this.uint16[n+0]=e,this.uint16[n+1]=r,t}}Cs.prototype.bytesPerElement=4,wi("StructArrayLayout2ui4",Cs);class Bs extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer);}emplaceBack(t){const e=this.length;return this.resize(e+1),this.emplace(e,t)}emplace(t,e){return this.uint16[1*t+0]=e,t}}Bs.prototype.bytesPerElement=2,wi("StructArrayLayout1ui2",Bs);class Vs extends os{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.float32=new Float32Array(this.arrayBuffer);}emplaceBack(t,e,r,n){const i=this.length;return this.resize(i+1),this.emplace(i,t,e,r,n)}emplace(t,e,r,n,i){const s=4*t;return this.float32[s+0]=e,this.float32[s+1]=r,this.float32[s+2]=n,this.float32[s+3]=i,t}}Vs.prototype.bytesPerElement=16,wi("StructArrayLayout4f16",Vs);class Es extends as{get anchorPointX(){return this._structArray.int16[this._pos2+0]}get anchorPointY(){return this._structArray.int16[this._pos2+1]}get x1(){return this._structArray.int16[this._pos2+2]}get y1(){return this._structArray.int16[this._pos2+3]}get x2(){return this._structArray.int16[this._pos2+4]}get y2(){return this._structArray.int16[this._pos2+5]}get featureIndex(){return this._structArray.uint32[this._pos4+3]}get sourceLayerIndex(){return this._structArray.uint16[this._pos2+8]}get bucketIndex(){return this._structArray.uint16[this._pos2+9]}get anchorPoint(){return new s(this.anchorPointX,this.anchorPointY)}}Es.prototype.size=20;class Fs extends bs{get(t){return new Es(this,t)}}wi("CollisionBoxArray",Fs);class Ts extends as{get anchorX(){return this._structArray.int16[this._pos2+0]}get anchorY(){return this._structArray.int16[this._pos2+1]}get glyphStartIndex(){return this._structArray.uint16[this._pos2+2]}get numGlyphs(){return this._structArray.uint16[this._pos2+3]}get vertexStartIndex(){return this._structArray.uint32[this._pos4+2]}get lineStartIndex(){return this._structArray.uint32[this._pos4+3]}get lineLength(){return this._structArray.uint32[this._pos4+4]}get segment(){return this._structArray.uint16[this._pos2+10]}get lowerSize(){return this._structArray.uint16[this._pos2+11]}get upperSize(){return this._structArray.uint16[this._pos2+12]}get lineOffsetX(){return this._structArray.float32[this._pos4+7]}get lineOffsetY(){return this._structArray.float32[this._pos4+8]}get writingMode(){return this._structArray.uint8[this._pos1+36]}get placedOrientation(){return this._structArray.uint8[this._pos1+37]}set placedOrientation(t){this._structArray.uint8[this._pos1+37]=t;}get hidden(){return this._structArray.uint8[this._pos1+38]}set hidden(t){this._structArray.uint8[this._pos1+38]=t;}get crossTileID(){return this._structArray.uint32[this._pos4+10]}set crossTileID(t){this._structArray.uint32[this._pos4+10]=t;}get associatedIconIndex(){return this._structArray.int16[this._pos2+22]}}Ts.prototype.size=48;class $s extends ks{get(t){return new Ts(this,t)}}wi("PlacedSymbolArray",$s);class Ls extends as{get anchorX(){return this._structArray.int16[this._pos2+0]}get anchorY(){return this._structArray.int16[this._pos2+1]}get rightJustifiedTextSymbolIndex(){return this._structArray.int16[this._pos2+2]}get centerJustifiedTextSymbolIndex(){return this._structArray.int16[this._pos2+3]}get leftJustifiedTextSymbolIndex(){return this._structArray.int16[this._pos2+4]}get verticalPlacedTextSymbolIndex(){return this._structArray.int16[this._pos2+5]}get placedIconSymbolIndex(){return this._structArray.int16[this._pos2+6]}get verticalPlacedIconSymbolIndex(){return this._structArray.int16[this._pos2+7]}get key(){return this._structArray.uint16[this._pos2+8]}get textBoxStartIndex(){return this._structArray.uint16[this._pos2+9]}get textBoxEndIndex(){return this._structArray.uint16[this._pos2+10]}get verticalTextBoxStartIndex(){return this._structArray.uint16[this._pos2+11]}get verticalTextBoxEndIndex(){return this._structArray.uint16[this._pos2+12]}get iconBoxStartIndex(){return this._structArray.uint16[this._pos2+13]}get iconBoxEndIndex(){return this._structArray.uint16[this._pos2+14]}get verticalIconBoxStartIndex(){return this._structArray.uint16[this._pos2+15]}get verticalIconBoxEndIndex(){return this._structArray.uint16[this._pos2+16]}get featureIndex(){return this._structArray.uint16[this._pos2+17]}get numHorizontalGlyphVertices(){return this._structArray.uint16[this._pos2+18]}get numVerticalGlyphVertices(){return this._structArray.uint16[this._pos2+19]}get numIconVertices(){return this._structArray.uint16[this._pos2+20]}get numVerticalIconVertices(){return this._structArray.uint16[this._pos2+21]}get useRuntimeCollisionCircles(){return this._structArray.uint16[this._pos2+22]}get crossTileID(){return this._structArray.uint32[this._pos4+12]}set crossTileID(t){this._structArray.uint32[this._pos4+12]=t;}get textBoxScale(){return this._structArray.float32[this._pos4+13]}get collisionCircleDiameter(){return this._structArray.float32[this._pos4+14]}get textAnchorOffsetStartIndex(){return this._structArray.uint16[this._pos2+30]}get textAnchorOffsetEndIndex(){return this._structArray.uint16[this._pos2+31]}}Ls.prototype.size=64;class Ds extends Ms{get(t){return new Ls(this,t)}}wi("SymbolInstanceArray",Ds);class Os extends Is{getoffsetX(t){return this.float32[1*t+0]}}wi("GlyphOffsetArray",Os);class js extends hs{getx(t){return this.int16[3*t+0]}gety(t){return this.int16[3*t+1]}gettileUnitDistanceFromAnchor(t){return this.int16[3*t+2]}}wi("SymbolLineVertexArray",js);class Rs extends as{get textAnchor(){return this._structArray.uint16[this._pos2+0]}get textOffset0(){return this._structArray.float32[this._pos4+1]}get textOffset1(){return this._structArray.float32[this._pos4+2]}}Rs.prototype.size=12;class Us extends zs{get(t){return new Rs(this,t)}}wi("TextAnchorOffsetArray",Us);class qs extends as{get featureIndex(){return this._structArray.uint32[this._pos4+0]}get sourceLayerIndex(){return this._structArray.uint16[this._pos2+2]}get bucketIndex(){return this._structArray.uint16[this._pos2+3]}}qs.prototype.size=8;class Ns extends Ps{get(t){return new qs(this,t)}}wi("FeatureIndexArray",Ns);class Zs extends cs{}class Gs extends cs{}class Ks extends cs{}class Xs extends fs{}class Hs extends ds{}class Ys extends ys{}class Js extends ms{}class Ws extends gs{}class Qs extends xs{}class ta extends vs{}class ea extends ws{}class ra extends As{}class na extends Ss{}class ia extends Cs{}const sa=ls([{name:"a_pos",components:2,type:"Int16"}],4),{members:aa}=sa;class oa{constructor(t=[]){this.segments=t;}prepareSegment(t,e,r,n){let i=this.segments[this.segments.length-1];return t>oa.MAX_VERTEX_ARRAY_LENGTH&&A(`Max vertices per segment is ${oa.MAX_VERTEX_ARRAY_LENGTH}: bucket requested ${t}`),(!i||i.vertexLength+t>oa.MAX_VERTEX_ARRAY_LENGTH||i.sortKey!==n)&&(i={vertexOffset:e.length,primitiveOffset:r.length,vertexLength:0,primitiveLength:0},void 0!==n&&(i.sortKey=n),this.segments.push(i)),i}get(){return this.segments}destroy(){for(const t of this.segments)for(const e in t.vaos)t.vaos[e].destroy();}static simpleSegment(t,e,r,n){return new oa([{vertexOffset:t,primitiveOffset:e,vertexLength:r,primitiveLength:n,vaos:{},sortKey:0}])}}function la(t,e){return 256*(t=y(Math.floor(t),0,255))+y(Math.floor(e),0,255)}oa.MAX_VERTEX_ARRAY_LENGTH=Math.pow(2,16)-1,wi("SegmentVector",oa);const ua=ls([{name:"a_pattern_from",components:4,type:"Uint16"},{name:"a_pattern_to",components:4,type:"Uint16"},{name:"a_pixel_ratio_from",components:1,type:"Uint16"},{name:"a_pixel_ratio_to",components:1,type:"Uint16"}]);var ca={exports:{}},ha={exports:{}};ha.exports=function(t,e){var r,n,i,s,a,o,l,u;for(n=t.length-(r=3&t.length),i=e,a=3432918353,o=461845907,u=0;u>>16)*a&65535)<<16)&4294967295)<<15|l>>>17))*o+(((l>>>16)*o&65535)<<16)&4294967295)<<13|i>>>19))+((5*(i>>>16)&65535)<<16)&4294967295))+((58964+(s>>>16)&65535)<<16);switch(l=0,r){case 3:l^=(255&t.charCodeAt(u+2))<<16;case 2:l^=(255&t.charCodeAt(u+1))<<8;case 1:i^=l=(65535&(l=(l=(65535&(l^=255&t.charCodeAt(u)))*a+(((l>>>16)*a&65535)<<16)&4294967295)<<15|l>>>17))*o+(((l>>>16)*o&65535)<<16)&4294967295;}return i^=t.length,i=2246822507*(65535&(i^=i>>>16))+((2246822507*(i>>>16)&65535)<<16)&4294967295,i=3266489909*(65535&(i^=i>>>13))+((3266489909*(i>>>16)&65535)<<16)&4294967295,(i^=i>>>16)>>>0};var pa=ha.exports,fa={exports:{}};fa.exports=function(t,e){for(var r,n=t.length,i=e^n,s=0;n>=4;)r=1540483477*(65535&(r=255&t.charCodeAt(s)|(255&t.charCodeAt(++s))<<8|(255&t.charCodeAt(++s))<<16|(255&t.charCodeAt(++s))<<24))+((1540483477*(r>>>16)&65535)<<16),i=1540483477*(65535&i)+((1540483477*(i>>>16)&65535)<<16)^(r=1540483477*(65535&(r^=r>>>24))+((1540483477*(r>>>16)&65535)<<16)),n-=4,++s;switch(n){case 3:i^=(255&t.charCodeAt(s+2))<<16;case 2:i^=(255&t.charCodeAt(s+1))<<8;case 1:i=1540483477*(65535&(i^=255&t.charCodeAt(s)))+((1540483477*(i>>>16)&65535)<<16);}return i=1540483477*(65535&(i^=i>>>13))+((1540483477*(i>>>16)&65535)<<16),(i^=i>>>15)>>>0};var da=pa,ya=fa.exports;ca.exports=da,ca.exports.murmur3=da,ca.exports.murmur2=ya;var ma=r(ca.exports);class ga{constructor(){this.ids=[],this.positions=[],this.indexed=!1;}add(t,e,r,n){this.ids.push(xa(t)),this.positions.push(e,r,n);}getPositions(t){if(!this.indexed)throw new Error("Trying to get index, but feature positions are not indexed");const e=xa(t);let r=0,n=this.ids.length-1;for(;r>1;this.ids[t]>=e?n=t:r=t+1;}const i=[];for(;this.ids[r]===e;)i.push({index:this.positions[3*r],start:this.positions[3*r+1],end:this.positions[3*r+2]}),r++;return i}static serialize(t,e){const r=new Float64Array(t.ids),n=new Uint32Array(t.positions);return va(r,n,0,r.length-1),e&&e.push(r.buffer,n.buffer),{ids:r,positions:n}}static deserialize(t){const e=new ga;return e.ids=t.ids,e.positions=t.positions,e.indexed=!0,e}}function xa(t){const e=+t;return !isNaN(e)&&e<=Number.MAX_SAFE_INTEGER?e:ma(String(t))}function va(t,e,r,n){for(;r>1];let s=r-1,a=n+1;for(;;){do{s++;}while(t[s]i);if(s>=a)break;ba(t,s,a),ba(e,3*s,3*a),ba(e,3*s+1,3*a+1),ba(e,3*s+2,3*a+2);}a-r`u_${t}`)),this.type=r;}setUniform(t,e,r){t.set(r.constantOr(this.value));}getBinding(t,e,r){return "color"===this.type?new Sa(t,e):new _a(t,e)}}class za{constructor(t,e){this.uniformNames=e.map((t=>`u_${t}`)),this.patternFrom=null,this.patternTo=null,this.pixelRatioFrom=1,this.pixelRatioTo=1;}setConstantPatternPositions(t,e){this.pixelRatioFrom=e.pixelRatio,this.pixelRatioTo=t.pixelRatio,this.patternFrom=e.tlbr,this.patternTo=t.tlbr;}setUniform(t,e,r,n){const i="u_pattern_to"===n?this.patternTo:"u_pattern_from"===n?this.patternFrom:"u_pixel_ratio_to"===n?this.pixelRatioTo:"u_pixel_ratio_from"===n?this.pixelRatioFrom:null;i&&t.set(i);}getBinding(t,e,r){return "u_pattern"===r.substr(0,9)?new Aa(t,e):new _a(t,e)}}class Pa{constructor(t,e,r,n){this.expression=t,this.type=r,this.maxValue=0,this.paintVertexAttributes=e.map((t=>({name:`a_${t}`,type:"Float32",components:"color"===r?2:1,offset:0}))),this.paintVertexArray=new n;}populatePaintArray(t,e,r,n,i){const s=this.paintVertexArray.length,a=this.expression.evaluate(new Ui(0),e,{},n,[],i);this.paintVertexArray.resize(t),this._setPaintValue(s,t,a);}updatePaintArray(t,e,r,n){const i=this.expression.evaluate({zoom:0},r,n);this._setPaintValue(t,e,i);}_setPaintValue(t,e,r){if("color"===this.type){const n=Ma(r);for(let r=t;r`u_${t}_t`)),this.type=r,this.useIntegerZoom=n,this.zoom=i,this.maxValue=0,this.paintVertexAttributes=e.map((t=>({name:`a_${t}`,type:"Float32",components:"color"===r?4:2,offset:0}))),this.paintVertexArray=new s;}populatePaintArray(t,e,r,n,i){const s=this.expression.evaluate(new Ui(this.zoom),e,{},n,[],i),a=this.expression.evaluate(new Ui(this.zoom+1),e,{},n,[],i),o=this.paintVertexArray.length;this.paintVertexArray.resize(t),this._setPaintValue(o,t,s,a);}updatePaintArray(t,e,r,n){const i=this.expression.evaluate({zoom:this.zoom},r,n),s=this.expression.evaluate({zoom:this.zoom+1},r,n);this._setPaintValue(t,e,i,s);}_setPaintValue(t,e,r,n){if("color"===this.type){const i=Ma(r),s=Ma(n);for(let r=t;r`#define HAS_UNIFORM_${t}`)));}return t}getBinderAttributes(){const t=[];for(const e in this.binders){const r=this.binders[e];if(r instanceof Pa||r instanceof Ca)for(let e=0;e!0)){this.programConfigurations={};for(const n of t)this.programConfigurations[n.id]=new Va(n,e,r);this.needsUpload=!1,this._featureMap=new ga,this._bufferOffset=0;}populatePaintArrays(t,e,r,n,i,s){for(const r in this.programConfigurations)this.programConfigurations[r].populatePaintArrays(t,e,n,i,s);void 0!==e.id&&this._featureMap.add(e.id,r,this._bufferOffset,t),this._bufferOffset=t,this.needsUpload=!0;}updatePaintArrays(t,e,r,n){for(const i of r)this.needsUpload=this.programConfigurations[i.id].updatePaintArrays(t,this._featureMap,e,i,n)||this.needsUpload;}get(t){return this.programConfigurations[t]}upload(t){if(this.needsUpload){for(const e in this.programConfigurations)this.programConfigurations[e].upload(t);this.needsUpload=!1;}}destroy(){for(const t in this.programConfigurations)this.programConfigurations[t].destroy();}}function Fa(t,e){return {"text-opacity":["opacity"],"icon-opacity":["opacity"],"text-color":["fill_color"],"icon-color":["fill_color"],"text-halo-color":["halo_color"],"icon-halo-color":["halo_color"],"text-halo-blur":["halo_blur"],"icon-halo-blur":["halo_blur"],"text-halo-width":["halo_width"],"icon-halo-width":["halo_width"],"line-gap-width":["gapwidth"],"line-pattern":["pattern_to","pattern_from","pixel_ratio_to","pixel_ratio_from"],"fill-pattern":["pattern_to","pattern_from","pixel_ratio_to","pixel_ratio_from"],"fill-extrusion-pattern":["pattern_to","pattern_from","pixel_ratio_to","pixel_ratio_from"]}[t]||[t.replace(`${e}-`,"").replace(/-/g,"_")]}function Ta(t,e,r){const n={color:{source:ys,composite:Vs},number:{source:Is,composite:ys}},i=function(t){return {"line-pattern":{source:Js,composite:Js},"fill-pattern":{source:Js,composite:Js},"fill-extrusion-pattern":{source:Js,composite:Js}}[t]}(t);return i&&i[r]||n[e][r]}wi("ConstantBinder",Ia),wi("CrossFadedConstantBinder",za),wi("SourceExpressionBinder",Pa),wi("CrossFadedCompositeBinder",Ba),wi("CompositeExpressionBinder",Ca),wi("ProgramConfiguration",Va,{omit:["_buffers"]}),wi("ProgramConfigurationSet",Ea);const $a=8192,La=Math.pow(2,14)-1,Da=-La-1;function Oa(t){const e=$a/t.extent,r=t.loadGeometry();for(let t=0;tr.x+1||sr.y+1)&&A("Geometry exceeds allowed extent, reduce your vector tile buffer size");}}return r}function ja(t,e){return {type:t.type,id:t.id,properties:t.properties,geometry:e?Oa(t):[]}}function Ra(t,e,r,n,i){t.emplaceBack(2*e+(n+1)/2,2*r+(i+1)/2);}class Ua{constructor(t){this.zoom=t.zoom,this.overscaling=t.overscaling,this.layers=t.layers,this.layerIds=this.layers.map((t=>t.id)),this.index=t.index,this.hasPattern=!1,this.layoutVertexArray=new Gs,this.indexArray=new na,this.segments=new oa,this.programConfigurations=new Ea(t.layers,t.zoom),this.stateDependentLayerIds=this.layers.filter((t=>t.isStateDependent())).map((t=>t.id));}populate(t,e,r){const n=this.layers[0],i=[];let s=null,a=!1;"circle"===n.type&&(s=n.layout.get("circle-sort-key"),a=!s.isConstant());for(const{feature:e,id:n,index:o,sourceLayerIndex:l}of t){const t=this.layers[0]._featureFilter.needGeometry,u=ja(e,t);if(!this.layers[0]._featureFilter.filter(new Ui(this.zoom),u,r))continue;const c=a?s.evaluate(u,{},r):void 0,h={id:n,properties:e.properties,type:e.type,sourceLayerIndex:l,index:o,geometry:t?u.geometry:Oa(e),patterns:{},sortKey:c};i.push(h);}a&&i.sort(((t,e)=>t.sortKey-e.sortKey));for(const n of i){const{geometry:i,index:s,sourceLayerIndex:a}=n,o=t[s].feature;this.addFeature(n,i,s,r),e.featureIndex.insert(o,i,s,a,this.index);}}update(t,e,r){this.stateDependentLayers.length&&this.programConfigurations.updatePaintArrays(t,e,this.stateDependentLayers,r);}isEmpty(){return 0===this.layoutVertexArray.length}uploadPending(){return !this.uploaded||this.programConfigurations.needsUpload}upload(t){this.uploaded||(this.layoutVertexBuffer=t.createVertexBuffer(this.layoutVertexArray,aa),this.indexBuffer=t.createIndexBuffer(this.indexArray)),this.programConfigurations.upload(t),this.uploaded=!0;}destroy(){this.layoutVertexBuffer&&(this.layoutVertexBuffer.destroy(),this.indexBuffer.destroy(),this.programConfigurations.destroy(),this.segments.destroy());}addFeature(t,e,r,n){for(const r of e)for(const e of r){const r=e.x,n=e.y;if(r<0||r>=$a||n<0||n>=$a)continue;const i=this.segments.prepareSegment(4,this.layoutVertexArray,this.indexArray,t.sortKey),s=i.vertexLength;Ra(this.layoutVertexArray,r,n,-1,-1),Ra(this.layoutVertexArray,r,n,1,-1),Ra(this.layoutVertexArray,r,n,1,1),Ra(this.layoutVertexArray,r,n,-1,1),this.indexArray.emplaceBack(s,s+1,s+2),this.indexArray.emplaceBack(s,s+3,s+2),i.vertexLength+=4,i.primitiveLength+=2;}this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length,t,r,{},n);}}function qa(t,e){for(let r=0;r1){if(Ka(t,e))return !0;for(let n=0;n1?r:r.sub(e)._mult(i)._add(e))}function Ja(t,e){let r,n,i,s=!1;for(let a=0;ae.y!=i.y>e.y&&e.x<(i.x-n.x)*(e.y-n.y)/(i.y-n.y)+n.x&&(s=!s);}return s}function Wa(t,e){let r=!1;for(let n=0,i=t.length-1;ne.y!=a.y>e.y&&e.x<(a.x-s.x)*(e.y-s.y)/(a.y-s.y)+s.x&&(r=!r);}return r}function Qa(t,e,r){const n=r[0],i=r[2];if(t.xi.x&&e.x>i.x||t.yi.y&&e.y>i.y)return !1;const s=S(t,e,r[0]);return s!==S(t,e,r[1])||s!==S(t,e,r[2])||s!==S(t,e,r[3])}function to(t,e,r){const n=e.paint.get(t).value;return "constant"===n.kind?n.value:r.programConfigurations.get(e.id).getMaxValue(t)}function eo(t){return Math.sqrt(t[0]*t[0]+t[1]*t[1])}function ro(t,e,r,n,i){if(!e[0]&&!e[1])return t;const a=s.convert(e)._mult(i);"viewport"===r&&a._rotate(-n);const o=[];for(let e=0;eyo(t,e)))}(l,o),p=c?u*a:u;for(const t of n)for(const e of t){const t=c?e:yo(e,o);let r=p;const n=po([],[e.x,e.y,0,1],o);if("viewport"===this.paint.get("circle-pitch-scale")&&"map"===this.paint.get("circle-pitch-alignment")?r*=n[3]/s.cameraToCenterDistance:"map"===this.paint.get("circle-pitch-scale")&&"viewport"===this.paint.get("circle-pitch-alignment")&&(r*=s.cameraToCenterDistance/n[3]),Na(h,t,r))return !0}return !1}}function yo(t,e){const r=po([],[t.x,t.y,0,1],e);return new s(r[0]/r[3],r[1]/r[3])}class mo extends Ua{}let go;wi("HeatmapBucket",mo,{omit:["layers"]});var xo={get paint(){return go=go||new rs({"heatmap-radius":new Wi(G.paint_heatmap["heatmap-radius"]),"heatmap-weight":new Wi(G.paint_heatmap["heatmap-weight"]),"heatmap-intensity":new Ji(G.paint_heatmap["heatmap-intensity"]),"heatmap-color":new es(G.paint_heatmap["heatmap-color"]),"heatmap-opacity":new Ji(G.paint_heatmap["heatmap-opacity"])})}};function vo(t,{width:e,height:r},n,i){if(i){if(i instanceof Uint8ClampedArray)i=new Uint8Array(i.buffer);else if(i.length!==e*r*n)throw new RangeError(`mismatched image size. expected: ${i.length} but got: ${e*r*n}`)}else i=new Uint8Array(e*r*n);return t.width=e,t.height=r,t.data=i,t}function bo(t,{width:e,height:r},n){if(e===t.width&&r===t.height)return;const i=vo({},{width:e,height:r},n);wo(t,i,{x:0,y:0},{x:0,y:0},{width:Math.min(t.width,e),height:Math.min(t.height,r)},n),t.width=e,t.height=r,t.data=i.data;}function wo(t,e,r,n,i,s){if(0===i.width||0===i.height)return e;if(i.width>t.width||i.height>t.height||r.x>t.width-i.width||r.y>t.height-i.height)throw new RangeError("out of range source coordinates for image copy");if(i.width>e.width||i.height>e.height||n.x>e.width-i.width||n.y>e.height-i.height)throw new RangeError("out of range destination coordinates for image copy");const a=t.data,o=e.data;if(a===o)throw new Error("srcData equals dstData, so image is already copied");for(let l=0;l{e[t.evaluationKey]=s;const a=t.expression.evaluate(e);i.data[r+n+0]=Math.floor(255*a.r/a.a),i.data[r+n+1]=Math.floor(255*a.g/a.a),i.data[r+n+2]=Math.floor(255*a.b/a.a),i.data[r+n+3]=Math.floor(255*a.a);};if(t.clips)for(let e=0,i=0;e80*r){o=1/0,l=1/0;let e=-1/0,n=-1/0;for(let s=r;se&&(e=r),i>n&&(n=i);}u=Math.max(e-o,n-l),u=0!==u?32767/u:0;}return To(s,a,r,o,l,u,0),a}function Eo(t,e,r,n,i){let s;if(i===function(t,e,r,n){let i=0;for(let s=e,a=r-n;s0)for(let i=e;i=e;i-=n)s=tl(i/n|0,t[i],t[i+1],s);return s&&Xo(s,s.next)&&(el(s),s=s.next),s}function Fo(t,e){if(!t)return t;e||(e=t);let r,n=t;do{if(r=!1,n.steiner||!Xo(n,n.next)&&0!==Ko(n.prev,n,n.next))n=n.next;else {if(el(n),n=e=n.prev,n===n.next)break;r=!0;}}while(r||n!==e);return e}function To(t,e,r,n,i,s,a){if(!t)return;!a&&s&&function(t,e,r,n){let i=t;do{0===i.z&&(i.z=qo(i.x,i.y,e,r,n)),i.prevZ=i.prev,i.nextZ=i.next,i=i.next;}while(i!==t);i.prevZ.nextZ=null,i.prevZ=null,function(t){let e,r=1;do{let n,i=t;t=null;let s=null;for(e=0;i;){e++;let a=i,o=0;for(let t=0;t0||l>0&&a;)0!==o&&(0===l||!a||i.z<=a.z)?(n=i,i=i.nextZ,o--):(n=a,a=a.nextZ,l--),s?s.nextZ=n:t=n,n.prevZ=s,s=n;i=a;}s.nextZ=null,r*=2;}while(e>1)}(i);}(t,n,i,s);let o=t;for(;t.prev!==t.next;){const l=t.prev,u=t.next;if(s?Lo(t,n,i,s):$o(t))e.push(l.i,t.i,u.i),el(t),t=u.next,o=u.next;else if((t=u)===o){a?1===a?To(t=Do(Fo(t),e),e,r,n,i,s,2):2===a&&Oo(t,e,r,n,i,s):To(Fo(t),e,r,n,i,s,1);break}}}function $o(t){const e=t.prev,r=t,n=t.next;if(Ko(e,r,n)>=0)return !1;const i=e.x,s=r.x,a=n.x,o=e.y,l=r.y,u=n.y,c=is?i>a?i:a:s>a?s:a,f=o>l?o>u?o:u:l>u?l:u;let d=n.next;for(;d!==e;){if(d.x>=c&&d.x<=p&&d.y>=h&&d.y<=f&&Zo(i,o,s,l,a,u,d.x,d.y)&&Ko(d.prev,d,d.next)>=0)return !1;d=d.next;}return !0}function Lo(t,e,r,n){const i=t.prev,s=t,a=t.next;if(Ko(i,s,a)>=0)return !1;const o=i.x,l=s.x,u=a.x,c=i.y,h=s.y,p=a.y,f=ol?o>u?o:u:l>u?l:u,m=c>h?c>p?c:p:h>p?h:p,g=qo(f,d,e,r,n),x=qo(y,m,e,r,n);let v=t.prevZ,b=t.nextZ;for(;v&&v.z>=g&&b&&b.z<=x;){if(v.x>=f&&v.x<=y&&v.y>=d&&v.y<=m&&v!==i&&v!==a&&Zo(o,c,l,h,u,p,v.x,v.y)&&Ko(v.prev,v,v.next)>=0)return !1;if(v=v.prevZ,b.x>=f&&b.x<=y&&b.y>=d&&b.y<=m&&b!==i&&b!==a&&Zo(o,c,l,h,u,p,b.x,b.y)&&Ko(b.prev,b,b.next)>=0)return !1;b=b.nextZ;}for(;v&&v.z>=g;){if(v.x>=f&&v.x<=y&&v.y>=d&&v.y<=m&&v!==i&&v!==a&&Zo(o,c,l,h,u,p,v.x,v.y)&&Ko(v.prev,v,v.next)>=0)return !1;v=v.prevZ;}for(;b&&b.z<=x;){if(b.x>=f&&b.x<=y&&b.y>=d&&b.y<=m&&b!==i&&b!==a&&Zo(o,c,l,h,u,p,b.x,b.y)&&Ko(b.prev,b,b.next)>=0)return !1;b=b.nextZ;}return !0}function Do(t,e){let r=t;do{const n=r.prev,i=r.next.next;!Xo(n,i)&&Ho(n,r,r.next,i)&&Wo(n,i)&&Wo(i,n)&&(e.push(n.i,r.i,i.i),el(r),el(r.next),r=t=i),r=r.next;}while(r!==t);return Fo(r)}function Oo(t,e,r,n,i,s){let a=t;do{let t=a.next.next;for(;t!==a.prev;){if(a.i!==t.i&&Go(a,t)){let o=Qo(a,t);return a=Fo(a,a.next),o=Fo(o,o.next),To(a,e,r,n,i,s,0),void To(o,e,r,n,i,s,0)}t=t.next;}a=a.next;}while(a!==t)}function jo(t,e){return t.x-e.x}function Ro(t,e){const r=function(t,e){let r=e;const n=t.x,i=t.y;let s,a=-1/0;do{if(i<=r.y&&i>=r.next.y&&r.next.y!==r.y){const t=r.x+(i-r.y)*(r.next.x-r.x)/(r.next.y-r.y);if(t<=n&&t>a&&(a=t,s=r.x=r.x&&r.x>=l&&n!==r.x&&Zo(is.x||r.x===s.x&&Uo(s,r)))&&(s=r,c=e);}r=r.next;}while(r!==o);return s}(t,e);if(!r)return e;const n=Qo(r,t);return Fo(n,n.next),Fo(r,r.next)}function Uo(t,e){return Ko(t.prev,t,e.prev)<0&&Ko(e.next,t,t.next)<0}function qo(t,e,r,n,i){return (t=1431655765&((t=858993459&((t=252645135&((t=16711935&((t=(t-r)*i|0)|t<<8))|t<<4))|t<<2))|t<<1))|(e=1431655765&((e=858993459&((e=252645135&((e=16711935&((e=(e-n)*i|0)|e<<8))|e<<4))|e<<2))|e<<1))<<1}function No(t){let e=t,r=t;do{(e.x=(t-a)*(s-o)&&(t-a)*(n-o)>=(r-a)*(e-o)&&(r-a)*(s-o)>=(i-a)*(n-o)}function Go(t,e){return t.next.i!==e.i&&t.prev.i!==e.i&&!function(t,e){let r=t;do{if(r.i!==t.i&&r.next.i!==t.i&&r.i!==e.i&&r.next.i!==e.i&&Ho(r,r.next,t,e))return !0;r=r.next;}while(r!==t);return !1}(t,e)&&(Wo(t,e)&&Wo(e,t)&&function(t,e){let r=t,n=!1;const i=(t.x+e.x)/2,s=(t.y+e.y)/2;do{r.y>s!=r.next.y>s&&r.next.y!==r.y&&i<(r.next.x-r.x)*(s-r.y)/(r.next.y-r.y)+r.x&&(n=!n),r=r.next;}while(r!==t);return n}(t,e)&&(Ko(t.prev,t,e.prev)||Ko(t,e.prev,e))||Xo(t,e)&&Ko(t.prev,t,t.next)>0&&Ko(e.prev,e,e.next)>0)}function Ko(t,e,r){return (e.y-t.y)*(r.x-e.x)-(e.x-t.x)*(r.y-e.y)}function Xo(t,e){return t.x===e.x&&t.y===e.y}function Ho(t,e,r,n){const i=Jo(Ko(t,e,r)),s=Jo(Ko(t,e,n)),a=Jo(Ko(r,n,t)),o=Jo(Ko(r,n,e));return i!==s&&a!==o||!(0!==i||!Yo(t,r,e))||!(0!==s||!Yo(t,n,e))||!(0!==a||!Yo(r,t,n))||!(0!==o||!Yo(r,e,n))}function Yo(t,e,r){return e.x<=Math.max(t.x,r.x)&&e.x>=Math.min(t.x,r.x)&&e.y<=Math.max(t.y,r.y)&&e.y>=Math.min(t.y,r.y)}function Jo(t){return t>0?1:t<0?-1:0}function Wo(t,e){return Ko(t.prev,t,t.next)<0?Ko(t,e,t.next)>=0&&Ko(t,t.prev,e)>=0:Ko(t,e,t.prev)<0||Ko(t,t.next,e)<0}function Qo(t,e){const r=rl(t.i,t.x,t.y),n=rl(e.i,e.x,e.y),i=t.next,s=e.prev;return t.next=e,e.prev=t,r.next=i,i.prev=r,n.next=r,r.prev=n,s.next=n,n.prev=s,n}function tl(t,e,r,n){const i=rl(t,e,r);return n?(i.next=n.next,i.prev=n,n.next.prev=i,n.next=i):(i.prev=i,i.next=i),i}function el(t){t.next.prev=t.prev,t.prev.next=t.next,t.prevZ&&(t.prevZ.nextZ=t.nextZ),t.nextZ&&(t.nextZ.prevZ=t.prevZ);}function rl(t,e,r){return {i:t,x:e,y:r,prev:null,next:null,z:0,prevZ:null,nextZ:null,steiner:!1}}function nl(t,e,r){const n=r.patternDependencies;let i=!1;for(const r of e){const e=r.paint.get(`${t}-pattern`);e.isConstant()||(i=!0);const s=e.constantOr(null);s&&(i=!0,n[s.to]=!0,n[s.from]=!0);}return i}function il(t,e,r,n,i){const s=i.patternDependencies;for(const a of e){const e=a.paint.get(`${t}-pattern`).value;if("constant"!==e.kind){let t=e.evaluate({zoom:n-1},r,{},i.availableImages),o=e.evaluate({zoom:n},r,{},i.availableImages),l=e.evaluate({zoom:n+1},r,{},i.availableImages);t=t&&t.name?t.name:t,o=o&&o.name?o.name:o,l=l&&l.name?l.name:l,s[t]=!0,s[o]=!0,s[l]=!0,r.patterns[a.id]={min:t,mid:o,max:l};}}return r}class sl{constructor(t){this.zoom=t.zoom,this.overscaling=t.overscaling,this.layers=t.layers,this.layerIds=this.layers.map((t=>t.id)),this.index=t.index,this.hasPattern=!1,this.patternFeatures=[],this.layoutVertexArray=new Ks,this.indexArray=new na,this.indexArray2=new ia,this.programConfigurations=new Ea(t.layers,t.zoom),this.segments=new oa,this.segments2=new oa,this.stateDependentLayerIds=this.layers.filter((t=>t.isStateDependent())).map((t=>t.id));}populate(t,e,r){this.hasPattern=nl("fill",this.layers,e);const n=this.layers[0].layout.get("fill-sort-key"),i=!n.isConstant(),s=[];for(const{feature:a,id:o,index:l,sourceLayerIndex:u}of t){const t=this.layers[0]._featureFilter.needGeometry,c=ja(a,t);if(!this.layers[0]._featureFilter.filter(new Ui(this.zoom),c,r))continue;const h=i?n.evaluate(c,{},r,e.availableImages):void 0,p={id:o,properties:a.properties,type:a.type,sourceLayerIndex:u,index:l,geometry:t?c.geometry:Oa(a),patterns:{},sortKey:h};s.push(p);}i&&s.sort(((t,e)=>t.sortKey-e.sortKey));for(const n of s){const{geometry:i,index:s,sourceLayerIndex:a}=n;if(this.hasPattern){const t=il("fill",this.layers,n,this.zoom,e);this.patternFeatures.push(t);}else this.addFeature(n,i,s,r,{});e.featureIndex.insert(t[s].feature,i,s,a,this.index);}}update(t,e,r){this.stateDependentLayers.length&&this.programConfigurations.updatePaintArrays(t,e,this.stateDependentLayers,r);}addFeatures(t,e,r){for(const t of this.patternFeatures)this.addFeature(t,t.geometry,t.index,e,r);}isEmpty(){return 0===this.layoutVertexArray.length}uploadPending(){return !this.uploaded||this.programConfigurations.needsUpload}upload(t){this.uploaded||(this.layoutVertexBuffer=t.createVertexBuffer(this.layoutVertexArray,Bo),this.indexBuffer=t.createIndexBuffer(this.indexArray),this.indexBuffer2=t.createIndexBuffer(this.indexArray2)),this.programConfigurations.upload(t),this.uploaded=!0;}destroy(){this.layoutVertexBuffer&&(this.layoutVertexBuffer.destroy(),this.indexBuffer.destroy(),this.indexBuffer2.destroy(),this.programConfigurations.destroy(),this.segments.destroy(),this.segments2.destroy());}addFeature(t,e,r,n,i){for(const t of xr(e,500)){let e=0;for(const r of t)e+=r.length;const r=this.segments.prepareSegment(e,this.layoutVertexArray,this.indexArray),n=r.vertexLength,i=[],s=[];for(const e of t){if(0===e.length)continue;e!==t[0]&&s.push(i.length/2);const r=this.segments2.prepareSegment(e.length,this.layoutVertexArray,this.indexArray2),n=r.vertexLength;this.layoutVertexArray.emplaceBack(e[0].x,e[0].y),this.indexArray2.emplaceBack(n+e.length-1,n),i.push(e[0].x),i.push(e[0].y);for(let t=1;t>3;}if(i--,1===n||2===n)s+=t.readSVarint(),a+=t.readSVarint(),1===n&&(e&&o.push(e),e=[]),e.push(new dl(s,a));else {if(7!==n)throw new Error("unknown command "+n);e&&e.push(e[0].clone());}}return e&&o.push(e),o},ml.prototype.bbox=function(){var t=this._pbf;t.pos=this._geometry;for(var e=t.readVarint()+t.pos,r=1,n=0,i=0,s=0,a=1/0,o=-1/0,l=1/0,u=-1/0;t.pos>3;}if(n--,1===r||2===r)(i+=t.readSVarint())o&&(o=i),(s+=t.readSVarint())u&&(u=s);else if(7!==r)throw new Error("unknown command "+r)}return [a,l,o,u]},ml.prototype.toGeoJSON=function(t,e,r){var n,i,s=this.extent*Math.pow(2,r),a=this.extent*t,o=this.extent*e,l=this.loadGeometry(),u=ml.types[this.type];function c(t){for(var e=0;e>3;e=1===n?t.readString():2===n?t.readFloat():3===n?t.readDouble():4===n?t.readVarint64():5===n?t.readVarint():6===n?t.readSVarint():7===n?t.readBoolean():null;}return e}(r));}wl.prototype.feature=function(t){if(t<0||t>=this._features.length)throw new Error("feature index out of bounds");this._pbf.pos=this._features[t];var e=this._pbf.readVarint()+this._pbf.pos;return new vl(this._pbf,e,this.extent,this._keys,this._values)};var Al=bl;function Sl(t,e,r){if(3===t){var n=new Al(r,r.readVarint()+r.pos);n.length&&(e[n.name]=n);}}fl.VectorTile=function(t,e){this.layers=t.readFields(Sl,{},e);},fl.VectorTileFeature=yl,fl.VectorTileLayer=bl;const kl=fl.VectorTileFeature.types,Ml=Math.pow(2,13);function Il(t,e,r,n,i,s,a,o){t.emplaceBack(e,r,2*Math.floor(n*Ml)+a,i*Ml*2,s*Ml*2,Math.round(o));}class zl{constructor(t){this.zoom=t.zoom,this.overscaling=t.overscaling,this.layers=t.layers,this.layerIds=this.layers.map((t=>t.id)),this.index=t.index,this.hasPattern=!1,this.layoutVertexArray=new Xs,this.centroidVertexArray=new Zs,this.indexArray=new na,this.programConfigurations=new Ea(t.layers,t.zoom),this.segments=new oa,this.stateDependentLayerIds=this.layers.filter((t=>t.isStateDependent())).map((t=>t.id));}populate(t,e,r){this.features=[],this.hasPattern=nl("fill-extrusion",this.layers,e);for(const{feature:n,id:i,index:s,sourceLayerIndex:a}of t){const t=this.layers[0]._featureFilter.needGeometry,o=ja(n,t);if(!this.layers[0]._featureFilter.filter(new Ui(this.zoom),o,r))continue;const l={id:i,sourceLayerIndex:a,index:s,geometry:t?o.geometry:Oa(n),properties:n.properties,type:n.type,patterns:{}};this.hasPattern?this.features.push(il("fill-extrusion",this.layers,l,this.zoom,e)):this.addFeature(l,l.geometry,s,r,{}),e.featureIndex.insert(n,l.geometry,s,a,this.index,!0);}}addFeatures(t,e,r){for(const t of this.features){const{geometry:n}=t;this.addFeature(t,n,t.index,e,r);}}update(t,e,r){this.stateDependentLayers.length&&this.programConfigurations.updatePaintArrays(t,e,this.stateDependentLayers,r);}isEmpty(){return 0===this.layoutVertexArray.length&&0===this.centroidVertexArray.length}uploadPending(){return !this.uploaded||this.programConfigurations.needsUpload}upload(t){this.uploaded||(this.layoutVertexBuffer=t.createVertexBuffer(this.layoutVertexArray,pl),this.centroidVertexBuffer=t.createVertexBuffer(this.centroidVertexArray,hl.members,!0),this.indexBuffer=t.createIndexBuffer(this.indexArray)),this.programConfigurations.upload(t),this.uploaded=!0;}destroy(){this.layoutVertexBuffer&&(this.layoutVertexBuffer.destroy(),this.indexBuffer.destroy(),this.programConfigurations.destroy(),this.segments.destroy(),this.centroidVertexBuffer.destroy());}addFeature(t,e,r,n,i){for(const r of xr(e,500)){const e={x:0,y:0,vertexCount:0};let n=0;for(const t of r)n+=t.length;let i=this.segments.prepareSegment(4,this.layoutVertexArray,this.indexArray);for(const t of r){if(0===t.length)continue;if(Cl(t))continue;let r=0;for(let n=0;n=1){const a=t[n-1];if(!Pl(s,a)){i.vertexLength+4>oa.MAX_VERTEX_ARRAY_LENGTH&&(i=this.segments.prepareSegment(4,this.layoutVertexArray,this.indexArray));const t=s.sub(a)._perp()._unit(),n=a.dist(s);r+n>32768&&(r=0),Il(this.layoutVertexArray,s.x,s.y,t.x,t.y,0,0,r),Il(this.layoutVertexArray,s.x,s.y,t.x,t.y,0,1,r),e.x+=2*s.x,e.y+=2*s.y,e.vertexCount+=2,r+=n,Il(this.layoutVertexArray,a.x,a.y,t.x,t.y,0,0,r),Il(this.layoutVertexArray,a.x,a.y,t.x,t.y,0,1,r),e.x+=2*a.x,e.y+=2*a.y,e.vertexCount+=2;const o=i.vertexLength;this.indexArray.emplaceBack(o,o+2,o+1),this.indexArray.emplaceBack(o+1,o+2,o+3),i.vertexLength+=4,i.primitiveLength+=2;}}}}if(i.vertexLength+n>oa.MAX_VERTEX_ARRAY_LENGTH&&(i=this.segments.prepareSegment(n,this.layoutVertexArray,this.indexArray)),"Polygon"!==kl[t.type])continue;const s=[],a=[],o=i.vertexLength;for(const t of r)if(0!==t.length){t!==r[0]&&a.push(s.length/2);for(let r=0;r$a)||t.y===e.y&&(t.y<0||t.y>$a)}function Cl(t){return t.every((t=>t.x<0))||t.every((t=>t.x>$a))||t.every((t=>t.y<0))||t.every((t=>t.y>$a))}let Bl;wi("FillExtrusionBucket",zl,{omit:["layers","features"]});var Vl={get paint(){return Bl=Bl||new rs({"fill-extrusion-opacity":new Ji(G["paint_fill-extrusion"]["fill-extrusion-opacity"]),"fill-extrusion-color":new Wi(G["paint_fill-extrusion"]["fill-extrusion-color"]),"fill-extrusion-translate":new Ji(G["paint_fill-extrusion"]["fill-extrusion-translate"]),"fill-extrusion-translate-anchor":new Ji(G["paint_fill-extrusion"]["fill-extrusion-translate-anchor"]),"fill-extrusion-pattern":new Qi(G["paint_fill-extrusion"]["fill-extrusion-pattern"]),"fill-extrusion-height":new Wi(G["paint_fill-extrusion"]["fill-extrusion-height"]),"fill-extrusion-base":new Wi(G["paint_fill-extrusion"]["fill-extrusion-base"]),"fill-extrusion-vertical-gradient":new Ji(G["paint_fill-extrusion"]["fill-extrusion-vertical-gradient"])})}};class El extends is{constructor(t){super(t,Vl);}createBucket(t){return new zl(t)}queryRadius(){return eo(this.paint.get("fill-extrusion-translate"))}is3D(){return !0}queryIntersectsFeature(t,e,r,n,i,a,o,l){const u=ro(t,this.paint.get("fill-extrusion-translate"),this.paint.get("fill-extrusion-translate-anchor"),a.angle,o),c=this.paint.get("fill-extrusion-height").evaluate(e,r),h=this.paint.get("fill-extrusion-base").evaluate(e,r),p=function(t,e,r,n){const i=[];for(const r of t){const t=[r.x,r.y,0,1];po(t,t,e),i.push(new s(t[0]/t[3],t[1]/t[3]));}return i}(u,l),f=function(t,e,r,n){const i=[],a=[],o=n[8]*e,l=n[9]*e,u=n[10]*e,c=n[11]*e,h=n[8]*r,p=n[9]*r,f=n[10]*r,d=n[11]*r;for(const e of t){const t=[],r=[];for(const i of e){const e=i.x,a=i.y,y=n[0]*e+n[4]*a+n[12],m=n[1]*e+n[5]*a+n[13],g=n[2]*e+n[6]*a+n[14],x=n[3]*e+n[7]*a+n[15],v=g+u,b=x+c,w=y+h,_=m+p,A=g+f,S=x+d,k=new s((y+o)/b,(m+l)/b);k.z=v/b,t.push(k);const M=new s(w/S,_/S);M.z=A/S,r.push(M);}i.push(t),a.push(r);}return [i,a]}(n,h,c,l);return function(t,e,r){let n=1/0;Za(r,e)&&(n=Tl(r,e[0]));for(let i=0;it.id)),this.index=t.index,this.hasPattern=!1,this.patternFeatures=[],this.lineClipsArray=[],this.gradients={},this.layers.forEach((t=>{this.gradients[t.id]={};})),this.layoutVertexArray=new Hs,this.layoutVertexArray2=new Ys,this.indexArray=new na,this.programConfigurations=new Ea(t.layers,t.zoom),this.segments=new oa,this.maxLineLength=0,this.stateDependentLayerIds=this.layers.filter((t=>t.isStateDependent())).map((t=>t.id));}populate(t,e,r){this.hasPattern=nl("line",this.layers,e);const n=this.layers[0].layout.get("line-sort-key"),i=!n.isConstant(),s=[];for(const{feature:e,id:a,index:o,sourceLayerIndex:l}of t){const t=this.layers[0]._featureFilter.needGeometry,u=ja(e,t);if(!this.layers[0]._featureFilter.filter(new Ui(this.zoom),u,r))continue;const c=i?n.evaluate(u,{},r):void 0,h={id:a,properties:e.properties,type:e.type,sourceLayerIndex:l,index:o,geometry:t?u.geometry:Oa(e),patterns:{},sortKey:c};s.push(h);}i&&s.sort(((t,e)=>t.sortKey-e.sortKey));for(const n of s){const{geometry:i,index:s,sourceLayerIndex:a}=n;if(this.hasPattern){const t=il("line",this.layers,n,this.zoom,e);this.patternFeatures.push(t);}else this.addFeature(n,i,s,r,{});e.featureIndex.insert(t[s].feature,i,s,a,this.index);}}update(t,e,r){this.stateDependentLayers.length&&this.programConfigurations.updatePaintArrays(t,e,this.stateDependentLayers,r);}addFeatures(t,e,r){for(const t of this.patternFeatures)this.addFeature(t,t.geometry,t.index,e,r);}isEmpty(){return 0===this.layoutVertexArray.length}uploadPending(){return !this.uploaded||this.programConfigurations.needsUpload}upload(t){this.uploaded||(0!==this.layoutVertexArray2.length&&(this.layoutVertexBuffer2=t.createVertexBuffer(this.layoutVertexArray2,Ol)),this.layoutVertexBuffer=t.createVertexBuffer(this.layoutVertexArray,Ll),this.indexBuffer=t.createIndexBuffer(this.indexArray)),this.programConfigurations.upload(t),this.uploaded=!0;}destroy(){this.layoutVertexBuffer&&(this.layoutVertexBuffer.destroy(),this.indexBuffer.destroy(),this.programConfigurations.destroy(),this.segments.destroy());}lineFeatureClips(t){if(t.properties&&Object.prototype.hasOwnProperty.call(t.properties,"mapbox_clip_start")&&Object.prototype.hasOwnProperty.call(t.properties,"mapbox_clip_end"))return {start:+t.properties.mapbox_clip_start,end:+t.properties.mapbox_clip_end}}addFeature(t,e,r,n,i){const s=this.layers[0].layout,a=s.get("line-join").evaluate(t,{}),o=s.get("line-cap"),l=s.get("line-miter-limit"),u=s.get("line-round-limit");this.lineClips=this.lineFeatureClips(t);for(const r of e)this.addLine(r,t,a,o,l,u);this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length,t,r,i,n);}addLine(t,e,r,n,i,s){if(this.distance=0,this.scaledDistance=0,this.totalDistance=0,this.lineClips){this.lineClipsArray.push(this.lineClips);for(let e=0;e=2&&t[o-1].equals(t[o-2]);)o--;let l=0;for(;l0;if(w&&e>l){const t=h.dist(p);if(t>2*u){const e=h.sub(h.sub(p)._mult(u/t)._round());this.updateDistance(p,e),this.addCurrentVertex(e,d,0,0,c),p=e;}}const A=p&&f;let S=A?r:a?"butt":n;if(A&&"round"===S&&(vi&&(S="bevel"),"bevel"===S&&(v>2&&(S="flipbevel"),v100)m=y.mult(-1);else {const t=v*d.add(y).mag()/d.sub(y).mag();m._perp()._mult(t*(_?-1:1));}this.addCurrentVertex(h,m,0,0,c),this.addCurrentVertex(h,m.mult(-1),0,0,c);}else if("bevel"===S||"fakeround"===S){const t=-Math.sqrt(v*v-1),e=_?t:0,r=_?0:t;if(p&&this.addCurrentVertex(h,d,e,r,c),"fakeround"===S){const t=Math.round(180*b/Math.PI/20);for(let e=1;e2*u){const e=h.add(f.sub(h)._mult(u/t)._round());this.updateDistance(h,e),this.addCurrentVertex(e,y,0,0,c),h=e;}}}}addCurrentVertex(t,e,r,n,i,s=!1){const a=e.y*n-e.x,o=-e.y-e.x*n;this.addHalfVertex(t,e.x+e.y*r,e.y-e.x*r,s,!1,r,i),this.addHalfVertex(t,a,o,s,!0,-n,i),this.distance>Ul/2&&0===this.totalDistance&&(this.distance=0,this.updateScaledDistance(),this.addCurrentVertex(t,e,r,n,i,s));}addHalfVertex({x:t,y:e},r,n,i,s,a,o){const l=.5*(this.lineClips?this.scaledDistance*(Ul-1):this.scaledDistance);this.layoutVertexArray.emplaceBack((t<<1)+(i?1:0),(e<<1)+(s?1:0),Math.round(63*r)+128,Math.round(63*n)+128,1+(0===a?0:a<0?-1:1)|(63&l)<<2,l>>6),this.lineClips&&this.layoutVertexArray2.emplaceBack((this.scaledDistance-this.lineClips.start)/(this.lineClips.end-this.lineClips.start),this.lineClipsArray.length);const u=o.vertexLength++;this.e1>=0&&this.e2>=0&&(this.indexArray.emplaceBack(this.e1,this.e2,u),o.primitiveLength++),s?this.e2=u:this.e1=u;}updateScaledDistance(){this.scaledDistance=this.lineClips?this.lineClips.start+(this.lineClips.end-this.lineClips.start)*this.distance/this.totalDistance:this.distance;}updateDistance(t,e){this.distance+=t.dist(e),this.updateScaledDistance();}}let Nl,Zl;wi("LineBucket",ql,{omit:["layers","patternFeatures"]});var Gl={get paint(){return Zl=Zl||new rs({"line-opacity":new Wi(G.paint_line["line-opacity"]),"line-color":new Wi(G.paint_line["line-color"]),"line-translate":new Ji(G.paint_line["line-translate"]),"line-translate-anchor":new Ji(G.paint_line["line-translate-anchor"]),"line-width":new Wi(G.paint_line["line-width"]),"line-gap-width":new Wi(G.paint_line["line-gap-width"]),"line-offset":new Wi(G.paint_line["line-offset"]),"line-blur":new Wi(G.paint_line["line-blur"]),"line-dasharray":new ts(G.paint_line["line-dasharray"]),"line-pattern":new Qi(G.paint_line["line-pattern"]),"line-gradient":new es(G.paint_line["line-gradient"])})},get layout(){return Nl=Nl||new rs({"line-cap":new Ji(G.layout_line["line-cap"]),"line-join":new Wi(G.layout_line["line-join"]),"line-miter-limit":new Ji(G.layout_line["line-miter-limit"]),"line-round-limit":new Ji(G.layout_line["line-round-limit"]),"line-sort-key":new Wi(G.layout_line["line-sort-key"])})}};class Kl extends Wi{possiblyEvaluate(t,e){return e=new Ui(Math.floor(e.zoom),{now:e.now,fadeDuration:e.fadeDuration,zoomHistory:e.zoomHistory,transition:e.transition}),super.possiblyEvaluate(t,e)}evaluate(t,e,r,n){return e=g({},e,{zoom:Math.floor(e.zoom)}),super.evaluate(t,e,r,n)}}let Xl;class Hl extends is{constructor(t){super(t,Gl),this.gradientVersion=0,Xl||(Xl=new Kl(Gl.paint.properties["line-width"].specification),Xl.useIntegerZoom=!0);}_handleSpecialPaintPropertyUpdate(t){if("line-gradient"===t){const t=this.gradientExpression();this.stepInterpolant=!!function(t){return void 0!==t._styleExpression}(t)&&t._styleExpression.expression instanceof Ae,this.gradientVersion=(this.gradientVersion+1)%Number.MAX_SAFE_INTEGER;}}gradientExpression(){return this._transitionablePaint._values["line-gradient"].value.expression}recalculate(t,e){super.recalculate(t,e),this.paint._values["line-floorwidth"]=Xl.possiblyEvaluate(this._transitioningPaint._values["line-width"].value,t);}createBucket(t){return new ql(t)}queryRadius(t){const e=t,r=Yl(to("line-width",this,e),to("line-gap-width",this,e)),n=to("line-offset",this,e);return r/2+Math.abs(n)+eo(this.paint.get("line-translate"))}queryIntersectsFeature(t,e,r,n,i,a,o){const l=ro(t,this.paint.get("line-translate"),this.paint.get("line-translate-anchor"),a.angle,o),u=o/2*Yl(this.paint.get("line-width").evaluate(e,r),this.paint.get("line-gap-width").evaluate(e,r)),c=this.paint.get("line-offset").evaluate(e,r);return c&&(n=function(t,e){const r=[];for(let n=0;n=3)for(let e=0;e0?e+2*t:t}const Jl=ls([{name:"a_pos_offset",components:4,type:"Int16"},{name:"a_data",components:4,type:"Uint16"},{name:"a_pixeloffset",components:4,type:"Int16"}],4),Wl=ls([{name:"a_projected_pos",components:3,type:"Float32"}],4);ls([{name:"a_fade_opacity",components:1,type:"Uint32"}],4);const Ql=ls([{name:"a_placed",components:2,type:"Uint8"},{name:"a_shift",components:2,type:"Float32"},{name:"a_box_real",components:2,type:"Int16"}]);ls([{type:"Int16",name:"anchorPointX"},{type:"Int16",name:"anchorPointY"},{type:"Int16",name:"x1"},{type:"Int16",name:"y1"},{type:"Int16",name:"x2"},{type:"Int16",name:"y2"},{type:"Uint32",name:"featureIndex"},{type:"Uint16",name:"sourceLayerIndex"},{type:"Uint16",name:"bucketIndex"}]);const tu=ls([{name:"a_pos",components:2,type:"Int16"},{name:"a_anchor_pos",components:2,type:"Int16"},{name:"a_extrude",components:2,type:"Int16"}],4),eu=ls([{name:"a_pos",components:2,type:"Float32"},{name:"a_radius",components:1,type:"Float32"},{name:"a_flags",components:2,type:"Int16"}],4);function ru(t,e,r){return t.sections.forEach((t=>{t.text=function(t,e,r){const n=e.layout.get("text-transform").evaluate(r,{});return "uppercase"===n?t=t.toLocaleUpperCase():"lowercase"===n&&(t=t.toLocaleLowerCase()),Ri.applyArabicShaping&&(t=Ri.applyArabicShaping(t)),t}(t.text,e,r);})),t}ls([{name:"triangle",components:3,type:"Uint16"}]),ls([{type:"Int16",name:"anchorX"},{type:"Int16",name:"anchorY"},{type:"Uint16",name:"glyphStartIndex"},{type:"Uint16",name:"numGlyphs"},{type:"Uint32",name:"vertexStartIndex"},{type:"Uint32",name:"lineStartIndex"},{type:"Uint32",name:"lineLength"},{type:"Uint16",name:"segment"},{type:"Uint16",name:"lowerSize"},{type:"Uint16",name:"upperSize"},{type:"Float32",name:"lineOffsetX"},{type:"Float32",name:"lineOffsetY"},{type:"Uint8",name:"writingMode"},{type:"Uint8",name:"placedOrientation"},{type:"Uint8",name:"hidden"},{type:"Uint32",name:"crossTileID"},{type:"Int16",name:"associatedIconIndex"}]),ls([{type:"Int16",name:"anchorX"},{type:"Int16",name:"anchorY"},{type:"Int16",name:"rightJustifiedTextSymbolIndex"},{type:"Int16",name:"centerJustifiedTextSymbolIndex"},{type:"Int16",name:"leftJustifiedTextSymbolIndex"},{type:"Int16",name:"verticalPlacedTextSymbolIndex"},{type:"Int16",name:"placedIconSymbolIndex"},{type:"Int16",name:"verticalPlacedIconSymbolIndex"},{type:"Uint16",name:"key"},{type:"Uint16",name:"textBoxStartIndex"},{type:"Uint16",name:"textBoxEndIndex"},{type:"Uint16",name:"verticalTextBoxStartIndex"},{type:"Uint16",name:"verticalTextBoxEndIndex"},{type:"Uint16",name:"iconBoxStartIndex"},{type:"Uint16",name:"iconBoxEndIndex"},{type:"Uint16",name:"verticalIconBoxStartIndex"},{type:"Uint16",name:"verticalIconBoxEndIndex"},{type:"Uint16",name:"featureIndex"},{type:"Uint16",name:"numHorizontalGlyphVertices"},{type:"Uint16",name:"numVerticalGlyphVertices"},{type:"Uint16",name:"numIconVertices"},{type:"Uint16",name:"numVerticalIconVertices"},{type:"Uint16",name:"useRuntimeCollisionCircles"},{type:"Uint32",name:"crossTileID"},{type:"Float32",name:"textBoxScale"},{type:"Float32",name:"collisionCircleDiameter"},{type:"Uint16",name:"textAnchorOffsetStartIndex"},{type:"Uint16",name:"textAnchorOffsetEndIndex"}]),ls([{type:"Float32",name:"offsetX"}]),ls([{type:"Int16",name:"x"},{type:"Int16",name:"y"},{type:"Int16",name:"tileUnitDistanceFromAnchor"}]),ls([{type:"Uint16",name:"textAnchor"},{type:"Float32",components:2,name:"textOffset"}]);const nu={"!":"︕","#":"#",$:"$","%":"%","&":"&","(":"︵",")":"︶","*":"*","+":"+",",":"︐","-":"︲",".":"・","/":"/",":":"︓",";":"︔","<":"︿","=":"=",">":"﹀","?":"︖","@":"@","[":"﹇","\\":"\","]":"﹈","^":"^",_:"︳","`":"`","{":"︷","|":"―","}":"︸","~":"~","¢":"¢","£":"£","¥":"¥","¦":"¦","¬":"¬","¯":" ̄","–":"︲","—":"︱","‘":"﹃","’":"﹄","“":"﹁","”":"﹂","…":"︙","‧":"・","₩":"₩","、":"︑","。":"︒","〈":"︿","〉":"﹀","《":"︽","》":"︾","「":"﹁","」":"﹂","『":"﹃","』":"﹄","【":"︻","】":"︼","〔":"︹","〕":"︺","〖":"︗","〗":"︘","!":"︕","(":"︵",")":"︶",",":"︐","-":"︲",".":"・",":":"︓",";":"︔","<":"︿",">":"﹀","?":"︖","[":"﹇","]":"﹈","_":"︳","{":"︷","|":"―","}":"︸","⦅":"︵","⦆":"︶","。":"︒","「":"﹁","」":"﹂"};var iu=24,su=lu,au=function(t,e,r,n,i){var s,a,o=8*i-n-1,l=(1<>1,c=-7,h=r?i-1:0,p=r?-1:1,f=t[e+h];for(h+=p,s=f&(1<<-c)-1,f>>=-c,c+=o;c>0;s=256*s+t[e+h],h+=p,c-=8);for(a=s&(1<<-c)-1,s>>=-c,c+=n;c>0;a=256*a+t[e+h],h+=p,c-=8);if(0===s)s=1-u;else {if(s===l)return a?NaN:1/0*(f?-1:1);a+=Math.pow(2,n),s-=u;}return (f?-1:1)*a*Math.pow(2,s-n)},ou=function(t,e,r,n,i,s){var a,o,l,u=8*s-i-1,c=(1<>1,p=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,f=n?0:s-1,d=n?1:-1,y=e<0||0===e&&1/e<0?1:0;for(e=Math.abs(e),isNaN(e)||e===1/0?(o=isNaN(e)?1:0,a=c):(a=Math.floor(Math.log(e)/Math.LN2),e*(l=Math.pow(2,-a))<1&&(a--,l*=2),(e+=a+h>=1?p/l:p*Math.pow(2,1-h))*l>=2&&(a++,l/=2),a+h>=c?(o=0,a=c):a+h>=1?(o=(e*l-1)*Math.pow(2,i),a+=h):(o=e*Math.pow(2,h-1)*Math.pow(2,i),a=0));i>=8;t[r+f]=255&o,f+=d,o/=256,i-=8);for(a=a<0;t[r+f]=255&a,f+=d,a/=256,u-=8);t[r+f-d]|=128*y;};function lu(t){this.buf=ArrayBuffer.isView&&ArrayBuffer.isView(t)?t:new Uint8Array(t||0),this.pos=0,this.type=0,this.length=this.buf.length;}lu.Varint=0,lu.Fixed64=1,lu.Bytes=2,lu.Fixed32=5;var uu=4294967296,cu=1/uu,hu="undefined"==typeof TextDecoder?null:new TextDecoder("utf-8");function pu(t){return t.type===lu.Bytes?t.readVarint()+t.pos:t.pos+1}function fu(t,e,r){return r?4294967296*e+(t>>>0):4294967296*(e>>>0)+(t>>>0)}function du(t,e,r){var n=e<=16383?1:e<=2097151?2:e<=268435455?3:Math.floor(Math.log(e)/(7*Math.LN2));r.realloc(n);for(var i=r.pos-1;i>=t;i--)r.buf[i+n]=r.buf[i];}function yu(t,e){for(var r=0;r>>8,t[r+2]=e>>>16,t[r+3]=e>>>24;}function Mu(t,e){return (t[e]|t[e+1]<<8|t[e+2]<<16)+(t[e+3]<<24)}lu.prototype={destroy:function(){this.buf=null;},readFields:function(t,e,r){for(r=r||this.length;this.pos>3,s=this.pos;this.type=7&n,t(i,e,this),this.pos===s&&this.skip(n);}return e},readMessage:function(t,e){return this.readFields(t,e,this.readVarint()+this.pos)},readFixed32:function(){var t=Su(this.buf,this.pos);return this.pos+=4,t},readSFixed32:function(){var t=Mu(this.buf,this.pos);return this.pos+=4,t},readFixed64:function(){var t=Su(this.buf,this.pos)+Su(this.buf,this.pos+4)*uu;return this.pos+=8,t},readSFixed64:function(){var t=Su(this.buf,this.pos)+Mu(this.buf,this.pos+4)*uu;return this.pos+=8,t},readFloat:function(){var t=au(this.buf,this.pos,!0,23,4);return this.pos+=4,t},readDouble:function(){var t=au(this.buf,this.pos,!0,52,8);return this.pos+=8,t},readVarint:function(t){var e,r,n=this.buf;return e=127&(r=n[this.pos++]),r<128?e:(e|=(127&(r=n[this.pos++]))<<7,r<128?e:(e|=(127&(r=n[this.pos++]))<<14,r<128?e:(e|=(127&(r=n[this.pos++]))<<21,r<128?e:function(t,e,r){var n,i,s=r.buf;if(n=(112&(i=s[r.pos++]))>>4,i<128)return fu(t,n,e);if(n|=(127&(i=s[r.pos++]))<<3,i<128)return fu(t,n,e);if(n|=(127&(i=s[r.pos++]))<<10,i<128)return fu(t,n,e);if(n|=(127&(i=s[r.pos++]))<<17,i<128)return fu(t,n,e);if(n|=(127&(i=s[r.pos++]))<<24,i<128)return fu(t,n,e);if(n|=(1&(i=s[r.pos++]))<<31,i<128)return fu(t,n,e);throw new Error("Expected varint not more than 10 bytes")}(e|=(15&(r=n[this.pos]))<<28,t,this))))},readVarint64:function(){return this.readVarint(!0)},readSVarint:function(){var t=this.readVarint();return t%2==1?(t+1)/-2:t/2},readBoolean:function(){return Boolean(this.readVarint())},readString:function(){var t=this.readVarint()+this.pos,e=this.pos;return this.pos=t,t-e>=12&&hu?function(t,e,r){return hu.decode(t.subarray(e,r))}(this.buf,e,t):function(t,e,r){for(var n="",i=e;i239?4:l>223?3:l>191?2:1;if(i+c>r)break;1===c?l<128&&(u=l):2===c?128==(192&(s=t[i+1]))&&(u=(31&l)<<6|63&s)<=127&&(u=null):3===c?(a=t[i+2],128==(192&(s=t[i+1]))&&128==(192&a)&&((u=(15&l)<<12|(63&s)<<6|63&a)<=2047||u>=55296&&u<=57343)&&(u=null)):4===c&&(a=t[i+2],o=t[i+3],128==(192&(s=t[i+1]))&&128==(192&a)&&128==(192&o)&&((u=(15&l)<<18|(63&s)<<12|(63&a)<<6|63&o)<=65535||u>=1114112)&&(u=null)),null===u?(u=65533,c=1):u>65535&&(u-=65536,n+=String.fromCharCode(u>>>10&1023|55296),u=56320|1023&u),n+=String.fromCharCode(u),i+=c;}return n}(this.buf,e,t)},readBytes:function(){var t=this.readVarint()+this.pos,e=this.buf.subarray(this.pos,t);return this.pos=t,e},readPackedVarint:function(t,e){if(this.type!==lu.Bytes)return t.push(this.readVarint(e));var r=pu(this);for(t=t||[];this.pos127;);else if(e===lu.Bytes)this.pos=this.readVarint()+this.pos;else if(e===lu.Fixed32)this.pos+=4;else {if(e!==lu.Fixed64)throw new Error("Unimplemented type: "+e);this.pos+=8;}},writeTag:function(t,e){this.writeVarint(t<<3|e);},realloc:function(t){for(var e=this.length||16;e268435455||t<0?function(t,e){var r,n;if(t>=0?(r=t%4294967296|0,n=t/4294967296|0):(n=~(-t/4294967296),4294967295^(r=~(-t%4294967296))?r=r+1|0:(r=0,n=n+1|0)),t>=0x10000000000000000||t<-0x10000000000000000)throw new Error("Given varint doesn't fit into 10 bytes");e.realloc(10),function(t,e,r){r.buf[r.pos++]=127&t|128,t>>>=7,r.buf[r.pos++]=127&t|128,t>>>=7,r.buf[r.pos++]=127&t|128,t>>>=7,r.buf[r.pos++]=127&t|128,r.buf[r.pos]=127&(t>>>=7);}(r,0,e),function(t,e){var r=(7&t)<<4;e.buf[e.pos++]|=r|((t>>>=3)?128:0),t&&(e.buf[e.pos++]=127&t|((t>>>=7)?128:0),t&&(e.buf[e.pos++]=127&t|((t>>>=7)?128:0),t&&(e.buf[e.pos++]=127&t|((t>>>=7)?128:0),t&&(e.buf[e.pos++]=127&t|((t>>>=7)?128:0),t&&(e.buf[e.pos++]=127&t)))));}(n,e);}(t,this):(this.realloc(4),this.buf[this.pos++]=127&t|(t>127?128:0),t<=127||(this.buf[this.pos++]=127&(t>>>=7)|(t>127?128:0),t<=127||(this.buf[this.pos++]=127&(t>>>=7)|(t>127?128:0),t<=127||(this.buf[this.pos++]=t>>>7&127))));},writeSVarint:function(t){this.writeVarint(t<0?2*-t-1:2*t);},writeBoolean:function(t){this.writeVarint(Boolean(t));},writeString:function(t){t=String(t),this.realloc(4*t.length),this.pos++;var e=this.pos;this.pos=function(t,e,r){for(var n,i,s=0;s55295&&n<57344){if(!i){n>56319||s+1===e.length?(t[r++]=239,t[r++]=191,t[r++]=189):i=n;continue}if(n<56320){t[r++]=239,t[r++]=191,t[r++]=189,i=n;continue}n=i-55296<<10|n-56320|65536,i=null;}else i&&(t[r++]=239,t[r++]=191,t[r++]=189,i=null);n<128?t[r++]=n:(n<2048?t[r++]=n>>6|192:(n<65536?t[r++]=n>>12|224:(t[r++]=n>>18|240,t[r++]=n>>12&63|128),t[r++]=n>>6&63|128),t[r++]=63&n|128);}return r}(this.buf,t,this.pos);var r=this.pos-e;r>=128&&du(e,r,this),this.pos=e-1,this.writeVarint(r),this.pos+=r;},writeFloat:function(t){this.realloc(4),ou(this.buf,t,this.pos,!0,23,4),this.pos+=4;},writeDouble:function(t){this.realloc(8),ou(this.buf,t,this.pos,!0,52,8),this.pos+=8;},writeBytes:function(t){var e=t.length;this.writeVarint(e),this.realloc(e);for(var r=0;r=128&&du(r,n,this),this.pos=r-1,this.writeVarint(n),this.pos+=n;},writeMessage:function(t,e,r){this.writeTag(t,lu.Bytes),this.writeRawMessage(e,r);},writePackedVarint:function(t,e){e.length&&this.writeMessage(t,yu,e);},writePackedSVarint:function(t,e){e.length&&this.writeMessage(t,mu,e);},writePackedBoolean:function(t,e){e.length&&this.writeMessage(t,vu,e);},writePackedFloat:function(t,e){e.length&&this.writeMessage(t,gu,e);},writePackedDouble:function(t,e){e.length&&this.writeMessage(t,xu,e);},writePackedFixed32:function(t,e){e.length&&this.writeMessage(t,bu,e);},writePackedSFixed32:function(t,e){e.length&&this.writeMessage(t,wu,e);},writePackedFixed64:function(t,e){e.length&&this.writeMessage(t,_u,e);},writePackedSFixed64:function(t,e){e.length&&this.writeMessage(t,Au,e);},writeBytesField:function(t,e){this.writeTag(t,lu.Bytes),this.writeBytes(e);},writeFixed32Field:function(t,e){this.writeTag(t,lu.Fixed32),this.writeFixed32(e);},writeSFixed32Field:function(t,e){this.writeTag(t,lu.Fixed32),this.writeSFixed32(e);},writeFixed64Field:function(t,e){this.writeTag(t,lu.Fixed64),this.writeFixed64(e);},writeSFixed64Field:function(t,e){this.writeTag(t,lu.Fixed64),this.writeSFixed64(e);},writeVarintField:function(t,e){this.writeTag(t,lu.Varint),this.writeVarint(e);},writeSVarintField:function(t,e){this.writeTag(t,lu.Varint),this.writeSVarint(e);},writeStringField:function(t,e){this.writeTag(t,lu.Bytes),this.writeString(e);},writeFloatField:function(t,e){this.writeTag(t,lu.Fixed32),this.writeFloat(e);},writeDoubleField:function(t,e){this.writeTag(t,lu.Fixed64),this.writeDouble(e);},writeBooleanField:function(t,e){this.writeVarintField(t,Boolean(e));}};var Iu=r(su);const zu=3;function Pu(t,e,r){1===t&&r.readMessage(Cu,e);}function Cu(t,e,r){if(3===t){const{id:t,bitmap:n,width:i,height:s,left:a,top:o,advance:l}=r.readMessage(Bu,{});e.push({id:t,bitmap:new _o({width:i+2*zu,height:s+2*zu},n),metrics:{width:i,height:s,left:a,top:o,advance:l}});}}function Bu(t,e,r){1===t?e.id=r.readVarint():2===t?e.bitmap=r.readBytes():3===t?e.width=r.readVarint():4===t?e.height=r.readVarint():5===t?e.left=r.readSVarint():6===t?e.top=r.readSVarint():7===t&&(e.advance=r.readVarint());}const Vu=zu;function Eu(t){let e=0,r=0;for(const n of t)e+=n.w*n.h,r=Math.max(r,n.w);t.sort(((t,e)=>e.h-t.h));const n=[{x:0,y:0,w:Math.max(Math.ceil(Math.sqrt(e/.95)),r),h:1/0}];let i=0,s=0;for(const e of t)for(let t=n.length-1;t>=0;t--){const r=n[t];if(!(e.w>r.w||e.h>r.h)){if(e.x=r.x,e.y=r.y,s=Math.max(s,e.y+e.h),i=Math.max(i,e.x+e.w),e.w===r.w&&e.h===r.h){const e=n.pop();t=0&&r>=t&&Uu[this.text.charCodeAt(r)];r--)e--;this.text=this.text.substring(t,e),this.sectionIndex=this.sectionIndex.slice(t,e);}substring(t,e){const r=new ju;return r.text=this.text.substring(t,e),r.sectionIndex=this.sectionIndex.slice(t,e),r.sections=this.sections,r}toString(){return this.text}getMaxScale(){return this.sectionIndex.reduce(((t,e)=>Math.max(t,this.sections[e].scale)),0)}addTextSection(t,e){this.text+=t.text,this.sections.push(Ou.forText(t.scale,t.fontStack||e));const r=this.sections.length-1;for(let e=0;e=63743?null:++this.imageSectionID:(this.imageSectionID=57344,this.imageSectionID)}}function Ru(e,r,n,i,s,a,o,l,u,c,h,p,f,d,y){const m=ju.fromFeature(e,s);let g;p===t.ah.vertical&&m.verticalizePunctuation();const{processBidirectionalText:x,processStyledBidirectionalText:v}=Ri;if(x&&1===m.sections.length){g=[];const t=x(m.toString(),Yu(m,c,a,r,i,d));for(const e of t){const t=new ju;t.text=e,t.sections=m.sections;for(let r=0;r0&&n>_&&(_=n);}else {const t=n[y.fontStack],e=t&&t[g];if(e&&e.rect)A=e.rect,b=e.metrics;else {const t=r[y.fontStack],e=t&&t[g];if(!e)continue;b=e.metrics;}x=(s-y.scale)*iu;}M?(e.verticalizable=!0,w.push({glyph:g,imageName:S,x:f,y:d+x,vertical:M,scale:y.scale,fontStack:y.fontStack,sectionIndex:m,metrics:b,rect:A}),f+=k*y.scale+c):(w.push({glyph:g,imageName:S,x:f,y:d+x,vertical:M,scale:y.scale,fontStack:y.fontStack,sectionIndex:m,metrics:b,rect:A}),f+=b.advance*y.scale+c);}0!==w.length&&(y=Math.max(f-c,y),Wu(w,0,w.length-1,g,_)),f=0;const A=a*s+_;b.lineOffset=Math.max(_,l),d+=A,m=Math.max(A,m),++x;}var v;const b=d-Du,{horizontalAlign:w,verticalAlign:_}=Ju(o);((function(t,e,r,n,i,s,a,o,l){const u=(e-r)*i;let c=0;c=s!==a?-o*n-Du:(-n*l+.5)*a;for(const e of t)for(const t of e.positionedGlyphs)t.x+=u,t.y+=c;}))(e.positionedLines,g,w,_,y,m,a,b,s.length),e.top+=-_*b,e.bottom=e.top+b,e.left+=-w*y,e.right=e.left+y;}(w,r,n,i,g,o,l,u,p,c,f,y),!function(t){for(const e of t)if(0!==e.positionedGlyphs.length)return !1;return !0}(b)&&w}const Uu={9:!0,10:!0,11:!0,12:!0,13:!0,32:!0},qu={10:!0,32:!0,38:!0,41:!0,43:!0,45:!0,47:!0,173:!0,183:!0,8203:!0,8208:!0,8211:!0,8231:!0},Nu={40:!0};function Zu(t,e,r,n,i,s){if(e.imageName){const t=n[e.imageName];return t?t.displaySize[0]*e.scale*iu/s+i:0}{const n=r[e.fontStack],s=n&&n[t];return s?s.metrics.advance*e.scale+i:0}}function Gu(t,e,r,n){const i=Math.pow(t-e,2);return n?t=0;let u=0;for(let r=0;ru){const t=Math.ceil(s/u);i*=t/a,a=t;}return {x1:n,y1:i,x2:n+s,y2:i+a}}function ec(t,e,r,n,i,s){const a=t.image;let o;if(a.content){const t=a.content,e=a.pixelRatio||1;o=[t[0]/e,t[1]/e,a.displaySize[0]-t[2]/e,a.displaySize[1]-t[3]/e];}const l=e.left*s,u=e.right*s;let c,h,p,f;"width"===r||"both"===r?(f=i[0]+l-n[3],h=i[0]+u+n[1]):(f=i[0]+(l+u-a.displaySize[0])/2,h=f+a.displaySize[0]);const d=e.top*s,y=e.bottom*s;return "height"===r||"both"===r?(c=i[1]+d-n[0],p=i[1]+y+n[2]):(c=i[1]+(d+y-a.displaySize[1])/2,p=c+a.displaySize[1]),{image:a,top:c,right:h,bottom:p,left:f,collisionPadding:o}}const rc=255,nc=128,ic=rc*nc;function sc(t,e){const{expression:r}=e;if("constant"===r.kind)return {kind:"constant",layoutSize:r.evaluate(new Ui(t+1))};if("source"===r.kind)return {kind:"source"};{const{zoomStops:e,interpolationType:n}=r;let i=0;for(;it.id)),this.index=e.index,this.pixelRatio=e.pixelRatio,this.sourceLayerIndex=e.sourceLayerIndex,this.hasPattern=!1,this.hasRTLText=!1,this.sortKeyRanges=[],this.collisionCircleArray=[],this.placementInvProjMatrix=lo([]),this.placementViewportMatrix=lo([]);const r=this.layers[0]._unevaluatedLayout._values;this.textSizeData=sc(this.zoom,r["text-size"]),this.iconSizeData=sc(this.zoom,r["icon-size"]);const n=this.layers[0].layout,i=n.get("symbol-sort-key"),s=n.get("symbol-z-order");this.canOverlap="never"!==ac(n,"text-overlap","text-allow-overlap")||"never"!==ac(n,"icon-overlap","icon-allow-overlap")||n.get("text-ignore-placement")||n.get("icon-ignore-placement"),this.sortFeaturesByKey="viewport-y"!==s&&!i.isConstant(),this.sortFeaturesByY=("viewport-y"===s||"auto"===s&&!this.sortFeaturesByKey)&&this.canOverlap,"point"===n.get("symbol-placement")&&(this.writingModes=n.get("text-writing-mode").map((e=>t.ah[e]))),this.stateDependentLayerIds=this.layers.filter((t=>t.isStateDependent())).map((t=>t.id)),this.sourceID=e.sourceID;}createArrays(){this.text=new pc(new Ea(this.layers,this.zoom,(t=>/^text/.test(t)))),this.icon=new pc(new Ea(this.layers,this.zoom,(t=>/^icon/.test(t)))),this.glyphOffsetArray=new Os,this.lineVertexArray=new js,this.symbolInstances=new Ds,this.textAnchorOffsets=new Us;}calculateGlyphDependencies(t,e,r,n,i){for(let s=0;s0)&&("constant"!==a.value.kind||a.value.value.length>0),c="constant"!==l.value.kind||!!l.value.value||Object.keys(l.parameters).length>0,h=s.get("symbol-sort-key");if(this.features=[],!u&&!c)return;const p=r.iconDependencies,f=r.glyphDependencies,d=r.availableImages,y=new Ui(this.zoom);for(const{feature:r,id:o,index:l,sourceLayerIndex:m}of e){const e=i._featureFilter.needGeometry,g=ja(r,e);if(!i._featureFilter.filter(y,g,n))continue;let x,v;if(e||(g.geometry=Oa(r)),u){const t=i.getValueAndResolveTokens("text-field",g,n,d),e=Yt.factory(t),r=this.hasRTLText=this.hasRTLText||hc(e);(!r||"unavailable"===Ri.getRTLTextPluginStatus()||r&&Ri.isParsed())&&(x=ru(e,i,g));}if(c){const t=i.getValueAndResolveTokens("icon-image",g,n,d);v=t instanceof te?t:te.fromString(t);}if(!x&&!v)continue;const b=this.sortFeaturesByKey?h.evaluate(g,{},n):void 0;if(this.features.push({id:o,text:x,icon:v,index:l,sourceLayerIndex:m,geometry:g.geometry,properties:r.properties,type:oc[r.type],sortKey:b}),v&&(p[v.name]=!0),x){const e=a.evaluate(g,{},n).join(","),r="viewport"!==s.get("text-rotation-alignment")&&"point"!==s.get("symbol-placement");this.allowVerticalPlacement=this.writingModes&&this.writingModes.indexOf(t.ah.vertical)>=0;for(const t of x.sections)if(t.image)p[t.image.name]=!0;else {const n=Pi(x.toString()),i=t.fontStack||e,s=f[i]=f[i]||{};this.calculateGlyphDependencies(t.text,s,r,this.allowVerticalPlacement,n);}}}"line"===s.get("symbol-placement")&&(this.features=function(t){const e={},r={},n=[];let i=0;function s(e){n.push(t[e]),i++;}function a(t,e,i){const s=r[t];return delete r[t],r[e]=s,n[s].geometry[0].pop(),n[s].geometry[0]=n[s].geometry[0].concat(i[0]),s}function o(t,r,i){const s=e[r];return delete e[r],e[t]=s,n[s].geometry[0].shift(),n[s].geometry[0]=i[0].concat(n[s].geometry[0]),s}function l(t,e,r){const n=r?e[0][e[0].length-1]:e[0][0];return `${t}:${n.x}:${n.y}`}for(let u=0;ut.geometry))}(this.features)),this.sortFeaturesByKey&&this.features.sort(((t,e)=>t.sortKey-e.sortKey));}update(t,e,r){this.stateDependentLayers.length&&(this.text.programConfigurations.updatePaintArrays(t,e,this.layers,r),this.icon.programConfigurations.updatePaintArrays(t,e,this.layers,r));}isEmpty(){return 0===this.symbolInstances.length&&!this.hasRTLText}uploadPending(){return !this.uploaded||this.text.programConfigurations.needsUpload||this.icon.programConfigurations.needsUpload}upload(t){!this.uploaded&&this.hasDebugData()&&(this.textCollisionBox.upload(t),this.iconCollisionBox.upload(t)),this.text.upload(t,this.sortFeaturesByY,!this.uploaded,this.text.programConfigurations.needsUpload),this.icon.upload(t,this.sortFeaturesByY,!this.uploaded,this.icon.programConfigurations.needsUpload),this.uploaded=!0;}destroyDebugData(){this.textCollisionBox.destroy(),this.iconCollisionBox.destroy();}destroy(){this.text.destroy(),this.icon.destroy(),this.hasDebugData()&&this.destroyDebugData();}addToLineVertexArray(t,e){const r=this.lineVertexArray.length;if(void 0!==t.segment){let r=t.dist(e[t.segment+1]),n=t.dist(e[t.segment]);const i={};for(let n=t.segment+1;n=0;r--)i[r]={x:e[r].x,y:e[r].y,tileUnitDistanceFromAnchor:n},r>0&&(n+=e[r-1].dist(e[r]));for(let t=0;t0}hasIconData(){return this.icon.segments.get().length>0}hasDebugData(){return this.textCollisionBox&&this.iconCollisionBox}hasTextCollisionBoxData(){return this.hasDebugData()&&this.textCollisionBox.segments.get().length>0}hasIconCollisionBoxData(){return this.hasDebugData()&&this.iconCollisionBox.segments.get().length>0}addIndicesForPlacedSymbol(t,e){const r=t.placedSymbolArray.get(e),n=r.vertexStartIndex+4*r.numGlyphs;for(let e=r.vertexStartIndex;en[t]-n[e]||i[e]-i[t])),s}addToSortKeyRanges(t,e){const r=this.sortKeyRanges[this.sortKeyRanges.length-1];r&&r.sortKey===e?r.symbolInstanceEnd=t+1:this.sortKeyRanges.push({sortKey:e,symbolInstanceStart:t,symbolInstanceEnd:t+1});}sortFeatures(t){if(this.sortFeaturesByY&&this.sortedAngle!==t&&!(this.text.segments.get().length>1||this.icon.segments.get().length>1)){this.symbolInstanceIndexes=this.getSortedSymbolIndexes(t),this.sortedAngle=t,this.text.indexArray.clear(),this.icon.indexArray.clear(),this.featureSortOrder=[];for(const t of this.symbolInstanceIndexes){const e=this.symbolInstances.get(t);this.featureSortOrder.push(e.featureIndex),[e.rightJustifiedTextSymbolIndex,e.centerJustifiedTextSymbolIndex,e.leftJustifiedTextSymbolIndex].forEach(((t,e,r)=>{t>=0&&r.indexOf(t)===e&&this.addIndicesForPlacedSymbol(this.text,t);})),e.verticalPlacedTextSymbolIndex>=0&&this.addIndicesForPlacedSymbol(this.text,e.verticalPlacedTextSymbolIndex),e.placedIconSymbolIndex>=0&&this.addIndicesForPlacedSymbol(this.icon,e.placedIconSymbolIndex),e.verticalPlacedIconSymbolIndex>=0&&this.addIndicesForPlacedSymbol(this.icon,e.verticalPlacedIconSymbolIndex);}this.text.indexBuffer&&this.text.indexBuffer.updateData(this.text.indexArray),this.icon.indexBuffer&&this.icon.indexBuffer.updateData(this.icon.indexArray);}}}let yc,mc;wi("SymbolBucket",dc,{omit:["layers","collisionBoxArray","features","compareText"]}),dc.MAX_GLYPHS=65535,dc.addDynamicAttributes=cc;var gc={get paint(){return mc=mc||new rs({"icon-opacity":new Wi(G.paint_symbol["icon-opacity"]),"icon-color":new Wi(G.paint_symbol["icon-color"]),"icon-halo-color":new Wi(G.paint_symbol["icon-halo-color"]),"icon-halo-width":new Wi(G.paint_symbol["icon-halo-width"]),"icon-halo-blur":new Wi(G.paint_symbol["icon-halo-blur"]),"icon-translate":new Ji(G.paint_symbol["icon-translate"]),"icon-translate-anchor":new Ji(G.paint_symbol["icon-translate-anchor"]),"text-opacity":new Wi(G.paint_symbol["text-opacity"]),"text-color":new Wi(G.paint_symbol["text-color"],{runtimeType:pt,getOverride:t=>t.textColor,hasOverride:t=>!!t.textColor}),"text-halo-color":new Wi(G.paint_symbol["text-halo-color"]),"text-halo-width":new Wi(G.paint_symbol["text-halo-width"]),"text-halo-blur":new Wi(G.paint_symbol["text-halo-blur"]),"text-translate":new Ji(G.paint_symbol["text-translate"]),"text-translate-anchor":new Ji(G.paint_symbol["text-translate-anchor"])})},get layout(){return yc=yc||new rs({"symbol-placement":new Ji(G.layout_symbol["symbol-placement"]),"symbol-spacing":new Ji(G.layout_symbol["symbol-spacing"]),"symbol-avoid-edges":new Ji(G.layout_symbol["symbol-avoid-edges"]),"symbol-sort-key":new Wi(G.layout_symbol["symbol-sort-key"]),"symbol-z-order":new Ji(G.layout_symbol["symbol-z-order"]),"icon-allow-overlap":new Ji(G.layout_symbol["icon-allow-overlap"]),"icon-overlap":new Ji(G.layout_symbol["icon-overlap"]),"icon-ignore-placement":new Ji(G.layout_symbol["icon-ignore-placement"]),"icon-optional":new Ji(G.layout_symbol["icon-optional"]),"icon-rotation-alignment":new Ji(G.layout_symbol["icon-rotation-alignment"]),"icon-size":new Wi(G.layout_symbol["icon-size"]),"icon-text-fit":new Ji(G.layout_symbol["icon-text-fit"]),"icon-text-fit-padding":new Ji(G.layout_symbol["icon-text-fit-padding"]),"icon-image":new Wi(G.layout_symbol["icon-image"]),"icon-rotate":new Wi(G.layout_symbol["icon-rotate"]),"icon-padding":new Wi(G.layout_symbol["icon-padding"]),"icon-keep-upright":new Ji(G.layout_symbol["icon-keep-upright"]),"icon-offset":new Wi(G.layout_symbol["icon-offset"]),"icon-anchor":new Wi(G.layout_symbol["icon-anchor"]),"icon-pitch-alignment":new Ji(G.layout_symbol["icon-pitch-alignment"]),"text-pitch-alignment":new Ji(G.layout_symbol["text-pitch-alignment"]),"text-rotation-alignment":new Ji(G.layout_symbol["text-rotation-alignment"]),"text-field":new Wi(G.layout_symbol["text-field"]),"text-font":new Wi(G.layout_symbol["text-font"]),"text-size":new Wi(G.layout_symbol["text-size"]),"text-max-width":new Wi(G.layout_symbol["text-max-width"]),"text-line-height":new Ji(G.layout_symbol["text-line-height"]),"text-letter-spacing":new Wi(G.layout_symbol["text-letter-spacing"]),"text-justify":new Wi(G.layout_symbol["text-justify"]),"text-radial-offset":new Wi(G.layout_symbol["text-radial-offset"]),"text-variable-anchor":new Ji(G.layout_symbol["text-variable-anchor"]),"text-variable-anchor-offset":new Wi(G.layout_symbol["text-variable-anchor-offset"]),"text-anchor":new Wi(G.layout_symbol["text-anchor"]),"text-max-angle":new Ji(G.layout_symbol["text-max-angle"]),"text-writing-mode":new Ji(G.layout_symbol["text-writing-mode"]),"text-rotate":new Wi(G.layout_symbol["text-rotate"]),"text-padding":new Ji(G.layout_symbol["text-padding"]),"text-keep-upright":new Ji(G.layout_symbol["text-keep-upright"]),"text-transform":new Wi(G.layout_symbol["text-transform"]),"text-offset":new Wi(G.layout_symbol["text-offset"]),"text-allow-overlap":new Ji(G.layout_symbol["text-allow-overlap"]),"text-overlap":new Ji(G.layout_symbol["text-overlap"]),"text-ignore-placement":new Ji(G.layout_symbol["text-ignore-placement"]),"text-optional":new Ji(G.layout_symbol["text-optional"])})}};class xc{constructor(t){if(void 0===t.property.overrides)throw new Error("overrides must be provided to instantiate FormatSectionOverride class");this.type=t.property.overrides?t.property.overrides.runtimeType:lt,this.defaultValue=t;}evaluate(t){if(t.formattedSection){const e=this.defaultValue.property.overrides;if(e&&e.hasOverride(t.formattedSection))return e.getOverride(t.formattedSection)}return t.feature&&t.featureState?this.defaultValue.evaluate(t.feature,t.featureState):this.defaultValue.property.specification.default}eachChild(t){this.defaultValue.isConstant()||t(this.defaultValue.value._styleExpression.expression);}outputDefined(){return !1}serialize(){return null}}wi("FormatSectionOverride",xc,{omit:["defaultValue"]});class vc extends is{constructor(t){super(t,gc);}recalculate(t,e){if(super.recalculate(t,e),"auto"===this.layout.get("icon-rotation-alignment")&&(this.layout._values["icon-rotation-alignment"]="point"!==this.layout.get("symbol-placement")?"map":"viewport"),"auto"===this.layout.get("text-rotation-alignment")&&(this.layout._values["text-rotation-alignment"]="point"!==this.layout.get("symbol-placement")?"map":"viewport"),"auto"===this.layout.get("text-pitch-alignment")&&(this.layout._values["text-pitch-alignment"]="map"===this.layout.get("text-rotation-alignment")?"map":"viewport"),"auto"===this.layout.get("icon-pitch-alignment")&&(this.layout._values["icon-pitch-alignment"]=this.layout.get("icon-rotation-alignment")),"point"===this.layout.get("symbol-placement")){const t=this.layout.get("text-writing-mode");if(t){const e=[];for(const r of t)e.indexOf(r)<0&&e.push(r);this.layout._values["text-writing-mode"]=e;}else this.layout._values["text-writing-mode"]=["horizontal"];}this._setPaintOverrides();}getValueAndResolveTokens(t,e,r,n){const i=this.layout.get(t).evaluate(e,{},r,n),s=this._unevaluatedLayout._values[t];return s.isDataDriven()||vn(s.value)||!i?i:function(t,e){return e.replace(/{([^{}]+)}/g,((e,r)=>t&&r in t?String(t[r]):""))}(e.properties,i)}createBucket(t){return new dc(t)}queryRadius(){return 0}queryIntersectsFeature(){throw new Error("Should take a different path in FeatureIndex")}_setPaintOverrides(){for(const t of gc.paint.overridableProperties){if(!vc.hasPaintOverride(this.layout,t))continue;const e=this.paint.get(t),r=new xc(e),n=new xn(r,e.property.specification);let i=null;i="constant"===e.value.kind||"source"===e.value.kind?new wn("source",n):new _n("composite",n,e.value.zoomStops),this.paint._values[t]=new Hi(e.property,i,e.parameters);}}_handleOverridablePaintPropertyUpdate(t,e,r){return !(!this.layout||e.isDataDriven()||r.isDataDriven())&&vc.hasPaintOverride(this.layout,t)}static hasPaintOverride(t,e){const r=t.get("text-field"),n=gc.paint.properties[e];let i=!1;const s=t=>{for(const e of t)if(n.overrides&&n.overrides.hasOverride(e))return void(i=!0)};if("constant"===r.value.kind&&r.value.value instanceof Yt)s(r.value.value.sections);else if("source"===r.value.kind){const t=e=>{i||(e instanceof se&&ne(e.value)===mt?s(e.value.sections):e instanceof Ze?s(e.sections):e.eachChild(t));},e=r.value;e._styleExpression&&t(e._styleExpression.expression);}return i}}let bc;var wc={get paint(){return bc=bc||new rs({"background-color":new Ji(G.paint_background["background-color"]),"background-pattern":new ts(G.paint_background["background-pattern"]),"background-opacity":new Ji(G.paint_background["background-opacity"])})}};class _c extends is{constructor(t){super(t,wc);}}let Ac;var Sc={get paint(){return Ac=Ac||new rs({"raster-opacity":new Ji(G.paint_raster["raster-opacity"]),"raster-hue-rotate":new Ji(G.paint_raster["raster-hue-rotate"]),"raster-brightness-min":new Ji(G.paint_raster["raster-brightness-min"]),"raster-brightness-max":new Ji(G.paint_raster["raster-brightness-max"]),"raster-saturation":new Ji(G.paint_raster["raster-saturation"]),"raster-contrast":new Ji(G.paint_raster["raster-contrast"]),"raster-resampling":new Ji(G.paint_raster["raster-resampling"]),"raster-fade-duration":new Ji(G.paint_raster["raster-fade-duration"])})}};class kc extends is{constructor(t){super(t,Sc);}}class Mc extends is{constructor(t){super(t,{}),this.onAdd=t=>{this.implementation.onAdd&&this.implementation.onAdd(t,t.painter.context.gl);},this.onRemove=t=>{this.implementation.onRemove&&this.implementation.onRemove(t,t.painter.context.gl);},this.implementation=t;}is3D(){return "3d"===this.implementation.renderingMode}hasOffscreenPass(){return void 0!==this.implementation.prerender}recalculate(){}updateTransitions(){}hasTransition(){return !1}serialize(){throw new Error("Custom layers cannot be serialized")}}class Ic{constructor(t){this._methodToThrottle=t,this._triggered=!1,"undefined"!=typeof MessageChannel&&(this._channel=new MessageChannel,this._channel.port2.onmessage=()=>{this._triggered=!1,this._methodToThrottle();});}trigger(){this._triggered||(this._triggered=!0,this._channel?this._channel.port1.postMessage(!0):setTimeout((()=>{this._triggered=!1,this._methodToThrottle();}),0));}remove(){delete this._channel,this._methodToThrottle=()=>{};}}const zc=6371008.8;class Pc{constructor(t,e){if(isNaN(t)||isNaN(e))throw new Error(`Invalid LngLat object: (${t}, ${e})`);if(this.lng=+t,this.lat=+e,this.lat>90||this.lat<-90)throw new Error("Invalid LngLat latitude value: must be between -90 and 90")}wrap(){return new Pc(m(this.lng,-180,180),this.lat)}toArray(){return [this.lng,this.lat]}toString(){return `LngLat(${this.lng}, ${this.lat})`}distanceTo(t){const e=Math.PI/180,r=this.lat*e,n=t.lat*e,i=Math.sin(r)*Math.sin(n)+Math.cos(r)*Math.cos(n)*Math.cos((t.lng-this.lng)*e);return zc*Math.acos(Math.min(i,1))}static convert(t){if(t instanceof Pc)return t;if(Array.isArray(t)&&(2===t.length||3===t.length))return new Pc(Number(t[0]),Number(t[1]));if(!Array.isArray(t)&&"object"==typeof t&&null!==t)return new Pc(Number("lng"in t?t.lng:t.lon),Number(t.lat));throw new Error("`LngLatLike` argument must be specified as a LngLat instance, an object {lng: , lat: }, an object {lon: , lat: }, or an array of [, ]")}}const Cc=2*Math.PI*zc;function Bc(t){return Cc*Math.cos(t*Math.PI/180)}function Vc(t){return (180+t)/360}function Ec(t){return (180-180/Math.PI*Math.log(Math.tan(Math.PI/4+t*Math.PI/360)))/360}function Fc(t,e){return t/Bc(e)}function Tc(t){return 360/Math.PI*Math.atan(Math.exp((180-360*t)*Math.PI/180))-90}class $c{constructor(t,e,r=0){this.x=+t,this.y=+e,this.z=+r;}static fromLngLat(t,e=0){const r=Pc.convert(t);return new $c(Vc(r.lng),Ec(r.lat),Fc(e,r.lat))}toLngLat(){return new Pc(360*this.x-180,Tc(this.y))}toAltitude(){return this.z*Bc(Tc(this.y))}meterInMercatorCoordinateUnits(){return 1/Cc*(t=Tc(this.y),1/Math.cos(t*Math.PI/180));var t;}}function Lc(t,e,r){var n=2*Math.PI*6378137/256/Math.pow(2,r);return [t*n-2*Math.PI*6378137/2,e*n-2*Math.PI*6378137/2]}class Dc{constructor(t,e,r){if(!function(t,e,r){return !(t<0||t>25||r<0||r>=Math.pow(2,t)||e<0||e>=Math.pow(2,t))}(t,e,r))throw new Error(`x=${e}, y=${r}, z=${t} outside of bounds. 0<=x<${Math.pow(2,t)}, 0<=y<${Math.pow(2,t)} 0<=z<=25 `);this.z=t,this.x=e,this.y=r,this.key=Rc(0,t,t,e,r);}equals(t){return this.z===t.z&&this.x===t.x&&this.y===t.y}url(t,e,r){const n=(s=this.y,a=this.z,o=Lc(256*(i=this.x),256*(s=Math.pow(2,a)-s-1),a),l=Lc(256*(i+1),256*(s+1),a),o[0]+","+o[1]+","+l[0]+","+l[1]);var i,s,a,o,l;const u=function(t,e,r){let n,i="";for(let s=t;s>0;s--)n=1<1?"@2x":"").replace(/{quadkey}/g,u).replace(/{bbox-epsg-3857}/g,n)}isChildOf(t){const e=this.z-t.z;return e>0&&t.x===this.x>>e&&t.y===this.y>>e}getTilePoint(t){const e=Math.pow(2,this.z);return new s((t.x*e-this.x)*$a,(t.y*e-this.y)*$a)}toString(){return `${this.z}/${this.x}/${this.y}`}}class Oc{constructor(t,e){this.wrap=t,this.canonical=e,this.key=Rc(t,e.z,e.z,e.x,e.y);}}class jc{constructor(t,e,r,n,i){if(t= z; overscaledZ = ${t}; z = ${r}`);this.overscaledZ=t,this.wrap=e,this.canonical=new Dc(r,+n,+i),this.key=Rc(e,t,r,n,i);}clone(){return new jc(this.overscaledZ,this.wrap,this.canonical.z,this.canonical.x,this.canonical.y)}equals(t){return this.overscaledZ===t.overscaledZ&&this.wrap===t.wrap&&this.canonical.equals(t.canonical)}scaledTo(t){if(t>this.overscaledZ)throw new Error(`targetZ > this.overscaledZ; targetZ = ${t}; overscaledZ = ${this.overscaledZ}`);const e=this.canonical.z-t;return t>this.canonical.z?new jc(t,this.wrap,this.canonical.z,this.canonical.x,this.canonical.y):new jc(t,this.wrap,t,this.canonical.x>>e,this.canonical.y>>e)}calculateScaledKey(t,e){if(t>this.overscaledZ)throw new Error(`targetZ > this.overscaledZ; targetZ = ${t}; overscaledZ = ${this.overscaledZ}`);const r=this.canonical.z-t;return t>this.canonical.z?Rc(this.wrap*+e,t,this.canonical.z,this.canonical.x,this.canonical.y):Rc(this.wrap*+e,t,t,this.canonical.x>>r,this.canonical.y>>r)}isChildOf(t){if(t.wrap!==this.wrap)return !1;const e=this.canonical.z-t.canonical.z;return 0===t.overscaledZ||t.overscaledZ>e&&t.canonical.y===this.canonical.y>>e}children(t){if(this.overscaledZ>=t)return [new jc(this.overscaledZ+1,this.wrap,this.canonical.z,this.canonical.x,this.canonical.y)];const e=this.canonical.z+1,r=2*this.canonical.x,n=2*this.canonical.y;return [new jc(e,this.wrap,e,r,n),new jc(e,this.wrap,e,r+1,n),new jc(e,this.wrap,e,r,n+1),new jc(e,this.wrap,e,r+1,n+1)]}isLessThan(t){return this.wrapt.wrap)&&(this.overscaledZt.overscaledZ)&&(this.canonical.xt.canonical.x)&&this.canonical.ythis.max&&(this.max=r),r=this.dim+1||e<-1||e>=this.dim+1)throw new RangeError("out of range source coordinates for DEM data");return (e+1)*this.stride+(t+1)}unpack(t,e,r){return t*this.redFactor+e*this.greenFactor+r*this.blueFactor-this.baseShift}getPixels(){return new Ao({width:this.stride,height:this.stride},new Uint8Array(this.data.buffer))}backfillBorder(t,e,r){if(this.dim!==t.dim)throw new Error("dem dimension mismatch");let n=e*this.dim,i=e*this.dim+this.dim,s=r*this.dim,a=r*this.dim+this.dim;switch(e){case-1:n=i-1;break;case 1:i=n+1;}switch(r){case-1:s=a-1;break;case 1:a=s+1;}const o=-e*this.dim,l=-r*this.dim;for(let e=s;e=this._numberToString.length)throw new Error(`Out of bounds. Index requested n=${t} can't be >= this._numberToString.length ${this._numberToString.length}`);return this._numberToString[t]}}class Nc{constructor(t,e,r,n,i){this.type="Feature",this._vectorTileFeature=t,t._z=e,t._x=r,t._y=n,this.properties=t.properties,this.id=i;}get geometry(){return void 0===this._geometry&&(this._geometry=this._vectorTileFeature.toGeoJSON(this._vectorTileFeature._x,this._vectorTileFeature._y,this._vectorTileFeature._z).geometry),this._geometry}set geometry(t){this._geometry=t;}toJSON(){const t={geometry:this.geometry};for(const e in this)"_geometry"!==e&&"_vectorTileFeature"!==e&&(t[e]=this[e]);return t}}class Zc{constructor(t,e){this.tileID=t,this.x=t.canonical.x,this.y=t.canonical.y,this.z=t.canonical.z,this.grid=new vi($a,16,0),this.grid3D=new vi($a,16,0),this.featureIndexArray=new Ns,this.promoteId=e;}insert(t,e,r,n,i,s){const a=this.featureIndexArray.length;this.featureIndexArray.emplaceBack(r,n,i);const o=s?this.grid3D:this.grid;for(let t=0;t=0&&n[3]>=0&&o.insert(a,n[0],n[1],n[2],n[3]);}}loadVTLayers(){return this.vtLayers||(this.vtLayers=new fl.VectorTile(new Iu(this.rawTileData)).layers,this.sourceLayerCoder=new qc(this.vtLayers?Object.keys(this.vtLayers).sort():["_geojsonTileLayer"])),this.vtLayers}query(t,e,r,n){this.loadVTLayers();const i=t.params||{},a=$a/t.tileSize/t.scale,o=zn(i.filter),l=t.queryGeometry,u=t.queryPadding*a,c=Kc(l),h=this.grid.query(c.minX-u,c.minY-u,c.maxX+u,c.maxY+u),p=Kc(t.cameraQueryGeometry),f=this.grid3D.query(p.minX-u,p.minY-u,p.maxX+u,p.maxY+u,((e,r,n,i)=>function(t,e,r,n,i){for(const s of t)if(e<=s.x&&r<=s.y&&n>=s.x&&i>=s.y)return !0;const a=[new s(e,r),new s(e,i),new s(n,i),new s(n,r)];if(t.length>2)for(const e of a)if(Wa(t,e))return !0;for(let e=0;e(p||(p=Oa(e)),r.queryIntersectsFeature(l,e,n,p,this.z,t.transform,a,t.pixelPosMatrix))));}return d}loadMatchingFeature(t,e,r,n,i,s,a,o,l,u,c){const h=this.bucketLayerIDs[e];if(s&&!function(t,e){for(let r=0;r=0)return !0;return !1}(s,h))return;const p=this.sourceLayerCoder.decode(r),f=this.vtLayers[p].feature(n);if(i.needGeometry){const t=ja(f,!0);if(!i.filter(new Ui(this.tileID.overscaledZ),t,this.tileID.canonical))return}else if(!i.filter(new Ui(this.tileID.overscaledZ),f))return;const d=this.getId(f,p);for(let e=0;e{const a=e instanceof Yi?e.get(s):null;return a&&a.evaluate?a.evaluate(r,n,i):a}))}function Kc(t){let e=1/0,r=1/0,n=-1/0,i=-1/0;for(const s of t)e=Math.min(e,s.x),r=Math.min(r,s.y),n=Math.max(n,s.x),i=Math.max(i,s.y);return {minX:e,minY:r,maxX:n,maxY:i}}function Xc(t,e){return e-t}function Hc(t,e,r,n,i){const a=[];for(let o=0;o=n&&c.x>=n||(o.x>=n?o=new s(n,o.y+(n-o.x)/(c.x-o.x)*(c.y-o.y))._round():c.x>=n&&(c=new s(n,o.y+(n-o.x)/(c.x-o.x)*(c.y-o.y))._round()),o.y>=i&&c.y>=i||(o.y>=i?o=new s(o.x+(i-o.y)/(c.y-o.y)*(c.x-o.x),i)._round():c.y>=i&&(c=new s(o.x+(i-o.y)/(c.y-o.y)*(c.x-o.x),i)._round()),u&&o.equals(u[u.length-1])||(u=[o],a.push(u)),u.push(c)))));}}return a}wi("FeatureIndex",Zc,{omit:["rawTileData","sourceLayerCoder"]});class Yc extends s{constructor(t,e,r,n){super(t,e),this.angle=r,void 0!==n&&(this.segment=n);}clone(){return new Yc(this.x,this.y,this.angle,this.segment)}}function Jc(t,e,r,n,i){if(void 0===e.segment||0===r)return !0;let s=e,a=e.segment+1,o=0;for(;o>-r/2;){if(a--,a<0)return !1;o-=t[a].dist(s),s=t[a];}o+=t[a].dist(t[a+1]),a++;const l=[];let u=0;for(;on;)u-=l.shift().angleDelta;if(u>i)return !1;a++,o+=e.dist(r);}return !0}function Wc(t){let e=0;for(let r=0;ru){const c=(u-l)/s,h=Ce.number(n.x,i.x,c),p=Ce.number(n.y,i.y,c),f=new Yc(h,p,i.angleTo(n),r);return f._round(),!a||Jc(t,f,o,a,e)?f:void 0}l+=s;}}function rh(t,e,r,n,i,s,a,o,l){const u=Qc(n,s,a),c=th(n,i),h=c*a,p=0===t[0].x||t[0].x===l||0===t[0].y||t[0].y===l;return e-h=0&&g=0&&x=0&&p+u<=c){const r=new Yc(g,x,y,e);r._round(),n&&!Jc(t,r,s,n,i)||f.push(r);}}h+=d;}return o||f.length||a||(f=nh(t,h/2,r,n,i,s,a,!0,l)),f}wi("Anchor",Yc);const ih=Fu;function sh(t,e,r,n){const i=[],a=t.image,o=a.pixelRatio,l=a.paddedRect.w-2*ih,u=a.paddedRect.h-2*ih;let c={x1:t.left,y1:t.top,x2:t.right,y2:t.bottom};const h=a.stretchX||[[0,l]],p=a.stretchY||[[0,u]],f=(t,e)=>t+e[1]-e[0],d=h.reduce(f,0),y=p.reduce(f,0),m=l-d,g=u-y;let x=0,v=d,b=0,w=y,_=0,A=m,S=0,k=g;if(a.content&&n){const e=a.content,r=e[2]-e[0],n=e[3]-e[1];(a.textFitWidth||a.textFitHeight)&&(c=tc(t)),x=ah(h,0,e[0]),b=ah(p,0,e[1]),v=ah(h,e[0],e[2]),w=ah(p,e[1],e[3]),_=e[0]-x,S=e[1]-b,A=r-v,k=n-w;}const M=c.x1,I=c.y1,z=c.x2-M,P=c.y2-I,C=(t,n,i,l)=>{const u=lh(t.stretch-x,v,z,M),c=uh(t.fixed-_,A,t.stretch,d),h=lh(n.stretch-b,w,P,I),p=uh(n.fixed-S,k,n.stretch,y),f=lh(i.stretch-x,v,z,M),m=uh(i.fixed-_,A,i.stretch,d),g=lh(l.stretch-b,w,P,I),C=uh(l.fixed-S,k,l.stretch,y),B=new s(u,h),V=new s(f,h),E=new s(f,g),F=new s(u,g),T=new s(c/o,p/o),$=new s(m/o,C/o),L=e*Math.PI/180;if(L){const t=Math.sin(L),e=Math.cos(L),r=[e,-t,t,e];B._matMult(r),V._matMult(r),F._matMult(r),E._matMult(r);}const D=t.stretch+t.fixed,O=n.stretch+n.fixed;return {tl:B,tr:V,bl:F,br:E,tex:{x:a.paddedRect.x+ih+D,y:a.paddedRect.y+ih+O,w:i.stretch+i.fixed-D,h:l.stretch+l.fixed-O},writingMode:void 0,glyphOffset:[0,0],sectionIndex:0,pixelOffsetTL:T,pixelOffsetBR:$,minFontScaleX:A/o/z,minFontScaleY:k/o/P,isSDF:r}};if(n&&(a.stretchX||a.stretchY)){const t=oh(h,m,d),e=oh(p,g,y);for(let r=0;r0&&(n=Math.max(10,n),this.circleDiameter=n);}else {const u=(null===(h=a.image)||void 0===h?void 0:h.content)&&(a.image.textFitWidth||a.image.textFitHeight)?tc(a):{x1:a.left,y1:a.top,x2:a.right,y2:a.bottom};u.y1=u.y1*o-l[0],u.y2=u.y2*o+l[2],u.x1=u.x1*o-l[3],u.x2=u.x2*o+l[1];const p=a.collisionPadding;if(p&&(u.x1-=p[0]*o,u.y1-=p[1]*o,u.x2+=p[2]*o,u.y2+=p[3]*o),c){const t=new s(u.x1,u.y1),e=new s(u.x2,u.y1),r=new s(u.x1,u.y2),n=new s(u.x2,u.y2),i=c*Math.PI/180;t._rotate(i),e._rotate(i),r._rotate(i),n._rotate(i),u.x1=Math.min(t.x,e.x,r.x,n.x),u.x2=Math.max(t.x,e.x,r.x,n.x),u.y1=Math.min(t.y,e.y,r.y,n.y),u.y2=Math.max(t.y,e.y,r.y,n.y);}t.emplaceBack(e.x,e.y,u.x1,u.y1,u.x2,u.y2,r,n,i);}this.boxEndIndex=t.length;}}class hh{constructor(t=[],e=((t,e)=>te?1:0)){if(this.data=t,this.length=this.data.length,this.compare=e,this.length>0)for(let t=(this.length>>1)-1;t>=0;t--)this._down(t);}push(t){this.data.push(t),this._up(this.length++);}pop(){if(0===this.length)return;const t=this.data[0],e=this.data.pop();return --this.length>0&&(this.data[0]=e,this._down(0)),t}peek(){return this.data[0]}_up(t){const{data:e,compare:r}=this,n=e[t];for(;t>0;){const i=t-1>>1,s=e[i];if(r(n,s)>=0)break;e[t]=s,t=i;}e[t]=n;}_down(t){const{data:e,compare:r}=this,n=this.length>>1,i=e[t];for(;t=0)break;e[t]=e[n],t=n;}e[t]=i;}}function ph(t,e=1,r=!1){let n=1/0,i=1/0,a=-1/0,o=-1/0;const l=t[0];for(let t=0;ta)&&(a=e.x),(!t||e.y>o)&&(o=e.y);}const u=Math.min(a-n,o-i);let c=u/2;const h=new hh([],fh);if(0===u)return new s(n,i);for(let e=n;ep.d||!p.d)&&(p=n,r&&console.log("found best %d after %d probes",Math.round(1e4*n.d)/1e4,f)),n.max-p.d<=e||(c=n.h/2,h.push(new dh(n.p.x-c,n.p.y-c,c,t)),h.push(new dh(n.p.x+c,n.p.y-c,c,t)),h.push(new dh(n.p.x-c,n.p.y+c,c,t)),h.push(new dh(n.p.x+c,n.p.y+c,c,t)),f+=4);}return r&&(console.log(`num probes: ${f}`),console.log(`best distance: ${p.d}`)),p.p}function fh(t,e){return e.max-t.max}function dh(t,e,r,n){this.p=new s(t,e),this.h=r,this.d=function(t,e){let r=!1,n=1/0;for(let i=0;it.y!=o.y>t.y&&t.x<(o.x-i.x)*(t.y-i.y)/(o.y-i.y)+i.x&&(r=!r),n=Math.min(n,Ya(t,i,o));}}return (r?1:-1)*Math.sqrt(n)}(this.p,n),this.max=this.d+this.h*Math.SQRT2;}var yh;t.aq=void 0,(yh=t.aq||(t.aq={}))[yh.center=1]="center",yh[yh.left=2]="left",yh[yh.right=3]="right",yh[yh.top=4]="top",yh[yh.bottom=5]="bottom",yh[yh["top-left"]=6]="top-left",yh[yh["top-right"]=7]="top-right",yh[yh["bottom-left"]=8]="bottom-left",yh[yh["bottom-right"]=9]="bottom-right";const mh=7,gh=Number.POSITIVE_INFINITY;function xh(t,e){return e[1]!==gh?function(t,e,r){let n=0,i=0;switch(e=Math.abs(e),r=Math.abs(r),t){case"top-right":case"top-left":case"top":i=r-mh;break;case"bottom-right":case"bottom-left":case"bottom":i=-r+mh;}switch(t){case"top-right":case"bottom-right":case"right":n=-e;break;case"top-left":case"bottom-left":case"left":n=e;}return [n,i]}(t,e[0],e[1]):function(t,e){let r=0,n=0;e<0&&(e=0);const i=e/Math.SQRT2;switch(t){case"top-right":case"top-left":n=i-mh;break;case"bottom-right":case"bottom-left":n=-i+mh;break;case"bottom":n=-e+mh;break;case"top":n=e-mh;}switch(t){case"top-right":case"bottom-right":r=-i;break;case"top-left":case"bottom-left":r=i;break;case"left":r=e;break;case"right":r=-e;}return [r,n]}(t,e[0])}function vh(t,e,r){var n;const i=t.layout,s=null===(n=i.get("text-variable-anchor-offset"))||void 0===n?void 0:n.evaluate(e,{},r);if(s){const t=s.values,e=[];for(let r=0;rt*iu));n.startsWith("top")?i[1]-=mh:n.startsWith("bottom")&&(i[1]+=mh),e[r+1]=i;}return new Qt(e)}const a=i.get("text-variable-anchor");if(a){let n;n=void 0!==t._unevaluatedLayout.getValue("text-radial-offset")?[i.get("text-radial-offset").evaluate(e,{},r)*iu,gh]:i.get("text-offset").evaluate(e,{},r).map((t=>t*iu));const s=[];for(const t of a)s.push(t,xh(t,n));return new Qt(s)}return null}function bh(t){switch(t){case"right":case"top-right":case"bottom-right":return "right";case"left":case"top-left":case"bottom-left":return "left"}return "center"}function wh(e,r,n,i,s,a,o,l,u,c,h){let p=a.textMaxSize.evaluate(r,{});void 0===p&&(p=o);const f=e.layers[0].layout,d=f.get("icon-offset").evaluate(r,{},h),y=Ah(n.horizontal),m=o/24,g=e.tilePixelRatio*m,x=e.tilePixelRatio*p/24,v=e.tilePixelRatio*l,b=e.tilePixelRatio*f.get("symbol-spacing"),w=f.get("text-padding")*e.tilePixelRatio,_=function(t,e,r,n=1){const i=t.get("icon-padding").evaluate(e,{},r),s=i&&i.values;return [s[0]*n,s[1]*n,s[2]*n,s[3]*n]}(f,r,h,e.tilePixelRatio),S=f.get("text-max-angle")/180*Math.PI,k="viewport"!==f.get("text-rotation-alignment")&&"point"!==f.get("symbol-placement"),M="map"===f.get("icon-rotation-alignment")&&"point"!==f.get("symbol-placement"),I=f.get("symbol-placement"),z=b/2,P=f.get("icon-text-fit");let C;i&&"none"!==P&&(e.allowVerticalPlacement&&n.vertical&&(C=ec(i,n.vertical,P,f.get("icon-text-fit-padding"),d,m)),y&&(i=ec(i,y,P,f.get("icon-text-fit-padding"),d,m)));const B=(l,p)=>{p.x<0||p.x>=$a||p.y<0||p.y>=$a||function(e,r,n,i,s,a,o,l,u,c,h,p,f,d,y,m,g,x,v,b,w,_,S,k,M){const I=e.addToLineVertexArray(r,n);let z,P,C,B,V=0,E=0,F=0,T=0,$=-1,L=-1;const D={};let O=ma("");if(e.allowVerticalPlacement&&i.vertical){const t=l.layout.get("text-rotate").evaluate(w,{},k)+90;C=new ch(u,r,c,h,p,i.vertical,f,d,y,t),o&&(B=new ch(u,r,c,h,p,o,g,x,y,t));}if(s){const n=l.layout.get("icon-rotate").evaluate(w,{}),i="none"!==l.layout.get("icon-text-fit"),a=sh(s,n,S,i),f=o?sh(o,n,S,i):void 0;P=new ch(u,r,c,h,p,s,g,x,!1,n),V=4*a.length;const d=e.iconSizeData;let y=null;"source"===d.kind?(y=[nc*l.layout.get("icon-size").evaluate(w,{})],y[0]>ic&&A(`${e.layerIds[0]}: Value for "icon-size" is >= ${rc}. Reduce your "icon-size".`)):"composite"===d.kind&&(y=[nc*_.compositeIconSizes[0].evaluate(w,{},k),nc*_.compositeIconSizes[1].evaluate(w,{},k)],(y[0]>ic||y[1]>ic)&&A(`${e.layerIds[0]}: Value for "icon-size" is >= ${rc}. Reduce your "icon-size".`)),e.addSymbols(e.icon,a,y,b,v,w,t.ah.none,r,I.lineStartIndex,I.lineLength,-1,k),$=e.icon.placedSymbolArray.length-1,f&&(E=4*f.length,e.addSymbols(e.icon,f,y,b,v,w,t.ah.vertical,r,I.lineStartIndex,I.lineLength,-1,k),L=e.icon.placedSymbolArray.length-1);}const j=Object.keys(i.horizontal);for(const n of j){const s=i.horizontal[n];if(!z){O=ma(s.text);const t=l.layout.get("text-rotate").evaluate(w,{},k);z=new ch(u,r,c,h,p,s,f,d,y,t);}const o=1===s.positionedLines.length;if(F+=_h(e,r,s,a,l,y,w,m,I,i.vertical?t.ah.horizontal:t.ah.horizontalOnly,o?j:[n],D,$,_,k),o)break}i.vertical&&(T+=_h(e,r,i.vertical,a,l,y,w,m,I,t.ah.vertical,["vertical"],D,L,_,k));const R=z?z.boxStartIndex:e.collisionBoxArray.length,U=z?z.boxEndIndex:e.collisionBoxArray.length,q=C?C.boxStartIndex:e.collisionBoxArray.length,N=C?C.boxEndIndex:e.collisionBoxArray.length,Z=P?P.boxStartIndex:e.collisionBoxArray.length,G=P?P.boxEndIndex:e.collisionBoxArray.length,K=B?B.boxStartIndex:e.collisionBoxArray.length,X=B?B.boxEndIndex:e.collisionBoxArray.length;let H=-1;const Y=(t,e)=>t&&t.circleDiameter?Math.max(t.circleDiameter,e):e;H=Y(z,H),H=Y(C,H),H=Y(P,H),H=Y(B,H);const J=H>-1?1:0;J&&(H*=M/iu),e.glyphOffsetArray.length>=dc.MAX_GLYPHS&&A("Too many glyphs being rendered in a tile. See https://github.com/mapbox/mapbox-gl-js/issues/2907"),void 0!==w.sortKey&&e.addToSortKeyRanges(e.symbolInstances.length,w.sortKey);const W=vh(l,w,k),[Q,tt]=function(e,r){const n=e.length,i=null==r?void 0:r.values;if((null==i?void 0:i.length)>0)for(let r=0;r=0?D.right:-1,D.center>=0?D.center:-1,D.left>=0?D.left:-1,D.vertical||-1,$,L,O,R,U,q,N,Z,G,K,X,c,F,T,V,E,J,0,f,H,Q,tt);}(e,p,l,n,i,s,C,e.layers[0],e.collisionBoxArray,r.index,r.sourceLayerIndex,e.index,g,[w,w,w,w],k,u,v,_,M,d,r,a,c,h,o);};if("line"===I)for(const t of Hc(r.geometry,0,0,$a,$a)){const r=rh(t,b,S,n.vertical||y,i,24,x,e.overscaling,$a);for(const n of r)y&&Sh(e,y.text,z,n)||B(t,n);}else if("line-center"===I){for(const t of r.geometry)if(t.length>1){const e=eh(t,S,n.vertical||y,i,24,x);e&&B(t,e);}}else if("Polygon"===r.type)for(const t of xr(r.geometry,0)){const e=ph(t,16);B(t[0],new Yc(e.x,e.y,0));}else if("LineString"===r.type)for(const t of r.geometry)B(t,new Yc(t[0].x,t[0].y,0));else if("Point"===r.type)for(const t of r.geometry)for(const e of t)B([e],new Yc(e.x,e.y,0));}function _h(t,e,r,n,i,a,o,l,u,c,h,p,f,d,y){const m=function(t,e,r,n,i,a,o,l){const u=n.layout.get("text-rotate").evaluate(a,{})*Math.PI/180,c=[];for(const t of e.positionedLines)for(const n of t.positionedGlyphs){if(!n.rect)continue;const a=n.rect||{};let h=Vu+1,p=!0,f=1,d=0;const y=(i||l)&&n.vertical,m=n.metrics.advance*n.scale/2;if(l&&e.verticalizable&&(d=t.lineOffset/2-(n.imageName?-(iu-n.metrics.width*n.scale)/2:(n.scale-1)*iu)),n.imageName){const t=o[n.imageName];p=t.sdf,f=t.pixelRatio,h=Fu/f;}const g=i?[n.x+m,n.y]:[0,0];let x=i?[0,0]:[n.x+m+r[0],n.y+r[1]-d],v=[0,0];y&&(v=x,x=[0,0]);const b=n.metrics.isDoubleResolution?2:1,w=(n.metrics.left-h)*n.scale-m+x[0],_=(-n.metrics.top-h)*n.scale+x[1],A=w+a.w/b*n.scale/f,S=_+a.h/b*n.scale/f,k=new s(w,_),M=new s(A,_),I=new s(w,S),z=new s(A,S);if(y){const t=new s(-m,m-Du),e=-Math.PI/2,r=iu/2-m,i=new s(5-Du-r,-(n.imageName?r:0)),a=new s(...v);k._rotateAround(e,t)._add(i)._add(a),M._rotateAround(e,t)._add(i)._add(a),I._rotateAround(e,t)._add(i)._add(a),z._rotateAround(e,t)._add(i)._add(a);}if(u){const t=Math.sin(u),e=Math.cos(u),r=[e,-t,t,e];k._matMult(r),M._matMult(r),I._matMult(r),z._matMult(r);}const P=new s(0,0),C=new s(0,0);c.push({tl:k,tr:M,bl:I,br:z,tex:a,writingMode:e.writingMode,glyphOffset:g,sectionIndex:n.sectionIndex,isSDF:p,pixelOffsetTL:P,pixelOffsetBR:C,minFontScaleX:0,minFontScaleY:0});}return c}(0,r,l,i,a,o,n,t.allowVerticalPlacement),g=t.textSizeData;let x=null;"source"===g.kind?(x=[nc*i.layout.get("text-size").evaluate(o,{})],x[0]>ic&&A(`${t.layerIds[0]}: Value for "text-size" is >= ${rc}. Reduce your "text-size".`)):"composite"===g.kind&&(x=[nc*d.compositeTextSizes[0].evaluate(o,{},y),nc*d.compositeTextSizes[1].evaluate(o,{},y)],(x[0]>ic||x[1]>ic)&&A(`${t.layerIds[0]}: Value for "text-size" is >= ${rc}. Reduce your "text-size".`)),t.addSymbols(t.text,m,x,l,a,o,c,e,u.lineStartIndex,u.lineLength,f,y);for(const e of h)p[e]=t.text.placedSymbolArray.length-1;return 4*m.length}function Ah(t){for(const e in t)return t[e];return null}function Sh(t,e,r,n){const i=t.compareText;if(e in i){const t=i[e];for(let e=t.length-1;e>=0;e--)if(n.dist(t[e])>4;if(1!==n)throw new Error(`Got v${n} data when expected v1.`);const i=kh[15&r];if(!i)throw new Error("Unrecognized array type.");const[s]=new Uint16Array(t,2,1),[a]=new Uint32Array(t,4,1);return new Mh(a,s,i,t)}constructor(t,e=64,r=Float64Array,n){if(isNaN(t)||t<0)throw new Error(`Unpexpected numItems value: ${t}.`);this.numItems=+t,this.nodeSize=Math.min(Math.max(+e,2),65535),this.ArrayType=r,this.IndexArrayType=t<65536?Uint16Array:Uint32Array;const i=kh.indexOf(this.ArrayType),s=2*t*this.ArrayType.BYTES_PER_ELEMENT,a=t*this.IndexArrayType.BYTES_PER_ELEMENT,o=(8-a%8)%8;if(i<0)throw new Error(`Unexpected typed array class: ${r}.`);n&&n instanceof ArrayBuffer?(this.data=n,this.ids=new this.IndexArrayType(this.data,8,t),this.coords=new this.ArrayType(this.data,8+a+o,2*t),this._pos=2*t,this._finished=!0):(this.data=new ArrayBuffer(8+s+a+o),this.ids=new this.IndexArrayType(this.data,8,t),this.coords=new this.ArrayType(this.data,8+a+o,2*t),this._pos=0,this._finished=!1,new Uint8Array(this.data,0,2).set([219,16+i]),new Uint16Array(this.data,2,1)[0]=e,new Uint32Array(this.data,4,1)[0]=t);}add(t,e){const r=this._pos>>1;return this.ids[r]=r,this.coords[this._pos++]=t,this.coords[this._pos++]=e,r}finish(){const t=this._pos>>1;if(t!==this.numItems)throw new Error(`Added ${t} items when expected ${this.numItems}.`);return Ih(this.ids,this.coords,this.nodeSize,0,this.numItems-1,0),this._finished=!0,this}range(t,e,r,n){if(!this._finished)throw new Error("Data not yet indexed - call index.finish().");const{ids:i,coords:s,nodeSize:a}=this,o=[0,i.length-1,0],l=[];for(;o.length;){const u=o.pop()||0,c=o.pop()||0,h=o.pop()||0;if(c-h<=a){for(let a=h;a<=c;a++){const o=s[2*a],u=s[2*a+1];o>=t&&o<=r&&u>=e&&u<=n&&l.push(i[a]);}continue}const p=h+c>>1,f=s[2*p],d=s[2*p+1];f>=t&&f<=r&&d>=e&&d<=n&&l.push(i[p]),(0===u?t<=f:e<=d)&&(o.push(h),o.push(p-1),o.push(1-u)),(0===u?r>=f:n>=d)&&(o.push(p+1),o.push(c),o.push(1-u));}return l}within(t,e,r){if(!this._finished)throw new Error("Data not yet indexed - call index.finish().");const{ids:n,coords:i,nodeSize:s}=this,a=[0,n.length-1,0],o=[],l=r*r;for(;a.length;){const u=a.pop()||0,c=a.pop()||0,h=a.pop()||0;if(c-h<=s){for(let r=h;r<=c;r++)Bh(i[2*r],i[2*r+1],t,e)<=l&&o.push(n[r]);continue}const p=h+c>>1,f=i[2*p],d=i[2*p+1];Bh(f,d,t,e)<=l&&o.push(n[p]),(0===u?t-r<=f:e-r<=d)&&(a.push(h),a.push(p-1),a.push(1-u)),(0===u?t+r>=f:e+r>=d)&&(a.push(p+1),a.push(c),a.push(1-u));}return o}}function Ih(t,e,r,n,i,s){if(i-n<=r)return;const a=n+i>>1;zh(t,e,a,n,i,s),Ih(t,e,r,n,a-1,1-s),Ih(t,e,r,a+1,i,1-s);}function zh(t,e,r,n,i,s){for(;i>n;){if(i-n>600){const a=i-n+1,o=r-n+1,l=Math.log(a),u=.5*Math.exp(2*l/3),c=.5*Math.sqrt(l*u*(a-u)/a)*(o-a/2<0?-1:1);zh(t,e,r,Math.max(n,Math.floor(r-o*u/a+c)),Math.min(i,Math.floor(r+(a-o)*u/a+c)),s);}const a=e[2*r+s];let o=n,l=i;for(Ph(t,e,n,r),e[2*i+s]>a&&Ph(t,e,n,i);oa;)l--;}e[2*n+s]===a?Ph(t,e,n,l):(l++,Ph(t,e,l,i)),l<=r&&(n=l+1),r<=l&&(i=l-1);}}function Ph(t,e,r,n){Ch(t,r,n),Ch(e,2*r,2*n),Ch(e,2*r+1,2*n+1);}function Ch(t,e,r){const n=t[e];t[e]=t[r],t[r]=n;}function Bh(t,e,r,n){const i=t-r,s=e-n;return i*i+s*s}var Vh;t.bg=void 0,(Vh=t.bg||(t.bg={})).create="create",Vh.load="load",Vh.fullLoad="fullLoad";let Eh=null,Fh=[];const Th=1e3/60,$h="loadTime",Lh="fullLoadTime",Dh={mark(t){performance.mark(t);},frame(t){const e=t;null!=Eh&&Fh.push(e-Eh),Eh=e;},clearMetrics(){Eh=null,Fh=[],performance.clearMeasures($h),performance.clearMeasures(Lh);for(const e in t.bg)performance.clearMarks(t.bg[e]);},getPerformanceMetrics(){performance.measure($h,t.bg.create,t.bg.load),performance.measure(Lh,t.bg.create,t.bg.fullLoad);const e=performance.getEntriesByName($h)[0].duration,r=performance.getEntriesByName(Lh)[0].duration,n=Fh.length,i=1/(Fh.reduce(((t,e)=>t+e),0)/n/1e3),s=Fh.filter((t=>t>Th)).reduce(((t,e)=>t+(e-Th)/Th),0);return {loadTime:e,fullLoadTime:r,fps:i,percentDroppedFrames:s/(n+s)*100,totalFrames:n}}};t.$=class extends ps{},t.A=oo,t.B=yi,t.C=function(t){if(null==M){const e=t.navigator?t.navigator.userAgent:null;M=!!t.safari||!(!e||!(/\b(iPad|iPhone|iPod)\b/.test(e)||e.match("Safari")&&!e.match("Chrome")));}return M},t.D=Ji,t.E=Z,t.F=class{constructor(t,e){this.target=t,this.mapId=e,this.resolveRejects={},this.tasks={},this.taskQueue=[],this.abortControllers={},this.messageHandlers={},this.invoker=new Ic((()=>this.process())),this.subscription=function(t,e,r,n){return t.addEventListener(e,r,!1),{unsubscribe:()=>{t.removeEventListener(e,r,!1);}}}(this.target,"message",(t=>this.receive(t))),this.globalScope=k(self)?t:window;}registerMessageHandler(t,e){this.messageHandlers[t]=e;}sendAsync(t,e){return new Promise(((r,n)=>{const i=Math.round(1e18*Math.random()).toString(36).substring(0,10);this.resolveRejects[i]={resolve:r,reject:n},e&&e.signal.addEventListener("abort",(()=>{delete this.resolveRejects[i];const e={id:i,type:"",origin:location.origin,targetMapId:t.targetMapId,sourceMapId:this.mapId};this.target.postMessage(e);}),{once:!0});const s=[],a=Object.assign(Object.assign({},t),{id:i,sourceMapId:this.mapId,origin:location.origin,data:ki(t.data,s)});this.target.postMessage(a,{transfer:s});}))}receive(t){const e=t.data,r=e.id;if(!("file://"!==e.origin&&"file://"!==location.origin&&"resource://android"!==e.origin&&"resource://android"!==location.origin&&e.origin!==location.origin||e.targetMapId&&this.mapId!==e.targetMapId)){if(""===e.type){delete this.tasks[r];const t=this.abortControllers[r];return delete this.abortControllers[r],void(t&&t.abort())}if(k(self)||e.mustQueue)return this.tasks[r]=e,this.taskQueue.push(r),void this.invoker.trigger();this.processTask(r,e);}}process(){if(0===this.taskQueue.length)return;const t=this.taskQueue.shift(),e=this.tasks[t];delete this.tasks[t],this.taskQueue.length>0&&this.invoker.trigger(),e&&this.processTask(t,e);}processTask(t,r){return e(this,void 0,void 0,(function*(){if(""===r.type){const e=this.resolveRejects[t];if(delete this.resolveRejects[t],!e)return;return void(r.error?e.reject(Mi(r.error)):e.resolve(Mi(r.data)))}if(!this.messageHandlers[r.type])return void this.completeTask(t,new Error(`Could not find a registered handler for ${r.type}, map ID: ${this.mapId}, available handlers: ${Object.keys(this.messageHandlers).join(", ")}`));const e=Mi(r.data),n=new AbortController;this.abortControllers[t]=n;try{const i=yield this.messageHandlers[r.type](r.sourceMapId,e,n);this.completeTask(t,null,i);}catch(e){this.completeTask(t,e);}}))}completeTask(t,e,r){const n=[];delete this.abortControllers[t];const i={id:t,type:"",sourceMapId:this.mapId,origin:location.origin,error:e?ki(e):null,data:ki(r,n)};this.target.postMessage(i,{transfer:n});}remove(){this.invoker.remove(),this.subscription.unsubscribe();}},t.G=$,t.H=function(){var t=new oo(16);return oo!=Float32Array&&(t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[11]=0,t[12]=0,t[13]=0,t[14]=0),t[0]=1,t[5]=1,t[10]=1,t[15]=1,t},t.I=Tu,t.J=function(t,e,r){var n,i,s,a,o,l,u,c,h,p,f,d,y=r[0],m=r[1],g=r[2];return e===t?(t[12]=e[0]*y+e[4]*m+e[8]*g+e[12],t[13]=e[1]*y+e[5]*m+e[9]*g+e[13],t[14]=e[2]*y+e[6]*m+e[10]*g+e[14],t[15]=e[3]*y+e[7]*m+e[11]*g+e[15]):(i=e[1],s=e[2],a=e[3],o=e[4],l=e[5],u=e[6],c=e[7],h=e[8],p=e[9],f=e[10],d=e[11],t[0]=n=e[0],t[1]=i,t[2]=s,t[3]=a,t[4]=o,t[5]=l,t[6]=u,t[7]=c,t[8]=h,t[9]=p,t[10]=f,t[11]=d,t[12]=n*y+o*m+h*g+e[12],t[13]=i*y+l*m+p*g+e[13],t[14]=s*y+u*m+f*g+e[14],t[15]=a*y+c*m+d*g+e[15]),t},t.K=function(t,e,r){var n=r[0],i=r[1],s=r[2];return t[0]=e[0]*n,t[1]=e[1]*n,t[2]=e[2]*n,t[3]=e[3]*n,t[4]=e[4]*i,t[5]=e[5]*i,t[6]=e[6]*i,t[7]=e[7]*i,t[8]=e[8]*s,t[9]=e[9]*s,t[10]=e[10]*s,t[11]=e[11]*s,t[12]=e[12],t[13]=e[13],t[14]=e[14],t[15]=e[15],t},t.L=uo,t.M=function(t,e){const r={};for(let n=0;n{const e=window.document.createElement("video");return e.muted=!0,new Promise((r=>{e.onloadstart=()=>{r(e);};for(const r of t){const t=window.document.createElement("source");j(r)||(e.crossOrigin="Anonymous"),t.src=r,e.appendChild(t);}}))},t.a4=function(){return x++},t.a5=Fs,t.a6=dc,t.a7=zn,t.a8=ja,t.a9=Nc,t.aA=function(t){if("custom"===t.type)return new Mc(t);switch(t.type){case"background":return new _c(t);case"circle":return new fo(t);case"fill":return new ul(t);case"fill-extrusion":return new El(t);case"heatmap":return new Mo(t);case"hillshade":return new Po(t);case"line":return new Hl(t);case"raster":return new kc(t);case"symbol":return new vc(t)}},t.aB=w,t.aC=function(t,e){if(!t)return [{command:"setStyle",args:[e]}];let r=[];try{if(!H(t.version,e.version))return [{command:"setStyle",args:[e]}];H(t.center,e.center)||r.push({command:"setCenter",args:[e.center]}),H(t.zoom,e.zoom)||r.push({command:"setZoom",args:[e.zoom]}),H(t.bearing,e.bearing)||r.push({command:"setBearing",args:[e.bearing]}),H(t.pitch,e.pitch)||r.push({command:"setPitch",args:[e.pitch]}),H(t.sprite,e.sprite)||r.push({command:"setSprite",args:[e.sprite]}),H(t.glyphs,e.glyphs)||r.push({command:"setGlyphs",args:[e.glyphs]}),H(t.transition,e.transition)||r.push({command:"setTransition",args:[e.transition]}),H(t.light,e.light)||r.push({command:"setLight",args:[e.light]}),H(t.terrain,e.terrain)||r.push({command:"setTerrain",args:[e.terrain]}),H(t.sky,e.sky)||r.push({command:"setSky",args:[e.sky]}),H(t.projection,e.projection)||r.push({command:"setProjection",args:[e.projection]});const n={},i=[];!function(t,e,r,n){let i;for(i in e=e||{},t=t||{})Object.prototype.hasOwnProperty.call(t,i)&&(Object.prototype.hasOwnProperty.call(e,i)||W(i,r,n));for(i in e)Object.prototype.hasOwnProperty.call(e,i)&&(Object.prototype.hasOwnProperty.call(t,i)?H(t[i],e[i])||("geojson"===t[i].type&&"geojson"===e[i].type&&tt(t,e,i)?Y(r,{command:"setGeoJSONSourceData",args:[i,e[i].data]}):Q(i,e,r,n)):J(i,e,r));}(t.sources,e.sources,i,n);const s=[];t.layers&&t.layers.forEach((t=>{"source"in t&&n[t.source]?r.push({command:"removeLayer",args:[t.id]}):s.push(t);})),r=r.concat(i),function(t,e,r){e=e||[];const n=(t=t||[]).map(rt),i=e.map(rt),s=t.reduce(nt,{}),a=e.reduce(nt,{}),o=n.slice(),l=Object.create(null);let u,c,h,p,f;for(let t=0,e=0;t@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)(?:\=(?:([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)|(?:\"((?:[^"\\]|\\.)*)\")))?/g,((t,r,n,i)=>{const s=n||i;return e[r]=!s||s.toLowerCase(),""})),e["max-age"]){const t=parseInt(e["max-age"],10);isNaN(t)?delete e["max-age"]:e["max-age"]=t;}return e},t.ab=function(t,e){const r=[];for(const n in t)n in e||r.push(n);return r},t.ac=y,t.ad=function(t,e,r){var n=Math.sin(r),i=Math.cos(r),s=e[0],a=e[1],o=e[2],l=e[3],u=e[4],c=e[5],h=e[6],p=e[7];return e!==t&&(t[8]=e[8],t[9]=e[9],t[10]=e[10],t[11]=e[11],t[12]=e[12],t[13]=e[13],t[14]=e[14],t[15]=e[15]),t[0]=s*i+u*n,t[1]=a*i+c*n,t[2]=o*i+h*n,t[3]=l*i+p*n,t[4]=u*i-s*n,t[5]=c*i-a*n,t[6]=h*i-o*n,t[7]=p*i-l*n,t},t.ae=function(t){var e=new oo(16);return e[0]=t[0],e[1]=t[1],e[2]=t[2],e[3]=t[3],e[4]=t[4],e[5]=t[5],e[6]=t[6],e[7]=t[7],e[8]=t[8],e[9]=t[9],e[10]=t[10],e[11]=t[11],e[12]=t[12],e[13]=t[13],e[14]=t[14],e[15]=t[15],e},t.af=po,t.ag=function(t,e){let r=0,n=0;if("constant"===t.kind)n=t.layoutSize;else if("source"!==t.kind){const{interpolationType:i,minZoom:s,maxZoom:a}=t,o=i?y(Be.interpolationFactor(i,e,s,a),0,1):0;"camera"===t.kind?n=Ce.number(t.minSize,t.maxSize,o):r=o;}return {uSizeT:r,uSize:n}},t.ai=function(t,{uSize:e,uSizeT:r},{lowerSize:n,upperSize:i}){return "source"===t.kind?n/nc:"composite"===t.kind?Ce.number(n/nc,i/nc,r):e},t.aj=cc,t.ak=function(t,e,r,n){const i=e.y-t.y,a=e.x-t.x,o=n.y-r.y,l=n.x-r.x,u=o*a-l*i;if(0===u)return null;const c=(l*(t.y-r.y)-o*(t.x-r.x))/u;return new s(t.x+c*a,t.y+c*i)},t.al=Hc,t.am=qa,t.an=lo,t.ao=function(t){let e=1/0,r=1/0,n=-1/0,i=-1/0;for(const s of t)e=Math.min(e,s.x),r=Math.min(r,s.y),n=Math.max(n,s.x),i=Math.max(i,s.y);return [e,r,n,i]},t.ap=iu,t.ar=ac,t.as=function(t,e){var r=e[0],n=e[1],i=e[2],s=e[3],a=e[4],o=e[5],l=e[6],u=e[7],c=e[8],h=e[9],p=e[10],f=e[11],d=e[12],y=e[13],m=e[14],g=e[15],x=r*o-n*a,v=r*l-i*a,b=r*u-s*a,w=n*l-i*o,_=n*u-s*o,A=i*u-s*l,S=c*y-h*d,k=c*m-p*d,M=c*g-f*d,I=h*m-p*y,z=h*g-f*y,P=p*g-f*m,C=x*P-v*z+b*I+w*M-_*k+A*S;return C?(t[0]=(o*P-l*z+u*I)*(C=1/C),t[1]=(i*z-n*P-s*I)*C,t[2]=(y*A-m*_+g*w)*C,t[3]=(p*_-h*A-f*w)*C,t[4]=(l*M-a*P-u*k)*C,t[5]=(r*P-i*M+s*k)*C,t[6]=(m*b-d*A-g*v)*C,t[7]=(c*A-p*b+f*v)*C,t[8]=(a*z-o*M+u*S)*C,t[9]=(n*M-r*z-s*S)*C,t[10]=(d*_-y*b+g*x)*C,t[11]=(h*b-c*_-f*x)*C,t[12]=(o*k-a*I-l*S)*C,t[13]=(r*I-n*k+i*S)*C,t[14]=(y*v-d*w-m*x)*C,t[15]=(c*w-h*v+p*x)*C,t):null},t.at=bh,t.au=Ju,t.av=Mh,t.aw=function(){const t={},e=G.$version;for(const r in G.$root){const n=G.$root[r];if(n.required){let i=null;i="version"===r?e:"array"===n.type?[]:{},null!=i&&(t[r]=i);}}return t},t.ax=Ii,t.ay=D,t.az=function(t){t=t.slice();const e=Object.create(null);for(let r=0;r25||n<0||n>=1||r<0||r>=1)},t.bc=function(t,e){return t[0]=e[0],t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=e[1],t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[10]=e[2],t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,t},t.bd=class extends hs{},t.be=zc,t.bf=Dh,t.bh=L,t.bi=function(t,e){F.REGISTERED_PROTOCOLS[t]=e;},t.bj=function(t){delete F.REGISTERED_PROTOCOLS[t];},t.bk=function(t,e){const r={};for(let n=0;nt*iu));}let v=o?"center":n.get("text-justify").evaluate(i,{},e.canonical);const b="point"===n.get("symbol-placement")?n.get("text-max-width").evaluate(i,{},e.canonical)*iu:1/0,w=()=>{e.bucket.allowVerticalPlacement&&Pi(s)&&(d.vertical=Ru(y,e.glyphMap,e.glyphPositions,e.imagePositions,c,b,a,m,"left",f,g,t.ah.vertical,!0,p,h));};if(!o&&x){const r=new Set;if("auto"===v)for(let t=0;te(void 0,void 0,void 0,(function*(){if(0===t.byteLength)return createImageBitmap(new ImageData(1,1));const e=new Blob([new Uint8Array(t)],{type:"image/png"});try{return createImageBitmap(e)}catch(t){throw new Error(`Could not load image because of ${t.message}. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.`)}})),t.e=g,t.f=t=>new Promise(((e,r)=>{const n=new Image;n.onload=()=>{e(n),URL.revokeObjectURL(n.src),n.onload=null,window.requestAnimationFrame((()=>{n.src=z;}));},n.onerror=()=>r(new Error("Could not load image. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported."));const i=new Blob([new Uint8Array(t)],{type:"image/png"});n.src=t.byteLength?URL.createObjectURL(i):z;})),t.g=T,t.h=(t,e)=>O(g(t,{type:"json"}),e),t.i=k,t.j=N,t.k=q,t.l=(t,e)=>O(g(t,{type:"arrayBuffer"}),e),t.m=O,t.n=function(t){return new Iu(t).readFields(Pu,[])},t.o=_o,t.p=Eu,t.q=rs,t.r=di,t.s=j,t.t=xi,t.u=fi,t.v=G,t.w=A,t.x=function([t,e,r]){return e+=90,e*=Math.PI/180,r*=Math.PI/180,{x:t*Math.cos(e)*Math.sin(r),y:t*Math.sin(e)*Math.sin(r),z:t*Math.cos(r)}},t.y=Ce,t.z=Ui;})); + +define("worker",["./shared"],(function(e){"use strict";class t{constructor(e){this.keyCache={},e&&this.replace(e);}replace(e){this._layerConfigs={},this._layers={},this.update(e,[]);}update(t,o){for(const o of t){this._layerConfigs[o.id]=o;const t=this._layers[o.id]=e.aA(o);t._featureFilter=e.a7(t.filter),this.keyCache[o.id]&&delete this.keyCache[o.id];}for(const e of o)delete this.keyCache[e],delete this._layerConfigs[e],delete this._layers[e];this.familiesBySource={};const i=e.bk(Object.values(this._layerConfigs),this.keyCache);for(const e of i){const t=e.map((e=>this._layers[e.id])),o=t[0];if("none"===o.visibility)continue;const i=o.source||"";let s=this.familiesBySource[i];s||(s=this.familiesBySource[i]={});const r=o.sourceLayer||"_geojsonTileLayer";let n=s[r];n||(n=s[r]=[]),n.push(t);}}}class o{constructor(t){const o={},i=[];for(const e in t){const s=t[e],r=o[e]={};for(const e in s){const t=s[+e];if(!t||0===t.bitmap.width||0===t.bitmap.height)continue;const o={x:0,y:0,w:t.bitmap.width+2,h:t.bitmap.height+2};i.push(o),r[e]={rect:o,metrics:t.metrics};}}const{w:s,h:r}=e.p(i),n=new e.o({width:s||1,height:r||1});for(const i in t){const s=t[i];for(const t in s){const r=s[+t];if(!r||0===r.bitmap.width||0===r.bitmap.height)continue;const a=o[i][t].rect;e.o.copy(r.bitmap,n,{x:0,y:0},{x:a.x+1,y:a.y+1},r.bitmap);}}this.image=n,this.positions=o;}}e.bl("GlyphAtlas",o);class i{constructor(t){this.tileID=new e.S(t.tileID.overscaledZ,t.tileID.wrap,t.tileID.canonical.z,t.tileID.canonical.x,t.tileID.canonical.y),this.uid=t.uid,this.zoom=t.zoom,this.pixelRatio=t.pixelRatio,this.tileSize=t.tileSize,this.source=t.source,this.overscaling=this.tileID.overscaleFactor(),this.showCollisionBoxes=t.showCollisionBoxes,this.collectResourceTiming=!!t.collectResourceTiming,this.returnDependencies=!!t.returnDependencies,this.promoteId=t.promoteId,this.inFlightDependencies=[];}parse(t,i,r,n){return e._(this,void 0,void 0,(function*(){this.status="parsing",this.data=t,this.collisionBoxArray=new e.a5;const a=new e.bm(Object.keys(t.layers).sort()),l=new e.bn(this.tileID,this.promoteId);l.bucketLayerIDs=[];const c={},u={featureIndex:l,iconDependencies:{},patternDependencies:{},glyphDependencies:{},availableImages:r},h=i.familiesBySource[this.source];for(const o in h){const i=t.layers[o];if(!i)continue;1===i.version&&e.w(`Vector tile source "${this.source}" layer "${o}" does not use vector tile spec v2 and therefore may have some rendering errors.`);const n=a.encode(o),d=[];for(let e=0;e=o.maxzoom||"none"!==o.visibility&&(s(t,this.zoom,r),(c[o.id]=o.createBucket({index:l.bucketLayerIDs.length,layers:t,zoom:this.zoom,pixelRatio:this.pixelRatio,overscaling:this.overscaling,collisionBoxArray:this.collisionBoxArray,sourceLayerIndex:n,sourceID:this.source})).populate(d,u,this.tileID.canonical),l.bucketLayerIDs.push(t.map((e=>e.id))));}}const d=e.aF(u.glyphDependencies,(e=>Object.keys(e).map(Number)));this.inFlightDependencies.forEach((e=>null==e?void 0:e.abort())),this.inFlightDependencies=[];let f=Promise.resolve({});if(Object.keys(d).length){const e=new AbortController;this.inFlightDependencies.push(e),f=n.sendAsync({type:"GG",data:{stacks:d,source:this.source,tileID:this.tileID,type:"glyphs"}},e);}const g=Object.keys(u.iconDependencies);let p=Promise.resolve({});if(g.length){const e=new AbortController;this.inFlightDependencies.push(e),p=n.sendAsync({type:"GI",data:{icons:g,source:this.source,tileID:this.tileID,type:"icons"}},e);}const m=Object.keys(u.patternDependencies);let y=Promise.resolve({});if(m.length){const e=new AbortController;this.inFlightDependencies.push(e),y=n.sendAsync({type:"GI",data:{icons:m,source:this.source,tileID:this.tileID,type:"patterns"}},e);}const[v,w,x]=yield Promise.all([f,p,y]),b=new o(v),S=new e.bo(w,x);for(const t in c){const o=c[t];o instanceof e.a6?(s(o.layers,this.zoom,r),e.bp({bucket:o,glyphMap:v,glyphPositions:b.positions,imageMap:w,imagePositions:S.iconPositions,showCollisionBoxes:this.showCollisionBoxes,canonical:this.tileID.canonical})):o.hasPattern&&(o instanceof e.bq||o instanceof e.br||o instanceof e.bs)&&(s(o.layers,this.zoom,r),o.addFeatures(u,this.tileID.canonical,S.patternPositions));}return this.status="done",{buckets:Object.values(c).filter((e=>!e.isEmpty())),featureIndex:l,collisionBoxArray:this.collisionBoxArray,glyphAtlasImage:b.image,imageAtlas:S,glyphMap:this.returnDependencies?v:null,iconMap:this.returnDependencies?w:null,glyphPositions:this.returnDependencies?b.positions:null}}))}}function s(t,o,i){const s=new e.z(o);for(const e of t)e.recalculate(s,i);}class r{constructor(e,t,o){this.actor=e,this.layerIndex=t,this.availableImages=o,this.fetching={},this.loading={},this.loaded={};}loadVectorTile(t,o){return e._(this,void 0,void 0,(function*(){const i=yield e.l(t.request,o);try{return {vectorTile:new e.bt.VectorTile(new e.bu(i.data)),rawData:i.data,cacheControl:i.cacheControl,expires:i.expires}}catch(e){const o=new Uint8Array(i.data);let s=`Unable to parse the tile at ${t.request.url}, `;throw s+=31===o[0]&&139===o[1]?"please make sure the data is not gzipped and that you have configured the relevant header in the server":`got error: ${e.message}`,new Error(s)}}))}loadTile(t){return e._(this,void 0,void 0,(function*(){const o=t.uid,s=!!(t&&t.request&&t.request.collectResourceTiming)&&new e.bv(t.request),r=new i(t);this.loading[o]=r;const n=new AbortController;r.abort=n;try{const i=yield this.loadVectorTile(t,n);if(delete this.loading[o],!i)return null;const a=i.rawData,l={};i.expires&&(l.expires=i.expires),i.cacheControl&&(l.cacheControl=i.cacheControl);const c={};if(s){const e=s.finish();e&&(c.resourceTiming=JSON.parse(JSON.stringify(e)));}r.vectorTile=i.vectorTile;const u=r.parse(i.vectorTile,this.layerIndex,this.availableImages,this.actor);this.loaded[o]=r,this.fetching[o]={rawTileData:a,cacheControl:l,resourceTiming:c};try{const t=yield u;return e.e({rawTileData:a.slice(0)},t,l,c)}finally{delete this.fetching[o];}}catch(e){throw delete this.loading[o],r.status="done",this.loaded[o]=r,e}}))}reloadTile(t){return e._(this,void 0,void 0,(function*(){const o=t.uid;if(!this.loaded||!this.loaded[o])throw new Error("Should not be trying to reload a tile that was never loaded or has been removed");const i=this.loaded[o];if(i.showCollisionBoxes=t.showCollisionBoxes,"parsing"===i.status){const t=yield i.parse(i.vectorTile,this.layerIndex,this.availableImages,this.actor);let s;if(this.fetching[o]){const{rawTileData:i,cacheControl:r,resourceTiming:n}=this.fetching[o];delete this.fetching[o],s=e.e({rawTileData:i.slice(0)},t,r,n);}else s=t;return s}if("done"===i.status&&i.vectorTile)return i.parse(i.vectorTile,this.layerIndex,this.availableImages,this.actor)}))}abortTile(t){return e._(this,void 0,void 0,(function*(){const e=this.loading,o=t.uid;e&&e[o]&&e[o].abort&&(e[o].abort.abort(),delete e[o]);}))}removeTile(t){return e._(this,void 0,void 0,(function*(){this.loaded&&this.loaded[t.uid]&&delete this.loaded[t.uid];}))}}class n{constructor(){this.loaded={};}loadTile(t){return e._(this,void 0,void 0,(function*(){const{uid:o,encoding:i,rawImageData:s,redFactor:r,greenFactor:n,blueFactor:a,baseShift:l}=t,c=s.width+2,u=s.height+2,h=e.b(s)?new e.R({width:c,height:u},yield e.bw(s,-1,-1,c,u)):s,d=new e.bx(o,h,i,r,n,a,l);return this.loaded=this.loaded||{},this.loaded[o]=d,d}))}removeTile(e){const t=this.loaded,o=e.uid;t&&t[o]&&delete t[o];}}function a(e,t){if(0!==e.length){l(e[0],t);for(var o=1;o=Math.abs(a)?o-l+a:a-l+o,o=l;}o+i>=0!=!!t&&e.reverse();}var c=e.by((function e(t,o){var i,s=t&&t.type;if("FeatureCollection"===s)for(i=0;i>31}function I(e,t){for(var o=e.loadGeometry(),i=e.type,s=0,r=0,n=o.length,a=0;ae},D=Math.fround||(C=new Float32Array(1),e=>(C[0]=+e,C[0]));var C;const L=3,O=5,F=6;class z{constructor(e){this.options=Object.assign(Object.create(T),e),this.trees=new Array(this.options.maxZoom+1),this.stride=this.options.reduce?7:6,this.clusterProps=[];}load(e){const{log:t,minZoom:o,maxZoom:i}=this.options;t&&console.time("total time");const s=`prepare ${e.length} points`;t&&console.time(s),this.points=e;const r=[];for(let t=0;t=o;e--){const o=+Date.now();n=this.trees[e]=this._createTree(this._cluster(n,e)),t&&console.log("z%d: %d clusters in %dms",e,n.numItems,+Date.now()-o);}return t&&console.timeEnd("total time"),this}getClusters(e,t){let o=((e[0]+180)%360+360)%360-180;const i=Math.max(-90,Math.min(90,e[1]));let s=180===e[2]?180:((e[2]+180)%360+360)%360-180;const r=Math.max(-90,Math.min(90,e[3]));if(e[2]-e[0]>=360)o=-180,s=180;else if(o>s){const e=this.getClusters([o,i,180,r],t),n=this.getClusters([-180,i,s,r],t);return e.concat(n)}const n=this.trees[this._limitZoom(t)],a=n.range(G(o),j(r),G(s),j(i)),l=n.data,c=[];for(const e of a){const t=this.stride*e;c.push(l[t+O]>1?A(l,t,this.clusterProps):this.points[l[t+L]]);}return c}getChildren(e){const t=this._getOriginId(e),o=this._getOriginZoom(e),i="No cluster with the specified id.",s=this.trees[o];if(!s)throw new Error(i);const r=s.data;if(t*this.stride>=r.length)throw new Error(i);const n=this.options.radius/(this.options.extent*Math.pow(2,o-1)),a=s.within(r[t*this.stride],r[t*this.stride+1],n),l=[];for(const t of a){const o=t*this.stride;r[o+4]===e&&l.push(r[o+O]>1?A(r,o,this.clusterProps):this.points[r[o+L]]);}if(0===l.length)throw new Error(i);return l}getLeaves(e,t,o){const i=[];return this._appendLeaves(i,e,t=t||10,o=o||0,0),i}getTile(e,t,o){const i=this.trees[this._limitZoom(e)],s=Math.pow(2,e),{extent:r,radius:n}=this.options,a=n/r,l=(o-a)/s,c=(o+1+a)/s,u={features:[]};return this._addTileFeatures(i.range((t-a)/s,l,(t+1+a)/s,c),i.data,t,o,s,u),0===t&&this._addTileFeatures(i.range(1-a/s,l,1,c),i.data,s,o,s,u),t===s-1&&this._addTileFeatures(i.range(0,l,a/s,c),i.data,-1,o,s,u),u.features.length?u:null}getClusterExpansionZoom(e){let t=this._getOriginZoom(e)-1;for(;t<=this.options.maxZoom;){const o=this.getChildren(e);if(t++,1!==o.length)break;e=o[0].properties.cluster_id;}return t}_appendLeaves(e,t,o,i,s){const r=this.getChildren(t);for(const t of r){const r=t.properties;if(r&&r.cluster?s+r.point_count<=i?s+=r.point_count:s=this._appendLeaves(e,r.cluster_id,o,i,s):s1;let l,c,u;if(a)l=E(t,e,this.clusterProps),c=t[e],u=t[e+1];else {const o=this.points[t[e+L]];l=o.properties;const[i,s]=o.geometry.coordinates;c=G(i),u=j(s);}const h={type:1,geometry:[[Math.round(this.options.extent*(c*s-o)),Math.round(this.options.extent*(u*s-i))]],tags:l};let d;d=a||this.options.generateId?t[e+L]:this.points[t[e+L]].id,void 0!==d&&(h.id=d),r.features.push(h);}}_limitZoom(e){return Math.max(this.options.minZoom,Math.min(Math.floor(+e),this.options.maxZoom+1))}_cluster(e,t){const{radius:o,extent:i,reduce:s,minPoints:r}=this.options,n=o/(i*Math.pow(2,t)),a=e.data,l=[],c=this.stride;for(let o=0;ot&&(f+=a[o+O]);}if(f>d&&f>=r){let e,r=i*d,n=u*d,g=-1;const p=((o/c|0)<<5)+(t+1)+this.points.length;for(const i of h){const l=i*c;if(a[l+2]<=t)continue;a[l+2]=t;const u=a[l+O];r+=a[l]*u,n+=a[l+1]*u,a[l+4]=p,s&&(e||(e=this._map(a,o,!0),g=this.clusterProps.length,this.clusterProps.push(e)),s(e,this._map(a,l)));}a[o+4]=p,l.push(r/f,n/f,1/0,p,-1,f),s&&l.push(g);}else {for(let e=0;e1)for(const e of h){const o=e*c;if(!(a[o+2]<=t)){a[o+2]=t;for(let e=0;e>5}_getOriginZoom(e){return (e-this.points.length)%32}_map(e,t,o){if(e[t+O]>1){const i=this.clusterProps[e[t+F]];return o?Object.assign({},i):i}const i=this.points[e[t+L]].properties,s=this.options.map(i);return o&&s===i?Object.assign({},s):s}}function A(e,t,o){return {type:"Feature",id:e[t+L],properties:E(e,t,o),geometry:{type:"Point",coordinates:[(i=e[t],360*(i-.5)),R(e[t+1])]}};var i;}function E(e,t,o){const i=e[t+O],s=i>=1e4?`${Math.round(i/1e3)}k`:i>=1e3?Math.round(i/100)/10+"k":i,r=e[t+F],n=-1===r?{}:Object.assign({},o[r]);return Object.assign(n,{cluster:!0,cluster_id:e[t+L],point_count:i,point_count_abbreviated:s})}function G(e){return e/360+.5}function j(e){const t=Math.sin(e*Math.PI/180),o=.5-.25*Math.log((1+t)/(1-t))/Math.PI;return o<0?0:o>1?1:o}function R(e){const t=(180-360*e)*Math.PI/180;return 360*Math.atan(Math.exp(t))/Math.PI-90}function Z(e,t,o,i){let s=i;const r=t+(o-t>>1);let n,a=o-t;const l=e[t],c=e[t+1],u=e[o],h=e[o+1];for(let i=t+3;is)n=i,s=t;else if(t===s){const e=Math.abs(i-r);ei&&(n-t>3&&Z(e,t,n,i),e[n+2]=s,o-n>3&&Z(e,n,o,i));}function N(e,t,o,i,s,r){let n=s-o,a=r-i;if(0!==n||0!==a){const l=((e-o)*n+(t-i)*a)/(n*n+a*a);l>1?(o=s,i=r):l>0&&(o+=n*l,i+=a*l);}return n=e-o,a=t-i,n*n+a*a}function J(e,t,o,i){const s={id:null==e?null:e,type:t,geometry:o,tags:i,minX:1/0,minY:1/0,maxX:-1/0,maxY:-1/0};if("Point"===t||"MultiPoint"===t||"LineString"===t)W(s,o);else if("Polygon"===t)W(s,o[0]);else if("MultiLineString"===t)for(const e of o)W(s,e);else if("MultiPolygon"===t)for(const e of o)W(s,e[0]);return s}function W(e,t){for(let o=0;o0&&(n+=i?(s*l-a*r)/2:Math.sqrt(Math.pow(a-s,2)+Math.pow(l-r,2))),s=a,r=l;}const a=t.length-3;t[2]=1,Z(t,0,a,o),t[a+2]=1,t.size=Math.abs(n),t.start=0,t.end=t.size;}function q(e,t,o,i){for(let s=0;s1?1:o}function U(e,t,o,i,s,r,n,a){if(i/=t,r>=(o/=t)&&n=i)return null;const l=[];for(const t of e){const e=t.geometry;let r=t.type;const n=0===s?t.minX:t.minY,c=0===s?t.maxX:t.maxY;if(n>=o&&c=i)continue;let u=[];if("Point"===r||"MultiPoint"===r)$(e,u,o,i,s);else if("LineString"===r)K(e,u,o,i,s,!1,a.lineMetrics);else if("MultiLineString"===r)ee(e,u,o,i,s,!1);else if("Polygon"===r)ee(e,u,o,i,s,!0);else if("MultiPolygon"===r)for(const t of e){const e=[];ee(t,e,o,i,s,!0),e.length&&u.push(e);}if(u.length){if(a.lineMetrics&&"LineString"===r){for(const e of u)l.push(J(t.id,r,e,t.tags));continue}"LineString"!==r&&"MultiLineString"!==r||(1===u.length?(r="LineString",u=u[0]):r="MultiLineString"),"Point"!==r&&"MultiPoint"!==r||(r=3===u.length?"Point":"MultiPoint"),l.push(J(t.id,r,u,t.tags));}}return l.length?l:null}function $(e,t,o,i,s){for(let r=0;r=o&&n<=i&&te(t,e[r],e[r+1],e[r+2]);}}function K(e,t,o,i,s,r,n){let a=Q(e);const l=0===s?oe:ie;let c,u,h=e.start;for(let d=0;do&&(u=l(a,f,g,m,y,o),n&&(a.start=h+c*u)):v>i?w=o&&(u=l(a,f,g,m,y,o),x=!0),w>i&&v<=i&&(u=l(a,f,g,m,y,i),x=!0),!r&&x&&(n&&(a.end=h+c*u),t.push(a),a=Q(e)),n&&(h+=c);}let d=e.length-3;const f=e[d],g=e[d+1],p=0===s?f:g;p>=o&&p<=i&&te(a,f,g,e[d+2]),d=a.length-3,r&&d>=3&&(a[d]!==a[0]||a[d+1]!==a[1])&&te(a,a[0],a[1],a[2]),a.length&&t.push(a);}function Q(e){const t=[];return t.size=e.size,t.start=e.start,t.end=e.end,t}function ee(e,t,o,i,s,r){for(const n of e)K(n,t,o,i,s,r,!1);}function te(e,t,o,i){e.push(t,o,i);}function oe(e,t,o,i,s,r){const n=(r-t)/(i-t);return te(e,r,o+(s-o)*n,1),n}function ie(e,t,o,i,s,r){const n=(r-o)/(s-o);return te(e,t+(i-t)*n,r,1),n}function se(e,t){const o=[];for(let i=0;i0&&t.size<(s?n:i))return void(o.numPoints+=t.length/3);const a=[];for(let e=0;en)&&(o.numSimplified++,a.push(t[e],t[e+1])),o.numPoints++;s&&function(e,t){let o=0;for(let t=0,i=e.length,s=i-2;t0===t)for(let t=0,o=e.length;t24)throw new Error("maxZoom should be in the 0-24 range");if(t.promoteId&&t.generateId)throw new Error("promoteId and generateId cannot be used together.");let i=function(e,t){const o=[];if("FeatureCollection"===e.type)for(let i=0;i1&&console.time("creation"),d=this.tiles[h]=le(e,t,o,i,l),this.tileCoords.push({z:t,x:o,y:i}),c)){c>1&&(console.log("tile z%d-%d-%d (features: %d, points: %d, simplified: %d)",t,o,i,d.numFeatures,d.numPoints,d.numSimplified),console.timeEnd("creation"));const e=`z${t}`;this.stats[e]=(this.stats[e]||0)+1,this.total++;}if(d.source=e,null==s){if(t===l.indexMaxZoom||d.numPoints<=l.indexMaxPoints)continue}else {if(t===l.maxZoom||t===s)continue;if(null!=s){const e=s-t;if(o!==r>>e||i!==n>>e)continue}}if(d.source=null,0===e.length)continue;c>1&&console.time("clipping");const f=.5*l.buffer/l.extent,g=.5-f,p=.5+f,m=1+f;let y=null,v=null,w=null,x=null,b=U(e,u,o-f,o+p,0,d.minX,d.maxX,l),S=U(e,u,o+g,o+m,0,d.minX,d.maxX,l);e=null,b&&(y=U(b,u,i-f,i+p,1,d.minY,d.maxY,l),v=U(b,u,i+g,i+m,1,d.minY,d.maxY,l),b=null),S&&(w=U(S,u,i-f,i+p,1,d.minY,d.maxY,l),x=U(S,u,i+g,i+m,1,d.minY,d.maxY,l),S=null),c>1&&console.timeEnd("clipping"),a.push(y||[],t+1,2*o,2*i),a.push(v||[],t+1,2*o,2*i+1),a.push(w||[],t+1,2*o+1,2*i),a.push(x||[],t+1,2*o+1,2*i+1);}}getTile(e,t,o){e=+e,t=+t,o=+o;const i=this.options,{extent:s,debug:r}=i;if(e<0||e>24)return null;const n=1<1&&console.log("drilling down to z%d-%d-%d",e,t,o);let l,c=e,u=t,h=o;for(;!l&&c>0;)c--,u>>=1,h>>=1,l=this.tiles[fe(c,u,h)];return l&&l.source?(r>1&&(console.log("found parent tile z%d-%d-%d",c,u,h),console.time("drilling down")),this.splitTile(l.source,c,u,h,e,t,o),r>1&&console.timeEnd("drilling down"),this.tiles[a]?ne(this.tiles[a],s):null):null}}function fe(e,t,o){return 32*((1<{n.properties=e;const t={};for(const e of a)t[e]=i[e].evaluate(r,n);return t},t.reduce=(e,t)=>{n.properties=t;for(const t of a)r.accumulated=e[t],e[t]=s[t].evaluate(r,n);},t}(t)).load((yield this._pendingData).features):(s=yield this._pendingData,new de(s,t.geojsonVtOptions)),this.loaded={};const o={};if(i){const e=i.finish();e&&(o.resourceTiming={},o.resourceTiming[t.source]=JSON.parse(JSON.stringify(e)));}return o}catch(t){if(delete this._pendingRequest,e.bB(t))return {abandoned:!0};throw t}var s;}))}getData(){return e._(this,void 0,void 0,(function*(){return this._pendingData}))}reloadTile(e){const t=this.loaded;return t&&t[e.uid]?super.reloadTile(e):this.loadTile(e)}loadAndProcessGeoJSON(t,o){return e._(this,void 0,void 0,(function*(){let i=yield this.loadGeoJSON(t,o);if(delete this._pendingRequest,"object"!=typeof i)throw new Error(`Input data given to '${t.source}' is not a valid GeoJSON object.`);if(c(i,!0),t.filter){const o=e.bC(t.filter,{type:"boolean","property-type":"data-driven",overridable:!1,transition:!1});if("error"===o.result)throw new Error(o.value.map((e=>`${e.key}: ${e.message}`)).join(", "));const s=i.features.filter((e=>o.value.evaluate({zoom:0},e)));i={type:"FeatureCollection",features:s};}return i}))}loadGeoJSON(t,o){return e._(this,void 0,void 0,(function*(){const{promoteId:i}=t;if(t.request){const s=yield e.h(t.request,o);return this._dataUpdateable=pe(s.data,i)?me(s.data,i):void 0,s.data}if("string"==typeof t.data)try{const e=JSON.parse(t.data);return this._dataUpdateable=pe(e,i)?me(e,i):void 0,e}catch(e){throw new Error(`Input data given to '${t.source}' is not a valid GeoJSON object.`)}if(!t.dataDiff)throw new Error(`Input data given to '${t.source}' is not a valid GeoJSON object.`);if(!this._dataUpdateable)throw new Error(`Cannot update existing geojson data in ${t.source}`);return function(e,t,o){var i,s,r,n;if(t.removeAll&&e.clear(),t.remove)for(const o of t.remove)e.delete(o);if(t.add)for(const i of t.add){const t=ge(i,o);null!=t&&e.set(t,i);}if(t.update)for(const o of t.update){let t=e.get(o.id);if(null==t)continue;const a=!o.removeAllProperties&&((null===(i=o.removeProperties)||void 0===i?void 0:i.length)>0||(null===(s=o.addOrUpdateProperties)||void 0===s?void 0:s.length)>0);if((o.newGeometry||o.removeAllProperties||a)&&(t=Object.assign({},t),e.set(o.id,t),a&&(t.properties=Object.assign({},t.properties))),o.newGeometry&&(t.geometry=o.newGeometry),o.removeAllProperties)t.properties={};else if((null===(r=o.removeProperties)||void 0===r?void 0:r.length)>0)for(const e of o.removeProperties)Object.prototype.hasOwnProperty.call(t.properties,e)&&delete t.properties[e];if((null===(n=o.addOrUpdateProperties)||void 0===n?void 0:n.length)>0)for(const{key:e,value:i}of o.addOrUpdateProperties)t.properties[e]=i;}}(this._dataUpdateable,t.dataDiff,i),{type:"FeatureCollection",features:Array.from(this._dataUpdateable.values())}}))}removeSource(t){return e._(this,void 0,void 0,(function*(){this._pendingRequest&&this._pendingRequest.abort();}))}getClusterExpansionZoom(e){return this._geoJSONIndex.getClusterExpansionZoom(e.clusterId)}getClusterChildren(e){return this._geoJSONIndex.getChildren(e.clusterId)}getClusterLeaves(e){return this._geoJSONIndex.getLeaves(e.clusterId,e.limit,e.offset)}}class ve{constructor(t){this.self=t,this.actor=new e.F(t),this.layerIndexes={},this.availableImages={},this.workerSources={},this.demWorkerSources={},this.externalWorkerSourceTypes={},this.self.registerWorkerSource=(e,t)=>{if(this.externalWorkerSourceTypes[e])throw new Error(`Worker source with name "${e}" already registered.`);this.externalWorkerSourceTypes[e]=t;},this.self.addProtocol=e.bi,this.self.removeProtocol=e.bj,this.self.registerRTLTextPlugin=t=>{if(e.bD.isParsed())throw new Error("RTL text plugin already registered.");e.bD.setMethods(t);},this.actor.registerMessageHandler("LDT",((e,t)=>this._getDEMWorkerSource(e,t.source).loadTile(t))),this.actor.registerMessageHandler("RDT",((t,o)=>e._(this,void 0,void 0,(function*(){this._getDEMWorkerSource(t,o.source).removeTile(o);})))),this.actor.registerMessageHandler("GCEZ",((t,o)=>e._(this,void 0,void 0,(function*(){return this._getWorkerSource(t,o.type,o.source).getClusterExpansionZoom(o)})))),this.actor.registerMessageHandler("GCC",((t,o)=>e._(this,void 0,void 0,(function*(){return this._getWorkerSource(t,o.type,o.source).getClusterChildren(o)})))),this.actor.registerMessageHandler("GCL",((t,o)=>e._(this,void 0,void 0,(function*(){return this._getWorkerSource(t,o.type,o.source).getClusterLeaves(o)})))),this.actor.registerMessageHandler("LD",((e,t)=>this._getWorkerSource(e,t.type,t.source).loadData(t))),this.actor.registerMessageHandler("GD",((e,t)=>this._getWorkerSource(e,t.type,t.source).getData())),this.actor.registerMessageHandler("LT",((e,t)=>this._getWorkerSource(e,t.type,t.source).loadTile(t))),this.actor.registerMessageHandler("RT",((e,t)=>this._getWorkerSource(e,t.type,t.source).reloadTile(t))),this.actor.registerMessageHandler("AT",((e,t)=>this._getWorkerSource(e,t.type,t.source).abortTile(t))),this.actor.registerMessageHandler("RMT",((e,t)=>this._getWorkerSource(e,t.type,t.source).removeTile(t))),this.actor.registerMessageHandler("RS",((t,o)=>e._(this,void 0,void 0,(function*(){if(!this.workerSources[t]||!this.workerSources[t][o.type]||!this.workerSources[t][o.type][o.source])return;const e=this.workerSources[t][o.type][o.source];delete this.workerSources[t][o.type][o.source],void 0!==e.removeSource&&e.removeSource(o);})))),this.actor.registerMessageHandler("RM",(t=>e._(this,void 0,void 0,(function*(){delete this.layerIndexes[t],delete this.availableImages[t],delete this.workerSources[t],delete this.demWorkerSources[t];})))),this.actor.registerMessageHandler("SR",((t,o)=>e._(this,void 0,void 0,(function*(){this.referrer=o;})))),this.actor.registerMessageHandler("SRPS",((e,t)=>this._syncRTLPluginState(e,t))),this.actor.registerMessageHandler("IS",((t,o)=>e._(this,void 0,void 0,(function*(){this.self.importScripts(o);})))),this.actor.registerMessageHandler("SI",((e,t)=>this._setImages(e,t))),this.actor.registerMessageHandler("UL",((t,o)=>e._(this,void 0,void 0,(function*(){this._getLayerIndex(t).update(o.layers,o.removedIds);})))),this.actor.registerMessageHandler("SL",((t,o)=>e._(this,void 0,void 0,(function*(){this._getLayerIndex(t).replace(o);}))));}_setImages(t,o){return e._(this,void 0,void 0,(function*(){this.availableImages[t]=o;for(const e in this.workerSources[t]){const i=this.workerSources[t][e];for(const e in i)i[e].availableImages=o;}}))}_syncRTLPluginState(t,o){return e._(this,void 0,void 0,(function*(){if(e.bD.isParsed())return e.bD.getState();if("loading"!==o.pluginStatus)return e.bD.setState(o),o;const t=o.pluginURL;if(this.self.importScripts(t),e.bD.isParsed()){const o={pluginStatus:"loaded",pluginURL:t};return e.bD.setState(o),o}throw e.bD.setState({pluginStatus:"error",pluginURL:""}),new Error(`RTL Text Plugin failed to import scripts from ${t}`)}))}_getAvailableImages(e){let t=this.availableImages[e];return t||(t=[]),t}_getLayerIndex(e){let o=this.layerIndexes[e];return o||(o=this.layerIndexes[e]=new t),o}_getWorkerSource(e,t,o){if(this.workerSources[e]||(this.workerSources[e]={}),this.workerSources[e][t]||(this.workerSources[e][t]={}),!this.workerSources[e][t][o]){const i={sendAsync:(t,o)=>(t.targetMapId=e,this.actor.sendAsync(t,o))};switch(t){case"vector":this.workerSources[e][t][o]=new r(i,this._getLayerIndex(e),this._getAvailableImages(e));break;case"geojson":this.workerSources[e][t][o]=new ye(i,this._getLayerIndex(e),this._getAvailableImages(e));break;default:this.workerSources[e][t][o]=new this.externalWorkerSourceTypes[t](i,this._getLayerIndex(e),this._getAvailableImages(e));}}return this.workerSources[e][t][o]}_getDEMWorkerSource(e,t){return this.demWorkerSources[e]||(this.demWorkerSources[e]={}),this.demWorkerSources[e][t]||(this.demWorkerSources[e][t]=new n),this.demWorkerSources[e][t]}}return e.i(self)&&(self.worker=new ve(self)),ve})); + +define("index",["exports","./shared"],(function(t,e){"use strict";var i="4.7.1";let a,s;const o={now:"undefined"!=typeof performance&&performance&&performance.now?performance.now.bind(performance):Date.now.bind(Date),frameAsync:t=>new Promise(((i,a)=>{const s=requestAnimationFrame(i);t.signal.addEventListener("abort",(()=>{cancelAnimationFrame(s),a(e.c());}));})),getImageData(t,e=0){return this.getImageCanvasContext(t).getImageData(-e,-e,t.width+2*e,t.height+2*e)},getImageCanvasContext(t){const e=window.document.createElement("canvas"),i=e.getContext("2d",{willReadFrequently:!0});if(!i)throw new Error("failed to create canvas 2d context");return e.width=t.width,e.height=t.height,i.drawImage(t,0,0,t.width,t.height),i},resolveURL:t=>(a||(a=document.createElement("a")),a.href=t,a.href),hardwareConcurrency:"undefined"!=typeof navigator&&navigator.hardwareConcurrency||4,get prefersReducedMotion(){return !!matchMedia&&(null==s&&(s=matchMedia("(prefers-reduced-motion: reduce)")),s.matches)}};class r{static testProp(t){if(!r.docStyle)return t[0];for(let e=0;e{window.removeEventListener("click",r.suppressClickInternal,!0);}),0);}static getScale(t){const e=t.getBoundingClientRect();return {x:e.width/t.offsetWidth||1,y:e.height/t.offsetHeight||1,boundingClientRect:e}}static getPoint(t,i,a){const s=i.boundingClientRect;return new e.P((a.clientX-s.left)/i.x-t.clientLeft,(a.clientY-s.top)/i.y-t.clientTop)}static mousePos(t,e){const i=r.getScale(t);return r.getPoint(t,i,e)}static touchPos(t,e){const i=[],a=r.getScale(t);for(let s=0;s{l&&d(l),l=null,u=!0;},h.onerror=()=>{c=!0,l=null;},h.src="data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAQAAAAfQ//73v/+BiOh/AAA="),function(t){let i,a,s,o;t.resetRequestQueue=()=>{i=[],a=0,s=0,o={};},t.addThrottleControl=t=>{const e=s++;return o[e]=t,e},t.removeThrottleControl=t=>{delete o[t],l();},t.getImage=(t,a,s=!0)=>new Promise(((o,r)=>{n.supported&&(t.headers||(t.headers={}),t.headers.accept="image/webp,*/*"),e.e(t,{type:"image"}),i.push({abortController:a,requestParameters:t,supportImageRefresh:s,state:"queued",onError:t=>{r(t);},onSuccess:t=>{o(t);}}),l();}));const r=t=>e._(this,void 0,void 0,(function*(){t.state="running";const{requestParameters:i,supportImageRefresh:s,onError:o,onSuccess:r,abortController:n}=t,c=!1===s&&!e.i(self)&&!e.g(i.url)&&(!i.headers||Object.keys(i.headers).reduce(((t,e)=>t&&"accept"===e),!0));a++;const u=c?h(i,n):e.m(i,n);try{const i=yield u;delete t.abortController,t.state="completed",i.data instanceof HTMLImageElement||e.b(i.data)?r(i):i.data&&r({data:yield(d=i.data,"function"==typeof createImageBitmap?e.d(d):e.f(d)),cacheControl:i.cacheControl,expires:i.expires});}catch(e){delete t.abortController,o(e);}finally{a--,l();}var d;})),l=()=>{const t=(()=>{for(const t of Object.keys(o))if(o[t]())return !0;return !1})()?e.a.MAX_PARALLEL_IMAGE_REQUESTS_PER_FRAME:e.a.MAX_PARALLEL_IMAGE_REQUESTS;for(let e=a;e0;e++){const t=i.shift();t.abortController.signal.aborted?e--:r(t);}},h=(t,i)=>new Promise(((a,s)=>{const o=new Image,r=t.url,n=t.credentials;n&&"include"===n?o.crossOrigin="use-credentials":(n&&"same-origin"===n||!e.s(r))&&(o.crossOrigin="anonymous"),i.signal.addEventListener("abort",(()=>{o.src="",s(e.c());})),o.fetchPriority="high",o.onload=()=>{o.onerror=o.onload=null,a({data:o});},o.onerror=()=>{o.onerror=o.onload=null,i.signal.aborted||s(new Error("Could not load image. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported."));},o.src=r;}));}(_||(_={})),_.resetRequestQueue();class p{constructor(t){this._transformRequestFn=t;}transformRequest(t,e){return this._transformRequestFn&&this._transformRequestFn(t,e)||{url:t}}setTransformRequest(t){this._transformRequestFn=t;}}function m(t){var i=new e.A(3);return i[0]=t[0],i[1]=t[1],i[2]=t[2],i}var f,g=function(t,e,i){return t[0]=e[0]-i[0],t[1]=e[1]-i[1],t[2]=e[2]-i[2],t};f=new e.A(3),e.A!=Float32Array&&(f[0]=0,f[1]=0,f[2]=0);var v=function(t){var e=t[0],i=t[1];return e*e+i*i};function x(t){const e=[];if("string"==typeof t)e.push({id:"default",url:t});else if(t&&t.length>0){const i=[];for(const{id:a,url:s}of t){const t=`${a}${s}`;-1===i.indexOf(t)&&(i.push(t),e.push({id:a,url:s}));}}return e}function y(t,e,i){const a=t.split("?");return a[0]+=`${e}${i}`,a.join("?")}!function(){var t=new e.A(2);e.A!=Float32Array&&(t[0]=0,t[1]=0);}();class b{constructor(t,e,i,a){this.context=t,this.format=i,this.texture=t.gl.createTexture(),this.update(e,a);}update(t,i,a){const{width:s,height:o}=t,r=!(this.size&&this.size[0]===s&&this.size[1]===o||a),{context:n}=this,{gl:l}=n;if(this.useMipmap=Boolean(i&&i.useMipmap),l.bindTexture(l.TEXTURE_2D,this.texture),n.pixelStoreUnpackFlipY.set(!1),n.pixelStoreUnpack.set(1),n.pixelStoreUnpackPremultiplyAlpha.set(this.format===l.RGBA&&(!i||!1!==i.premultiply)),r)this.size=[s,o],t instanceof HTMLImageElement||t instanceof HTMLCanvasElement||t instanceof HTMLVideoElement||t instanceof ImageData||e.b(t)?l.texImage2D(l.TEXTURE_2D,0,this.format,this.format,l.UNSIGNED_BYTE,t):l.texImage2D(l.TEXTURE_2D,0,this.format,s,o,0,this.format,l.UNSIGNED_BYTE,t.data);else {const{x:i,y:r}=a||{x:0,y:0};t instanceof HTMLImageElement||t instanceof HTMLCanvasElement||t instanceof HTMLVideoElement||t instanceof ImageData||e.b(t)?l.texSubImage2D(l.TEXTURE_2D,0,i,r,l.RGBA,l.UNSIGNED_BYTE,t):l.texSubImage2D(l.TEXTURE_2D,0,i,r,s,o,l.RGBA,l.UNSIGNED_BYTE,t.data);}this.useMipmap&&this.isSizePowerOfTwo()&&l.generateMipmap(l.TEXTURE_2D);}bind(t,e,i){const{context:a}=this,{gl:s}=a;s.bindTexture(s.TEXTURE_2D,this.texture),i!==s.LINEAR_MIPMAP_NEAREST||this.isSizePowerOfTwo()||(i=s.LINEAR),t!==this.filter&&(s.texParameteri(s.TEXTURE_2D,s.TEXTURE_MAG_FILTER,t),s.texParameteri(s.TEXTURE_2D,s.TEXTURE_MIN_FILTER,i||t),this.filter=t),e!==this.wrap&&(s.texParameteri(s.TEXTURE_2D,s.TEXTURE_WRAP_S,e),s.texParameteri(s.TEXTURE_2D,s.TEXTURE_WRAP_T,e),this.wrap=e);}isSizePowerOfTwo(){return this.size[0]===this.size[1]&&Math.log(this.size[0])/Math.LN2%1==0}destroy(){const{gl:t}=this.context;t.deleteTexture(this.texture),this.texture=null;}}function w(t){const{userImage:e}=t;return !!(e&&e.render&&e.render())&&(t.data.replace(new Uint8Array(e.data.buffer)),!0)}class T extends e.E{constructor(){super(),this.images={},this.updatedImages={},this.callbackDispatchedThisFrame={},this.loaded=!1,this.requestors=[],this.patterns={},this.atlasImage=new e.R({width:1,height:1}),this.dirty=!0;}isLoaded(){return this.loaded}setLoaded(t){if(this.loaded!==t&&(this.loaded=t,t)){for(const{ids:t,promiseResolve:e}of this.requestors)e(this._getImagesForIds(t));this.requestors=[];}}getImage(t){const i=this.images[t];if(i&&!i.data&&i.spriteData){const t=i.spriteData;i.data=new e.R({width:t.width,height:t.height},t.context.getImageData(t.x,t.y,t.width,t.height).data),i.spriteData=null;}return i}addImage(t,e){if(this.images[t])throw new Error(`Image id ${t} already exist, use updateImage instead`);this._validate(t,e)&&(this.images[t]=e);}_validate(t,i){let a=!0;const s=i.data||i.spriteData;return this._validateStretch(i.stretchX,s&&s.width)||(this.fire(new e.j(new Error(`Image "${t}" has invalid "stretchX" value`))),a=!1),this._validateStretch(i.stretchY,s&&s.height)||(this.fire(new e.j(new Error(`Image "${t}" has invalid "stretchY" value`))),a=!1),this._validateContent(i.content,i)||(this.fire(new e.j(new Error(`Image "${t}" has invalid "content" value`))),a=!1),a}_validateStretch(t,e){if(!t)return !0;let i=0;for(const a of t){if(a[0]{let a=!0;if(!this.isLoaded())for(const e of t)this.images[e]||(a=!1);this.isLoaded()||a?e(this._getImagesForIds(t)):this.requestors.push({ids:t,promiseResolve:e});}))}_getImagesForIds(t){const i={};for(const a of t){let t=this.getImage(a);t||(this.fire(new e.k("styleimagemissing",{id:a})),t=this.getImage(a)),t?i[a]={data:t.data.clone(),pixelRatio:t.pixelRatio,sdf:t.sdf,version:t.version,stretchX:t.stretchX,stretchY:t.stretchY,content:t.content,textFitWidth:t.textFitWidth,textFitHeight:t.textFitHeight,hasRenderCallback:Boolean(t.userImage&&t.userImage.render)}:e.w(`Image "${a}" could not be loaded. Please make sure you have added the image with map.addImage() or a "sprite" property in your style. You can provide missing images by listening for the "styleimagemissing" map event.`);}return i}getPixelSize(){const{width:t,height:e}=this.atlasImage;return {width:t,height:e}}getPattern(t){const i=this.patterns[t],a=this.getImage(t);if(!a)return null;if(i&&i.position.version===a.version)return i.position;if(i)i.position.version=a.version;else {const i={w:a.data.width+2,h:a.data.height+2,x:0,y:0},s=new e.I(i,a);this.patterns[t]={bin:i,position:s};}return this._updatePatternAtlas(),this.patterns[t].position}bind(t){const e=t.gl;this.atlasTexture?this.dirty&&(this.atlasTexture.update(this.atlasImage),this.dirty=!1):this.atlasTexture=new b(t,this.atlasImage,e.RGBA),this.atlasTexture.bind(e.LINEAR,e.CLAMP_TO_EDGE);}_updatePatternAtlas(){const t=[];for(const e in this.patterns)t.push(this.patterns[e].bin);const{w:i,h:a}=e.p(t),s=this.atlasImage;s.resize({width:i||1,height:a||1});for(const t in this.patterns){const{bin:i}=this.patterns[t],a=i.x+1,o=i.y+1,r=this.getImage(t).data,n=r.width,l=r.height;e.R.copy(r,s,{x:0,y:0},{x:a,y:o},{width:n,height:l}),e.R.copy(r,s,{x:0,y:l-1},{x:a,y:o-1},{width:n,height:1}),e.R.copy(r,s,{x:0,y:0},{x:a,y:o+l},{width:n,height:1}),e.R.copy(r,s,{x:n-1,y:0},{x:a-1,y:o},{width:1,height:l}),e.R.copy(r,s,{x:0,y:0},{x:a+n,y:o},{width:1,height:l});}this.dirty=!0;}beginFrame(){this.callbackDispatchedThisFrame={};}dispatchRenderCallbacks(t){for(const i of t){if(this.callbackDispatchedThisFrame[i])continue;this.callbackDispatchedThisFrame[i]=!0;const t=this.getImage(i);t||e.w(`Image with ID: "${i}" was not found`),w(t)&&this.updateImage(i,t);}}}const I=1e20;function E(t,e,i,a,s,o,r,n,l){for(let h=e;h-1);l++,o[l]=n,r[l]=h,r[l+1]=I;}for(let n=0,l=0;n65535)throw new Error("glyphs > 65535 not supported");if(e.ranges[s])return {stack:t,id:i,glyph:a};if(!this.url)throw new Error("glyphsUrl is not set");if(!e.requests[s]){const i=C.loadGlyphRange(t,s,this.url,this.requestManager);e.requests[s]=i;}const o=yield e.requests[s];for(const t in o)this._doesCharSupportLocalGlyph(+t)||(e.glyphs[+t]=o[+t]);return e.ranges[s]=!0,{stack:t,id:i,glyph:o[i]||null}}))}_doesCharSupportLocalGlyph(t){return !!this.localIdeographFontFamily&&/\p{Ideo}|\p{sc=Hang}|\p{sc=Hira}|\p{sc=Kana}/u.test(String.fromCodePoint(t))}_tinySDF(t,i,a){const s=this.localIdeographFontFamily;if(!s)return;if(!this._doesCharSupportLocalGlyph(a))return;let o=t.tinySDF;if(!o){let e="400";/bold/i.test(i)?e="900":/medium/i.test(i)?e="500":/light/i.test(i)&&(e="200"),o=t.tinySDF=new C.TinySDF({fontSize:48,buffer:6,radius:16,cutoff:.25,fontFamily:s,fontWeight:e});}const r=o.draw(String.fromCharCode(a));return {id:a,bitmap:new e.o({width:r.width||60,height:r.height||60},r.data),metrics:{width:r.glyphWidth/2||24,height:r.glyphHeight/2||24,left:r.glyphLeft/2+.5||0,top:r.glyphTop/2-27.5||-8,advance:r.glyphAdvance/2||24,isDoubleResolution:!0}}}}C.loadGlyphRange=function(t,i,a,s){return e._(this,void 0,void 0,(function*(){const o=256*i,r=o+255,n=s.transformRequest(a.replace("{fontstack}",t).replace("{range}",`${o}-${r}`),"Glyphs"),l=yield e.l(n,new AbortController);if(!l||!l.data)throw new Error(`Could not load glyph range. range: ${i}, ${o}-${r}`);const h={};for(const t of e.n(l.data))h[t.id]=t;return h}))},C.TinySDF=class{constructor({fontSize:t=24,buffer:e=3,radius:i=8,cutoff:a=.25,fontFamily:s="sans-serif",fontWeight:o="normal",fontStyle:r="normal"}={}){this.buffer=e,this.cutoff=a,this.radius=i;const n=this.size=t+4*e,l=this._createCanvas(n),h=this.ctx=l.getContext("2d",{willReadFrequently:!0});h.font=`${r} ${o} ${t}px ${s}`,h.textBaseline="alphabetic",h.textAlign="left",h.fillStyle="black",this.gridOuter=new Float64Array(n*n),this.gridInner=new Float64Array(n*n),this.f=new Float64Array(n),this.z=new Float64Array(n+1),this.v=new Uint16Array(n);}_createCanvas(t){const e=document.createElement("canvas");return e.width=e.height=t,e}draw(t){const{width:e,actualBoundingBoxAscent:i,actualBoundingBoxDescent:a,actualBoundingBoxLeft:s,actualBoundingBoxRight:o}=this.ctx.measureText(t),r=Math.ceil(i),n=Math.max(0,Math.min(this.size-this.buffer,Math.ceil(o-s))),l=Math.min(this.size-this.buffer,r+Math.ceil(a)),h=n+2*this.buffer,c=l+2*this.buffer,u=Math.max(h*c,0),d=new Uint8ClampedArray(u),_={data:d,width:h,height:c,glyphWidth:n,glyphHeight:l,glyphTop:r,glyphLeft:0,glyphAdvance:e};if(0===n||0===l)return _;const{ctx:p,buffer:m,gridInner:f,gridOuter:g}=this;p.clearRect(m,m,n,l),p.fillText(t,m,m+r);const v=p.getImageData(m,m,n,l);g.fill(I,0,u),f.fill(0,0,u);for(let t=0;t0?t*t:0,f[a]=t<0?t*t:0;}}E(g,0,0,h,c,h,this.f,this.v,this.z),E(f,m,m,n,l,h,this.f,this.v,this.z);for(let t=0;t1&&(r=t[++o]);const l=Math.abs(n-r.left),h=Math.abs(n-r.right),c=Math.min(l,h);let u;const d=e/i*(a+1);if(r.isDash){const t=a-Math.abs(d);u=Math.sqrt(c*c+t*t);}else u=a-Math.sqrt(c*c+d*d);this.data[s+n]=Math.max(0,Math.min(255,u+128));}}}addRegularDash(t){for(let e=t.length-1;e>=0;--e){const i=t[e],a=t[e+1];i.zeroLength?t.splice(e,1):a&&a.isDash===i.isDash&&(a.left=i.left,t.splice(e,1));}const e=t[0],i=t[t.length-1];e.isDash===i.isDash&&(e.left=i.left-this.width,i.right=e.right+this.width);const a=this.width*this.nextRow;let s=0,o=t[s];for(let e=0;e1&&(o=t[++s]);const i=Math.abs(e-o.left),r=Math.abs(e-o.right),n=Math.min(i,r);this.data[a+e]=Math.max(0,Math.min(255,(o.isDash?n:-n)+128));}}addDash(t,i){const a=i?7:0,s=2*a+1;if(this.nextRow+s>this.height)return e.w("LineAtlas out of space"),null;let o=0;for(let e=0;e{t.terminate();})),this.workers=null);}isPreloaded(){return !!this.active[k]}numActive(){return Object.keys(this.active).length}}const F=Math.floor(o.hardwareConcurrency/2);let B,O;function N(){return B||(B=new L),B}L.workerCount=e.C(globalThis)?Math.max(Math.min(F,3),1):1;class U{constructor(t,i){this.workerPool=t,this.actors=[],this.currentActor=0,this.id=i;const a=this.workerPool.acquire(i);for(let t=0;t{t.remove();})),this.actors=[],t&&this.workerPool.release(this.id);}registerMessageHandler(t,e){for(const i of this.actors)i.registerMessageHandler(t,e);}}function j(){return O||(O=new U(N(),e.G),O.registerMessageHandler("GR",((t,i,a)=>e.m(i,a)))),O}function Z(t,i){const a=e.H();return e.J(a,a,[1,1,0]),e.K(a,a,[.5*t.width,.5*t.height,1]),e.L(a,a,t.calculatePosMatrix(i.toUnwrapped()))}function q(t,e,i,a,s,o){const r=function(t,e,i){if(t)for(const a of t){const t=e[a];if(t&&t.source===i&&"fill-extrusion"===t.type)return !0}else for(const t in e){const a=e[t];if(a.source===i&&"fill-extrusion"===a.type)return !0}return !1}(s&&s.layers,e,t.id),n=o.maxPitchScaleFactor(),l=t.tilesIn(a,n,r);l.sort(V);const h=[];for(const a of l)h.push({wrappedTileID:a.tileID.wrapped().key,queryResults:a.tile.queryRenderedFeatures(e,i,t._state,a.queryGeometry,a.cameraQueryGeometry,a.scale,s,o,n,Z(t.transform,a.tileID))});const c=function(t){const e={},i={};for(const a of t){const t=a.queryResults,s=a.wrappedTileID,o=i[s]=i[s]||{};for(const i in t){const a=t[i],s=o[i]=o[i]||{},r=e[i]=e[i]||[];for(const t of a)s[t.featureIndex]||(s[t.featureIndex]=!0,r.push(t));}}return e}(h);for(const e in c)c[e].forEach((e=>{const i=e.feature,a=t.getFeatureState(i.layer["source-layer"],i.id);i.source=i.layer.source,i.layer["source-layer"]&&(i.sourceLayer=i.layer["source-layer"]),i.state=a;}));return c}function V(t,e){const i=t.tileID,a=e.tileID;return i.overscaledZ-a.overscaledZ||i.canonical.y-a.canonical.y||i.wrap-a.wrap||i.canonical.x-a.canonical.x}function G(t,i,a){return e._(this,void 0,void 0,(function*(){let s=t;if(t.url?s=(yield e.h(i.transformRequest(t.url,"Source"),a)).data:yield o.frameAsync(a),!s)return null;const r=e.M(e.e(s,t),["tiles","minzoom","maxzoom","attribution","bounds","scheme","tileSize","encoding"]);return "vector_layers"in s&&s.vector_layers&&(r.vectorLayerIds=s.vector_layers.map((t=>t.id))),r}))}class H{constructor(t,e){t&&(e?this.setSouthWest(t).setNorthEast(e):Array.isArray(t)&&(4===t.length?this.setSouthWest([t[0],t[1]]).setNorthEast([t[2],t[3]]):this.setSouthWest(t[0]).setNorthEast(t[1])));}setNorthEast(t){return this._ne=t instanceof e.N?new e.N(t.lng,t.lat):e.N.convert(t),this}setSouthWest(t){return this._sw=t instanceof e.N?new e.N(t.lng,t.lat):e.N.convert(t),this}extend(t){const i=this._sw,a=this._ne;let s,o;if(t instanceof e.N)s=t,o=t;else {if(!(t instanceof H))return Array.isArray(t)?4===t.length||t.every(Array.isArray)?this.extend(H.convert(t)):this.extend(e.N.convert(t)):t&&("lng"in t||"lon"in t)&&"lat"in t?this.extend(e.N.convert(t)):this;if(s=t._sw,o=t._ne,!s||!o)return this}return i||a?(i.lng=Math.min(s.lng,i.lng),i.lat=Math.min(s.lat,i.lat),a.lng=Math.max(o.lng,a.lng),a.lat=Math.max(o.lat,a.lat)):(this._sw=new e.N(s.lng,s.lat),this._ne=new e.N(o.lng,o.lat)),this}getCenter(){return new e.N((this._sw.lng+this._ne.lng)/2,(this._sw.lat+this._ne.lat)/2)}getSouthWest(){return this._sw}getNorthEast(){return this._ne}getNorthWest(){return new e.N(this.getWest(),this.getNorth())}getSouthEast(){return new e.N(this.getEast(),this.getSouth())}getWest(){return this._sw.lng}getSouth(){return this._sw.lat}getEast(){return this._ne.lng}getNorth(){return this._ne.lat}toArray(){return [this._sw.toArray(),this._ne.toArray()]}toString(){return `LngLatBounds(${this._sw.toString()}, ${this._ne.toString()})`}isEmpty(){return !(this._sw&&this._ne)}contains(t){const{lng:i,lat:a}=e.N.convert(t);let s=this._sw.lng<=i&&i<=this._ne.lng;return this._sw.lng>this._ne.lng&&(s=this._sw.lng>=i&&i>=this._ne.lng),this._sw.lat<=a&&a<=this._ne.lat&&s}static convert(t){return t instanceof H?t:t?new H(t):t}static fromLngLat(t,i=0){const a=360*i/40075017,s=a/Math.cos(Math.PI/180*t.lat);return new H(new e.N(t.lng-s,t.lat-a),new e.N(t.lng+s,t.lat+a))}adjustAntiMeridian(){const t=new e.N(this._sw.lng,this._sw.lat),i=new e.N(this._ne.lng,this._ne.lat);return new H(t,t.lng>i.lng?new e.N(i.lng+360,i.lat):i)}}class W{constructor(t,e,i){this.bounds=H.convert(this.validateBounds(t)),this.minzoom=e||0,this.maxzoom=i||24;}validateBounds(t){return Array.isArray(t)&&4===t.length?[Math.max(-180,t[0]),Math.max(-90,t[1]),Math.min(180,t[2]),Math.min(90,t[3])]:[-180,-90,180,90]}contains(t){const i=Math.pow(2,t.z),a=Math.floor(e.O(this.bounds.getWest())*i),s=Math.floor(e.Q(this.bounds.getNorth())*i),o=Math.ceil(e.O(this.bounds.getEast())*i),r=Math.ceil(e.Q(this.bounds.getSouth())*i);return t.x>=a&&t.x=s&&t.y{this._options.tiles=t;})),this}setUrl(t){return this.setSourceProperty((()=>{this.url=t,this._options.url=t;})),this}onRemove(){this._tileJSONRequest&&(this._tileJSONRequest.abort(),this._tileJSONRequest=null);}serialize(){return e.e({},this._options)}loadTile(t){return e._(this,void 0,void 0,(function*(){const e=t.tileID.canonical.url(this.tiles,this.map.getPixelRatio(),this.scheme),i={request:this.map._requestManager.transformRequest(e,"Tile"),uid:t.uid,tileID:t.tileID,zoom:t.tileID.overscaledZ,tileSize:this.tileSize*t.tileID.overscaleFactor(),type:this.type,source:this.id,pixelRatio:this.map.getPixelRatio(),showCollisionBoxes:this.map.showCollisionBoxes,promoteId:this.promoteId};i.request.collectResourceTiming=this._collectResourceTiming;let a="RT";if(t.actor&&"expired"!==t.state){if("loading"===t.state)return new Promise(((e,i)=>{t.reloadPromise={resolve:e,reject:i};}))}else t.actor=this.dispatcher.getActor(),a="LT";t.abortController=new AbortController;try{const e=yield t.actor.sendAsync({type:a,data:i},t.abortController);if(delete t.abortController,t.aborted)return;this._afterTileLoadWorkerResponse(t,e);}catch(e){if(delete t.abortController,t.aborted)return;if(e&&404!==e.status)throw e;this._afterTileLoadWorkerResponse(t,null);}}))}_afterTileLoadWorkerResponse(t,e){if(e&&e.resourceTiming&&(t.resourceTiming=e.resourceTiming),e&&this.map._refreshExpiredTiles&&t.setExpiryData(e),t.loadVectorData(e,this.map.painter),t.reloadPromise){const e=t.reloadPromise;t.reloadPromise=null,this.loadTile(t).then(e.resolve).catch(e.reject);}}abortTile(t){return e._(this,void 0,void 0,(function*(){t.abortController&&(t.abortController.abort(),delete t.abortController),t.actor&&(yield t.actor.sendAsync({type:"AT",data:{uid:t.uid,type:this.type,source:this.id}}));}))}unloadTile(t){return e._(this,void 0,void 0,(function*(){t.unloadVectorData(),t.actor&&(yield t.actor.sendAsync({type:"RMT",data:{uid:t.uid,type:this.type,source:this.id}}));}))}hasTransition(){return !1}}class X extends e.E{constructor(t,i,a,s){super(),this.id=t,this.dispatcher=a,this.setEventedParent(s),this.type="raster",this.minzoom=0,this.maxzoom=22,this.roundZoom=!0,this.scheme="xyz",this.tileSize=512,this._loaded=!1,this._options=e.e({type:"raster"},i),e.e(this,e.M(i,["url","scheme","tileSize"]));}load(){return e._(this,void 0,void 0,(function*(){this._loaded=!1,this.fire(new e.k("dataloading",{dataType:"source"})),this._tileJSONRequest=new AbortController;try{const t=yield G(this._options,this.map._requestManager,this._tileJSONRequest);this._tileJSONRequest=null,this._loaded=!0,t&&(e.e(this,t),t.bounds&&(this.tileBounds=new W(t.bounds,this.minzoom,this.maxzoom)),this.fire(new e.k("data",{dataType:"source",sourceDataType:"metadata"})),this.fire(new e.k("data",{dataType:"source",sourceDataType:"content"})));}catch(t){this._tileJSONRequest=null,this.fire(new e.j(t));}}))}loaded(){return this._loaded}onAdd(t){this.map=t,this.load();}onRemove(){this._tileJSONRequest&&(this._tileJSONRequest.abort(),this._tileJSONRequest=null);}setSourceProperty(t){this._tileJSONRequest&&(this._tileJSONRequest.abort(),this._tileJSONRequest=null),t(),this.load();}setTiles(t){return this.setSourceProperty((()=>{this._options.tiles=t;})),this}setUrl(t){return this.setSourceProperty((()=>{this.url=t,this._options.url=t;})),this}serialize(){return e.e({},this._options)}hasTile(t){return !this.tileBounds||this.tileBounds.contains(t.canonical)}loadTile(t){return e._(this,void 0,void 0,(function*(){const e=t.tileID.canonical.url(this.tiles,this.map.getPixelRatio(),this.scheme);t.abortController=new AbortController;try{const i=yield _.getImage(this.map._requestManager.transformRequest(e,"Tile"),t.abortController,this.map._refreshExpiredTiles);if(delete t.abortController,t.aborted)return void(t.state="unloaded");if(i&&i.data){this.map._refreshExpiredTiles&&i.cacheControl&&i.expires&&t.setExpiryData({cacheControl:i.cacheControl,expires:i.expires});const e=this.map.painter.context,a=e.gl,s=i.data;t.texture=this.map.painter.getTileTexture(s.width),t.texture?t.texture.update(s,{useMipmap:!0}):(t.texture=new b(e,s,a.RGBA,{useMipmap:!0}),t.texture.bind(a.LINEAR,a.CLAMP_TO_EDGE,a.LINEAR_MIPMAP_NEAREST)),t.state="loaded";}}catch(e){if(delete t.abortController,t.aborted)t.state="unloaded";else if(e)throw t.state="errored",e}}))}abortTile(t){return e._(this,void 0,void 0,(function*(){t.abortController&&(t.abortController.abort(),delete t.abortController);}))}unloadTile(t){return e._(this,void 0,void 0,(function*(){t.texture&&this.map.painter.saveTileTexture(t.texture);}))}hasTransition(){return !1}}class K extends X{constructor(t,i,a,s){super(t,i,a,s),this.type="raster-dem",this.maxzoom=22,this._options=e.e({type:"raster-dem"},i),this.encoding=i.encoding||"mapbox",this.redFactor=i.redFactor,this.greenFactor=i.greenFactor,this.blueFactor=i.blueFactor,this.baseShift=i.baseShift;}loadTile(t){return e._(this,void 0,void 0,(function*(){const i=t.tileID.canonical.url(this.tiles,this.map.getPixelRatio(),this.scheme),a=this.map._requestManager.transformRequest(i,"Tile");t.neighboringTiles=this._getNeighboringTiles(t.tileID),t.abortController=new AbortController;try{const i=yield _.getImage(a,t.abortController,this.map._refreshExpiredTiles);if(delete t.abortController,t.aborted)return void(t.state="unloaded");if(i&&i.data){const a=i.data;this.map._refreshExpiredTiles&&i.cacheControl&&i.expires&&t.setExpiryData({cacheControl:i.cacheControl,expires:i.expires});const s=e.b(a)&&e.U()?a:yield this.readImageNow(a),o={type:this.type,uid:t.uid,source:this.id,rawImageData:s,encoding:this.encoding,redFactor:this.redFactor,greenFactor:this.greenFactor,blueFactor:this.blueFactor,baseShift:this.baseShift};if(!t.actor||"expired"===t.state){t.actor=this.dispatcher.getActor();const e=yield t.actor.sendAsync({type:"LDT",data:o});t.dem=e,t.needsHillshadePrepare=!0,t.needsTerrainPrepare=!0,t.state="loaded";}}}catch(e){if(delete t.abortController,t.aborted)t.state="unloaded";else if(e)throw t.state="errored",e}}))}readImageNow(t){return e._(this,void 0,void 0,(function*(){if("undefined"!=typeof VideoFrame&&e.V()){const i=t.width+2,a=t.height+2;try{return new e.R({width:i,height:a},yield e.W(t,-1,-1,i,a))}catch(t){}}return o.getImageData(t,1)}))}_getNeighboringTiles(t){const i=t.canonical,a=Math.pow(2,i.z),s=(i.x-1+a)%a,o=0===i.x?t.wrap-1:t.wrap,r=(i.x+1+a)%a,n=i.x+1===a?t.wrap+1:t.wrap,l={};return l[new e.S(t.overscaledZ,o,i.z,s,i.y).key]={backfilled:!1},l[new e.S(t.overscaledZ,n,i.z,r,i.y).key]={backfilled:!1},i.y>0&&(l[new e.S(t.overscaledZ,o,i.z,s,i.y-1).key]={backfilled:!1},l[new e.S(t.overscaledZ,t.wrap,i.z,i.x,i.y-1).key]={backfilled:!1},l[new e.S(t.overscaledZ,n,i.z,r,i.y-1).key]={backfilled:!1}),i.y+10&&e.e(s,{resourceTiming:a}),this.fire(new e.k("data",Object.assign(Object.assign({},s),{sourceDataType:"metadata"}))),this.fire(new e.k("data",Object.assign(Object.assign({},s),{sourceDataType:"content"})));}catch(t){if(this._pendingLoads--,this._removed)return void this.fire(new e.k("dataabort",{dataType:"source"}));this.fire(new e.j(t));}}))}loaded(){return 0===this._pendingLoads}loadTile(t){return e._(this,void 0,void 0,(function*(){const e=t.actor?"RT":"LT";t.actor=this.actor;const i={type:this.type,uid:t.uid,tileID:t.tileID,zoom:t.tileID.overscaledZ,maxZoom:this.maxzoom,tileSize:this.tileSize,source:this.id,pixelRatio:this.map.getPixelRatio(),showCollisionBoxes:this.map.showCollisionBoxes,promoteId:this.promoteId};t.abortController=new AbortController;const a=yield this.actor.sendAsync({type:e,data:i},t.abortController);delete t.abortController,t.unloadVectorData(),t.aborted||t.loadVectorData(a,this.map.painter,"RT"===e);}))}abortTile(t){return e._(this,void 0,void 0,(function*(){t.abortController&&(t.abortController.abort(),delete t.abortController),t.aborted=!0;}))}unloadTile(t){return e._(this,void 0,void 0,(function*(){t.unloadVectorData(),yield this.actor.sendAsync({type:"RMT",data:{uid:t.uid,type:this.type,source:this.id}});}))}onRemove(){this._removed=!0,this.actor.sendAsync({type:"RS",data:{type:this.type,source:this.id}});}serialize(){return e.e({},this._options,{type:this.type,data:this._data})}hasTransition(){return !1}}var Y=e.Y([{name:"a_pos",type:"Int16",components:2},{name:"a_texture_pos",type:"Int16",components:2}]);class Q extends e.E{constructor(t,e,i,a){super(),this.id=t,this.dispatcher=i,this.coordinates=e.coordinates,this.type="image",this.minzoom=0,this.maxzoom=22,this.tileSize=512,this.tiles={},this._loaded=!1,this.setEventedParent(a),this.options=e;}load(t){return e._(this,void 0,void 0,(function*(){this._loaded=!1,this.fire(new e.k("dataloading",{dataType:"source"})),this.url=this.options.url,this._request=new AbortController;try{const e=yield _.getImage(this.map._requestManager.transformRequest(this.url,"Image"),this._request);this._request=null,this._loaded=!0,e&&e.data&&(this.image=e.data,t&&(this.coordinates=t),this._finishLoading());}catch(t){this._request=null,this._loaded=!0,this.fire(new e.j(t));}}))}loaded(){return this._loaded}updateImage(t){return t.url?(this._request&&(this._request.abort(),this._request=null),this.options.url=t.url,this.load(t.coordinates).finally((()=>{this.texture=null;})),this):this}_finishLoading(){this.map&&(this.setCoordinates(this.coordinates),this.fire(new e.k("data",{dataType:"source",sourceDataType:"metadata"})));}onAdd(t){this.map=t,this.load();}onRemove(){this._request&&(this._request.abort(),this._request=null);}setCoordinates(t){this.coordinates=t;const i=t.map(e.Z.fromLngLat);this.tileID=function(t){let i=1/0,a=1/0,s=-1/0,o=-1/0;for(const e of t)i=Math.min(i,e.x),a=Math.min(a,e.y),s=Math.max(s,e.x),o=Math.max(o,e.y);const r=Math.max(s-i,o-a),n=Math.max(0,Math.floor(-Math.log(r)/Math.LN2)),l=Math.pow(2,n);return new e.a1(n,Math.floor((i+s)/2*l),Math.floor((a+o)/2*l))}(i),this.minzoom=this.maxzoom=this.tileID.z;const a=i.map((t=>this.tileID.getTilePoint(t)._round()));return this._boundsArray=new e.$,this._boundsArray.emplaceBack(a[0].x,a[0].y,0,0),this._boundsArray.emplaceBack(a[1].x,a[1].y,e.X,0),this._boundsArray.emplaceBack(a[3].x,a[3].y,0,e.X),this._boundsArray.emplaceBack(a[2].x,a[2].y,e.X,e.X),this.boundsBuffer&&(this.boundsBuffer.destroy(),delete this.boundsBuffer),this.fire(new e.k("data",{dataType:"source",sourceDataType:"content"})),this}prepare(){if(0===Object.keys(this.tiles).length||!this.image)return;const t=this.map.painter.context,i=t.gl;this.boundsBuffer||(this.boundsBuffer=t.createVertexBuffer(this._boundsArray,Y.members)),this.boundsSegments||(this.boundsSegments=e.a0.simpleSegment(0,0,4,2)),this.texture||(this.texture=new b(t,this.image,i.RGBA),this.texture.bind(i.LINEAR,i.CLAMP_TO_EDGE));let a=!1;for(const t in this.tiles){const e=this.tiles[t];"loaded"!==e.state&&(e.state="loaded",e.texture=this.texture,a=!0);}a&&this.fire(new e.k("data",{dataType:"source",sourceDataType:"idle",sourceId:this.id}));}loadTile(t){return e._(this,void 0,void 0,(function*(){this.tileID&&this.tileID.equals(t.tileID.canonical)?(this.tiles[String(t.tileID.wrap)]=t,t.buckets={}):t.state="errored";}))}serialize(){return {type:"image",url:this.options.url,coordinates:this.coordinates}}hasTransition(){return !1}}class tt extends Q{constructor(t,e,i,a){super(t,e,i,a),this.roundZoom=!0,this.type="video",this.options=e;}load(){return e._(this,void 0,void 0,(function*(){this._loaded=!1;const t=this.options;this.urls=[];for(const e of t.urls)this.urls.push(this.map._requestManager.transformRequest(e,"Source").url);try{const t=yield e.a3(this.urls);if(this._loaded=!0,!t)return;this.video=t,this.video.loop=!0,this.video.addEventListener("playing",(()=>{this.map.triggerRepaint();})),this.map&&this.video.play(),this._finishLoading();}catch(t){this.fire(new e.j(t));}}))}pause(){this.video&&this.video.pause();}play(){this.video&&this.video.play();}seek(t){if(this.video){const i=this.video.seekable;ti.end(0)?this.fire(new e.j(new e.a2(`sources.${this.id}`,null,`Playback for this video can be set only between the ${i.start(0)} and ${i.end(0)}-second mark.`))):this.video.currentTime=t;}}getVideo(){return this.video}onAdd(t){this.map||(this.map=t,this.load(),this.video&&(this.video.play(),this.setCoordinates(this.coordinates)));}prepare(){if(0===Object.keys(this.tiles).length||this.video.readyState<2)return;const t=this.map.painter.context,i=t.gl;this.boundsBuffer||(this.boundsBuffer=t.createVertexBuffer(this._boundsArray,Y.members)),this.boundsSegments||(this.boundsSegments=e.a0.simpleSegment(0,0,4,2)),this.texture?this.video.paused||(this.texture.bind(i.LINEAR,i.CLAMP_TO_EDGE),i.texSubImage2D(i.TEXTURE_2D,0,0,0,i.RGBA,i.UNSIGNED_BYTE,this.video)):(this.texture=new b(t,this.video,i.RGBA),this.texture.bind(i.LINEAR,i.CLAMP_TO_EDGE));let a=!1;for(const t in this.tiles){const e=this.tiles[t];"loaded"!==e.state&&(e.state="loaded",e.texture=this.texture,a=!0);}a&&this.fire(new e.k("data",{dataType:"source",sourceDataType:"idle",sourceId:this.id}));}serialize(){return {type:"video",urls:this.urls,coordinates:this.coordinates}}hasTransition(){return this.video&&!this.video.paused}}class et extends Q{constructor(t,i,a,s){super(t,i,a,s),i.coordinates?Array.isArray(i.coordinates)&&4===i.coordinates.length&&!i.coordinates.some((t=>!Array.isArray(t)||2!==t.length||t.some((t=>"number"!=typeof t))))||this.fire(new e.j(new e.a2(`sources.${t}`,null,'"coordinates" property must be an array of 4 longitude/latitude array pairs'))):this.fire(new e.j(new e.a2(`sources.${t}`,null,'missing required property "coordinates"'))),i.animate&&"boolean"!=typeof i.animate&&this.fire(new e.j(new e.a2(`sources.${t}`,null,'optional "animate" property must be a boolean value'))),i.canvas?"string"==typeof i.canvas||i.canvas instanceof HTMLCanvasElement||this.fire(new e.j(new e.a2(`sources.${t}`,null,'"canvas" must be either a string representing the ID of the canvas element from which to read, or an HTMLCanvasElement instance'))):this.fire(new e.j(new e.a2(`sources.${t}`,null,'missing required property "canvas"'))),this.options=i,this.animate=void 0===i.animate||i.animate;}load(){return e._(this,void 0,void 0,(function*(){this._loaded=!0,this.canvas||(this.canvas=this.options.canvas instanceof HTMLCanvasElement?this.options.canvas:document.getElementById(this.options.canvas)),this.width=this.canvas.width,this.height=this.canvas.height,this._hasInvalidDimensions()?this.fire(new e.j(new Error("Canvas dimensions cannot be less than or equal to zero."))):(this.play=function(){this._playing=!0,this.map.triggerRepaint();},this.pause=function(){this._playing&&(this.prepare(),this._playing=!1);},this._finishLoading());}))}getCanvas(){return this.canvas}onAdd(t){this.map=t,this.load(),this.canvas&&this.animate&&this.play();}onRemove(){this.pause();}prepare(){let t=!1;if(this.canvas.width!==this.width&&(this.width=this.canvas.width,t=!0),this.canvas.height!==this.height&&(this.height=this.canvas.height,t=!0),this._hasInvalidDimensions())return;if(0===Object.keys(this.tiles).length)return;const i=this.map.painter.context,a=i.gl;this.boundsBuffer||(this.boundsBuffer=i.createVertexBuffer(this._boundsArray,Y.members)),this.boundsSegments||(this.boundsSegments=e.a0.simpleSegment(0,0,4,2)),this.texture?(t||this._playing)&&this.texture.update(this.canvas,{premultiply:!0}):this.texture=new b(i,this.canvas,a.RGBA,{premultiply:!0});let s=!1;for(const t in this.tiles){const e=this.tiles[t];"loaded"!==e.state&&(e.state="loaded",e.texture=this.texture,s=!0);}s&&this.fire(new e.k("data",{dataType:"source",sourceDataType:"idle",sourceId:this.id}));}serialize(){return {type:"canvas",coordinates:this.coordinates}}hasTransition(){return this._playing}_hasInvalidDimensions(){for(const t of [this.canvas.width,this.canvas.height])if(isNaN(t)||t<=0)return !0;return !1}}const it={},at=t=>{switch(t){case"geojson":return J;case"image":return Q;case"raster":return X;case"raster-dem":return K;case"vector":return $;case"video":return tt;case"canvas":return et}return it[t]},st="RTLPluginLoaded";class ot extends e.E{constructor(){super(...arguments),this.status="unavailable",this.url=null,this.dispatcher=j();}_syncState(t){return this.status=t,this.dispatcher.broadcast("SRPS",{pluginStatus:t,pluginURL:this.url}).catch((t=>{throw this.status="error",t}))}getRTLTextPluginStatus(){return this.status}clearRTLTextPlugin(){this.status="unavailable",this.url=null;}setRTLTextPlugin(t){return e._(this,arguments,void 0,(function*(t,e=!1){if(this.url)throw new Error("setRTLTextPlugin cannot be called multiple times.");if(this.url=o.resolveURL(t),!this.url)throw new Error(`requested url ${t} is invalid`);if("unavailable"===this.status){if(!e)return this._requestImport();this.status="deferred",this._syncState(this.status);}else if("requested"===this.status)return this._requestImport()}))}_requestImport(){return e._(this,void 0,void 0,(function*(){yield this._syncState("loading"),this.status="loaded",this.fire(new e.k(st));}))}lazyLoad(){"unavailable"===this.status?this.status="requested":"deferred"===this.status&&this._requestImport();}}let rt=null;function nt(){return rt||(rt=new ot),rt}class lt{constructor(t,i){this.timeAdded=0,this.fadeEndTime=0,this.tileID=t,this.uid=e.a4(),this.uses=0,this.tileSize=i,this.buckets={},this.expirationTime=null,this.queryPadding=0,this.hasSymbolBuckets=!1,this.hasRTLText=!1,this.dependencies={},this.rtt=[],this.rttCoords={},this.expiredRequestCount=0,this.state="loading";}registerFadeDuration(t){const e=t+this.timeAdded;ee.getLayer(t))).filter(Boolean);if(0!==t.length){a.layers=t,a.stateDependentLayerIds&&(a.stateDependentLayers=a.stateDependentLayerIds.map((e=>t.filter((t=>t.id===e))[0])));for(const e of t)i[e.id]=a;}}return i}(t.buckets,i.style),this.hasSymbolBuckets=!1;for(const t in this.buckets){const i=this.buckets[t];if(i instanceof e.a6){if(this.hasSymbolBuckets=!0,!a)break;i.justReloaded=!0;}}if(this.hasRTLText=!1,this.hasSymbolBuckets)for(const t in this.buckets){const i=this.buckets[t];if(i instanceof e.a6&&i.hasRTLText){this.hasRTLText=!0,nt().lazyLoad();break}}this.queryPadding=0;for(const t in this.buckets){const e=this.buckets[t];this.queryPadding=Math.max(this.queryPadding,i.style.getLayer(t).queryRadius(e));}t.imageAtlas&&(this.imageAtlas=t.imageAtlas),t.glyphAtlasImage&&(this.glyphAtlasImage=t.glyphAtlasImage);}else this.collisionBoxArray=new e.a5;}unloadVectorData(){for(const t in this.buckets)this.buckets[t].destroy();this.buckets={},this.imageAtlasTexture&&this.imageAtlasTexture.destroy(),this.imageAtlas&&(this.imageAtlas=null),this.glyphAtlasTexture&&this.glyphAtlasTexture.destroy(),this.latestFeatureIndex=null,this.state="unloaded";}getBucket(t){return this.buckets[t.id]}upload(t){for(const e in this.buckets){const i=this.buckets[e];i.uploadPending()&&i.upload(t);}const e=t.gl;this.imageAtlas&&!this.imageAtlas.uploaded&&(this.imageAtlasTexture=new b(t,this.imageAtlas.image,e.RGBA),this.imageAtlas.uploaded=!0),this.glyphAtlasImage&&(this.glyphAtlasTexture=new b(t,this.glyphAtlasImage,e.ALPHA),this.glyphAtlasImage=null);}prepare(t){this.imageAtlas&&this.imageAtlas.patchUpdatedImages(t,this.imageAtlasTexture);}queryRenderedFeatures(t,e,i,a,s,o,r,n,l,h){return this.latestFeatureIndex&&this.latestFeatureIndex.rawTileData?this.latestFeatureIndex.query({queryGeometry:a,cameraQueryGeometry:s,scale:o,tileSize:this.tileSize,pixelPosMatrix:h,transform:n,params:r,queryPadding:this.queryPadding*l},t,e,i):{}}querySourceFeatures(t,i){const a=this.latestFeatureIndex;if(!a||!a.rawTileData)return;const s=a.loadVTLayers(),o=i&&i.sourceLayer?i.sourceLayer:"",r=s._geojsonTileLayer||s[o];if(!r)return;const n=e.a7(i&&i.filter),{z:l,x:h,y:c}=this.tileID.canonical,u={z:l,x:h,y:c};for(let i=0;it)e=!1;else if(i)if(this.expirationTime{this.remove(t,s);}),i)),this.data[a].push(s),this.order.push(a),this.order.length>this.max){const t=this._getAndRemoveByKey(this.order[0]);t&&this.onRemove(t);}return this}has(t){return t.wrapped().key in this.data}getAndRemove(t){return this.has(t)?this._getAndRemoveByKey(t.wrapped().key):null}_getAndRemoveByKey(t){const e=this.data[t].shift();return e.timeout&&clearTimeout(e.timeout),0===this.data[t].length&&delete this.data[t],this.order.splice(this.order.indexOf(t),1),e.value}getByKey(t){const e=this.data[t];return e?e[0].value:null}get(t){return this.has(t)?this.data[t.wrapped().key][0].value:null}remove(t,e){if(!this.has(t))return this;const i=t.wrapped().key,a=void 0===e?0:this.data[i].indexOf(e),s=this.data[i][a];return this.data[i].splice(a,1),s.timeout&&clearTimeout(s.timeout),0===this.data[i].length&&delete this.data[i],this.onRemove(s.value),this.order.splice(this.order.indexOf(i),1),this}setMaxSize(t){for(this.max=t;this.order.length>this.max;){const t=this._getAndRemoveByKey(this.order[0]);t&&this.onRemove(t);}return this}filter(t){const e=[];for(const i in this.data)for(const a of this.data[i])t(a.value)||e.push(a);for(const t of e)this.remove(t.value.tileID,t);}}class ct{constructor(){this.state={},this.stateChanges={},this.deletedStates={};}updateState(t,i,a){const s=String(i);if(this.stateChanges[t]=this.stateChanges[t]||{},this.stateChanges[t][s]=this.stateChanges[t][s]||{},e.e(this.stateChanges[t][s],a),null===this.deletedStates[t]){this.deletedStates[t]={};for(const e in this.state[t])e!==s&&(this.deletedStates[t][e]=null);}else if(this.deletedStates[t]&&null===this.deletedStates[t][s]){this.deletedStates[t][s]={};for(const e in this.state[t][s])a[e]||(this.deletedStates[t][s][e]=null);}else for(const e in a)this.deletedStates[t]&&this.deletedStates[t][s]&&null===this.deletedStates[t][s][e]&&delete this.deletedStates[t][s][e];}removeFeatureState(t,e,i){if(null===this.deletedStates[t])return;const a=String(e);if(this.deletedStates[t]=this.deletedStates[t]||{},i&&void 0!==e)null!==this.deletedStates[t][a]&&(this.deletedStates[t][a]=this.deletedStates[t][a]||{},this.deletedStates[t][a][i]=null);else if(void 0!==e)if(this.stateChanges[t]&&this.stateChanges[t][a])for(i in this.deletedStates[t][a]={},this.stateChanges[t][a])this.deletedStates[t][a][i]=null;else this.deletedStates[t][a]=null;else this.deletedStates[t]=null;}getState(t,i){const a=String(i),s=e.e({},(this.state[t]||{})[a],(this.stateChanges[t]||{})[a]);if(null===this.deletedStates[t])return {};if(this.deletedStates[t]){const e=this.deletedStates[t][i];if(null===e)return {};for(const t in e)delete s[t];}return s}initializeTileState(t,e){t.setFeatureState(this.state,e);}coalesceChanges(t,i){const a={};for(const t in this.stateChanges){this.state[t]=this.state[t]||{};const i={};for(const a in this.stateChanges[t])this.state[t][a]||(this.state[t][a]={}),e.e(this.state[t][a],this.stateChanges[t][a]),i[a]=this.state[t][a];a[t]=i;}for(const t in this.deletedStates){this.state[t]=this.state[t]||{};const i={};if(null===this.deletedStates[t])for(const e in this.state[t])i[e]={},this.state[t][e]={};else for(const e in this.deletedStates[t]){if(null===this.deletedStates[t][e])this.state[t][e]={};else for(const i of Object.keys(this.deletedStates[t][e]))delete this.state[t][e][i];i[e]=this.state[t][e];}a[t]=a[t]||{},e.e(a[t],i);}if(this.stateChanges={},this.deletedStates={},0!==Object.keys(a).length)for(const e in t)t[e].setFeatureState(a,i);}}class ut extends e.E{constructor(t,e,i){super(),this.id=t,this.dispatcher=i,this.on("data",(t=>this._dataHandler(t))),this.on("dataloading",(()=>{this._sourceErrored=!1;})),this.on("error",(()=>{this._sourceErrored=this._source.loaded();})),this._source=((t,e,i,a)=>{const s=new(at(e.type))(t,e,i,a);if(s.id!==t)throw new Error(`Expected Source id to be ${t} instead of ${s.id}`);return s})(t,e,i,this),this._tiles={},this._cache=new ht(0,(t=>this._unloadTile(t))),this._timers={},this._cacheTimers={},this._maxTileCacheSize=null,this._maxTileCacheZoomLevels=null,this._loadedParentTiles={},this._coveredTiles={},this._state=new ct,this._didEmitContent=!1,this._updated=!1;}onAdd(t){this.map=t,this._maxTileCacheSize=t?t._maxTileCacheSize:null,this._maxTileCacheZoomLevels=t?t._maxTileCacheZoomLevels:null,this._source&&this._source.onAdd&&this._source.onAdd(t);}onRemove(t){this.clearTiles(),this._source&&this._source.onRemove&&this._source.onRemove(t);}loaded(){if(this._sourceErrored)return !0;if(!this._sourceLoaded)return !1;if(!this._source.loaded())return !1;if(!(void 0===this.used&&void 0===this.usedForTerrain||this.used||this.usedForTerrain))return !0;if(!this._updated)return !1;for(const t in this._tiles){const e=this._tiles[t];if("loaded"!==e.state&&"errored"!==e.state)return !1}return !0}getSource(){return this._source}pause(){this._paused=!0;}resume(){if(!this._paused)return;const t=this._shouldReloadOnResume;this._paused=!1,this._shouldReloadOnResume=!1,t&&this.reload(),this.transform&&this.update(this.transform,this.terrain);}_loadTile(t,i,a){return e._(this,void 0,void 0,(function*(){try{yield this._source.loadTile(t),this._tileLoaded(t,i,a);}catch(i){t.state="errored",404!==i.status?this._source.fire(new e.j(i,{tile:t})):this.update(this.transform,this.terrain);}}))}_unloadTile(t){this._source.unloadTile&&this._source.unloadTile(t);}_abortTile(t){this._source.abortTile&&this._source.abortTile(t),this._source.fire(new e.k("dataabort",{tile:t,coord:t.tileID,dataType:"source"}));}serialize(){return this._source.serialize()}prepare(t){this._source.prepare&&this._source.prepare(),this._state.coalesceChanges(this._tiles,this.map?this.map.painter:null);for(const e in this._tiles){const i=this._tiles[e];i.upload(t),i.prepare(this.map.style.imageManager);}}getIds(){return Object.values(this._tiles).map((t=>t.tileID)).sort(dt).map((t=>t.key))}getRenderableIds(t){const i=[];for(const e in this._tiles)this._isIdRenderable(e,t)&&i.push(this._tiles[e]);return t?i.sort(((t,i)=>{const a=t.tileID,s=i.tileID,o=new e.P(a.canonical.x,a.canonical.y)._rotate(this.transform.angle),r=new e.P(s.canonical.x,s.canonical.y)._rotate(this.transform.angle);return a.overscaledZ-s.overscaledZ||r.y-o.y||r.x-o.x})).map((t=>t.tileID.key)):i.map((t=>t.tileID)).sort(dt).map((t=>t.key))}hasRenderableParent(t){const e=this.findLoadedParent(t,0);return !!e&&this._isIdRenderable(e.tileID.key)}_isIdRenderable(t,e){return this._tiles[t]&&this._tiles[t].hasData()&&!this._coveredTiles[t]&&(e||!this._tiles[t].holdingForFade())}reload(){if(this._paused)this._shouldReloadOnResume=!0;else {this._cache.reset();for(const t in this._tiles)"errored"!==this._tiles[t].state&&this._reloadTile(t,"reloading");}}_reloadTile(t,i){return e._(this,void 0,void 0,(function*(){const e=this._tiles[t];e&&("loading"!==e.state&&(e.state=i),yield this._loadTile(e,t,i));}))}_tileLoaded(t,i,a){t.timeAdded=o.now(),"expired"===a&&(t.refreshedUponExpiration=!0),this._setTileReloadTimer(i,t),"raster-dem"===this.getSource().type&&t.dem&&this._backfillDEM(t),this._state.initializeTileState(t,this.map?this.map.painter:null),t.aborted||this._source.fire(new e.k("data",{dataType:"source",tile:t,coord:t.tileID}));}_backfillDEM(t){const e=this.getRenderableIds();for(let a=0;a1||(Math.abs(i)>1&&(1===Math.abs(i+s)?i+=s:1===Math.abs(i-s)&&(i-=s)),e.dem&&t.dem&&(t.dem.backfillBorder(e.dem,i,a),t.neighboringTiles&&t.neighboringTiles[o]&&(t.neighboringTiles[o].backfilled=!0)));}}getTile(t){return this.getTileByID(t.key)}getTileByID(t){return this._tiles[t]}_retainLoadedChildren(t,e,i,a){for(const s in this._tiles){let o=this._tiles[s];if(a[s]||!o.hasData()||o.tileID.overscaledZ<=e||o.tileID.overscaledZ>i)continue;let r=o.tileID;for(;o&&o.tileID.overscaledZ>e+1;){const t=o.tileID.scaledTo(o.tileID.overscaledZ-1);o=this._tiles[t.key],o&&o.hasData()&&(r=t);}let n=r;for(;n.overscaledZ>e;)if(n=n.scaledTo(n.overscaledZ-1),t[n.key]){a[r.key]=r;break}}}findLoadedParent(t,e){if(t.key in this._loadedParentTiles){const i=this._loadedParentTiles[t.key];return i&&i.tileID.overscaledZ>=e?i:null}for(let i=t.overscaledZ-1;i>=e;i--){const e=t.scaledTo(i),a=this._getLoadedTile(e);if(a)return a}}findLoadedSibling(t){return this._getLoadedTile(t)}_getLoadedTile(t){const e=this._tiles[t.key];return e&&e.hasData()?e:this._cache.getByKey(t.wrapped().key)}updateCacheSize(t){const i=Math.ceil(t.width/this._source.tileSize)+1,a=Math.ceil(t.height/this._source.tileSize)+1,s=Math.floor(i*a*(null===this._maxTileCacheZoomLevels?e.a.MAX_TILE_CACHE_ZOOM_LEVELS:this._maxTileCacheZoomLevels)),o="number"==typeof this._maxTileCacheSize?Math.min(this._maxTileCacheSize,s):s;this._cache.setMaxSize(o);}handleWrapJump(t){const e=Math.round((t-(void 0===this._prevLng?t:this._prevLng))/360);if(this._prevLng=t,e){const t={};for(const i in this._tiles){const a=this._tiles[i];a.tileID=a.tileID.unwrapTo(a.tileID.wrap+e),t[a.tileID.key]=a;}this._tiles=t;for(const t in this._timers)clearTimeout(this._timers[t]),delete this._timers[t];for(const t in this._tiles)this._setTileReloadTimer(t,this._tiles[t]);}}_updateCoveredAndRetainedTiles(t,e,i,a,s,r){const n={},l={},h=Object.keys(t),c=o.now();for(const i of h){const a=t[i],s=this._tiles[i];if(!s||0!==s.fadeEndTime&&s.fadeEndTime<=c)continue;const o=this.findLoadedParent(a,e),r=this.findLoadedSibling(a),h=o||r||null;h&&(this._addTile(h.tileID),n[h.tileID.key]=h.tileID),l[i]=a;}this._retainLoadedChildren(l,a,i,t);for(const e in n)t[e]||(this._coveredTiles[e]=!0,t[e]=n[e]);if(r){const e={},i={};for(const t of s)this._tiles[t.key].hasData()?e[t.key]=t:i[t.key]=t;for(const a in i){const s=i[a].children(this._source.maxzoom);this._tiles[s[0].key]&&this._tiles[s[1].key]&&this._tiles[s[2].key]&&this._tiles[s[3].key]&&(e[s[0].key]=t[s[0].key]=s[0],e[s[1].key]=t[s[1].key]=s[1],e[s[2].key]=t[s[2].key]=s[2],e[s[3].key]=t[s[3].key]=s[3],delete i[a]);}for(const a in i){const s=i[a],o=this.findLoadedParent(s,this._source.minzoom),r=this.findLoadedSibling(s),n=o||r||null;if(n){e[n.tileID.key]=t[n.tileID.key]=n.tileID;for(const t in e)e[t].isChildOf(n.tileID)&&delete e[t];}}for(const t in this._tiles)e[t]||(this._coveredTiles[t]=!0);}}update(t,i){if(!this._sourceLoaded||this._paused)return;let a;this.transform=t,this.terrain=i,this.updateCacheSize(t),this.handleWrapJump(this.transform.center.lng),this._coveredTiles={},this.used||this.usedForTerrain?this._source.tileID?a=t.getVisibleUnwrappedCoordinates(this._source.tileID).map((t=>new e.S(t.canonical.z,t.wrap,t.canonical.z,t.canonical.x,t.canonical.y))):(a=t.coveringTiles({tileSize:this.usedForTerrain?this.tileSize:this._source.tileSize,minzoom:this._source.minzoom,maxzoom:this._source.maxzoom,roundZoom:!this.usedForTerrain&&this._source.roundZoom,reparseOverscaled:this._source.reparseOverscaled,terrain:i}),this._source.hasTile&&(a=a.filter((t=>this._source.hasTile(t))))):a=[];const s=t.coveringZoomLevel(this._source),o=Math.max(s-ut.maxOverzooming,this._source.minzoom),r=Math.max(s+ut.maxUnderzooming,this._source.minzoom);if(this.usedForTerrain){const t={};for(const e of a)if(e.canonical.z>this._source.minzoom){const i=e.scaledTo(e.canonical.z-1);t[i.key]=i;const a=e.scaledTo(Math.max(this._source.minzoom,Math.min(e.canonical.z,5)));t[a.key]=a;}a=a.concat(Object.values(t));}const n=0===a.length&&!this._updated&&this._didEmitContent;this._updated=!0,n&&this.fire(new e.k("data",{sourceDataType:"idle",dataType:"source",sourceId:this.id}));const l=this._updateRetainedTiles(a,s);_t(this._source.type)&&this._updateCoveredAndRetainedTiles(l,o,r,s,a,i);for(const t in l)this._tiles[t].clearFadeHold();const h=e.ab(this._tiles,l);for(const t of h){const e=this._tiles[t];e.hasSymbolBuckets&&!e.holdingForFade()?e.setHoldDuration(this.map._fadeDuration):e.hasSymbolBuckets&&!e.symbolFadeFinished()||this._removeTile(t);}this._updateLoadedParentTileCache(),this._updateLoadedSiblingTileCache();}releaseSymbolFadeTiles(){for(const t in this._tiles)this._tiles[t].holdingForFade()&&this._removeTile(t);}_updateRetainedTiles(t,e){var i;const a={},s={},o=Math.max(e-ut.maxOverzooming,this._source.minzoom),r=Math.max(e+ut.maxUnderzooming,this._source.minzoom),n={};for(const i of t){const t=this._addTile(i);a[i.key]=i,t.hasData()||ethis._source.maxzoom){const t=r.children(this._source.maxzoom)[0],e=this.getTile(t);if(e&&e.hasData()){a[t.key]=t;continue}}else {const t=r.children(this._source.maxzoom);if(a[t[0].key]&&a[t[1].key]&&a[t[2].key]&&a[t[3].key])continue}let n=t.wasRequested();for(let e=r.overscaledZ-1;e>=o;--e){const o=r.scaledTo(e);if(s[o.key])break;if(s[o.key]=!0,t=this.getTile(o),!t&&n&&(t=this._addTile(o)),t){const e=t.hasData();if((e||!(null===(i=this.map)||void 0===i?void 0:i.cancelPendingTileRequestsWhileZooming)||n)&&(a[o.key]=o),n=t.wasRequested(),e)break}}}return a}_updateLoadedParentTileCache(){this._loadedParentTiles={};for(const t in this._tiles){const e=[];let i,a=this._tiles[t].tileID;for(;a.overscaledZ>0;){if(a.key in this._loadedParentTiles){i=this._loadedParentTiles[a.key];break}e.push(a.key);const t=a.scaledTo(a.overscaledZ-1);if(i=this._getLoadedTile(t),i)break;a=t;}for(const t of e)this._loadedParentTiles[t]=i;}}_updateLoadedSiblingTileCache(){this._loadedSiblingTiles={};for(const t in this._tiles){const e=this._tiles[t].tileID,i=this._getLoadedTile(e);this._loadedSiblingTiles[e.key]=i;}}_addTile(t){let i=this._tiles[t.key];if(i)return i;i=this._cache.getAndRemove(t),i&&(this._setTileReloadTimer(t.key,i),i.tileID=t,this._state.initializeTileState(i,this.map?this.map.painter:null),this._cacheTimers[t.key]&&(clearTimeout(this._cacheTimers[t.key]),delete this._cacheTimers[t.key],this._setTileReloadTimer(t.key,i)));const a=i;return i||(i=new lt(t,this._source.tileSize*t.overscaleFactor()),this._loadTile(i,t.key,i.state)),i.uses++,this._tiles[t.key]=i,a||this._source.fire(new e.k("dataloading",{tile:i,coord:i.tileID,dataType:"source"})),i}_setTileReloadTimer(t,e){t in this._timers&&(clearTimeout(this._timers[t]),delete this._timers[t]);const i=e.getExpiryTimeout();i&&(this._timers[t]=setTimeout((()=>{this._reloadTile(t,"expired"),delete this._timers[t];}),i));}_removeTile(t){const e=this._tiles[t];e&&(e.uses--,delete this._tiles[t],this._timers[t]&&(clearTimeout(this._timers[t]),delete this._timers[t]),e.uses>0||(e.hasData()&&"reloading"!==e.state?this._cache.add(e.tileID,e,e.getExpiryTimeout()):(e.aborted=!0,this._abortTile(e),this._unloadTile(e))));}_dataHandler(t){const e=t.sourceDataType;"source"===t.dataType&&"metadata"===e&&(this._sourceLoaded=!0),this._sourceLoaded&&!this._paused&&"source"===t.dataType&&"content"===e&&(this.reload(),this.transform&&this.update(this.transform,this.terrain),this._didEmitContent=!0);}clearTiles(){this._shouldReloadOnResume=!1,this._paused=!1;for(const t in this._tiles)this._removeTile(t);this._cache.reset();}tilesIn(t,i,a){const s=[],o=this.transform;if(!o)return s;const r=a?o.getCameraQueryGeometry(t):t,n=t.map((t=>o.pointCoordinate(t,this.terrain))),l=r.map((t=>o.pointCoordinate(t,this.terrain))),h=this.getIds();let c=1/0,u=1/0,d=-1/0,_=-1/0;for(const t of l)c=Math.min(c,t.x),u=Math.min(u,t.y),d=Math.max(d,t.x),_=Math.max(_,t.y);for(let t=0;t=0&&f[1].y+m>=0){const t=n.map((t=>r.getTilePoint(t))),e=l.map((t=>r.getTilePoint(t)));s.push({tile:a,tileID:r,queryGeometry:t,cameraQueryGeometry:e,scale:p});}}return s}getVisibleCoordinates(t){const e=this.getRenderableIds(t).map((t=>this._tiles[t].tileID));for(const t of e)t.posMatrix=this.transform.calculatePosMatrix(t.toUnwrapped());return e}hasTransition(){if(this._source.hasTransition())return !0;if(_t(this._source.type)){const t=o.now();for(const e in this._tiles)if(this._tiles[e].fadeEndTime>=t)return !0}return !1}setFeatureState(t,e,i){this._state.updateState(t=t||"_geojsonTileLayer",e,i);}removeFeatureState(t,e,i){this._state.removeFeatureState(t=t||"_geojsonTileLayer",e,i);}getFeatureState(t,e){return this._state.getState(t=t||"_geojsonTileLayer",e)}setDependencies(t,e,i){const a=this._tiles[t];a&&a.setDependencies(e,i);}reloadTilesForDependencies(t,e){for(const i in this._tiles)this._tiles[i].hasDependency(t,e)&&this._reloadTile(i,"reloading");this._cache.filter((i=>!i.hasDependency(t,e)));}}function dt(t,e){const i=Math.abs(2*t.wrap)-+(t.wrap<0),a=Math.abs(2*e.wrap)-+(e.wrap<0);return t.overscaledZ-e.overscaledZ||a-i||e.canonical.y-t.canonical.y||e.canonical.x-t.canonical.x}function _t(t){return "raster"===t||"image"===t||"video"===t}ut.maxOverzooming=10,ut.maxUnderzooming=3;class pt{constructor(t,e){this.reset(t,e);}reset(t,e){this.points=t||[],this._distances=[0];for(let t=1;t0?(s-r)/n:0;return this.points[o].mult(1-l).add(this.points[i].mult(l))}}function mt(t,e){let i=!0;return "always"===t||"never"!==t&&"never"!==e||(i=!1),i}class ft{constructor(t,e,i){const a=this.boxCells=[],s=this.circleCells=[];this.xCellCount=Math.ceil(t/i),this.yCellCount=Math.ceil(e/i);for(let t=0;tthis.width||a<0||e>this.height)return [];const n=[];if(t<=0&&e<=0&&this.width<=i&&this.height<=a){if(s)return [{key:null,x1:t,y1:e,x2:i,y2:a}];for(let t=0;t0}hitTestCircle(t,e,i,a,s){const o=t-i,r=t+i,n=e-i,l=e+i;if(r<0||o>this.width||l<0||n>this.height)return !1;const h=[];return this._forEachCell(o,n,r,l,this._queryCellCircle,h,{hitTest:!0,overlapMode:a,circle:{x:t,y:e,radius:i},seenUids:{box:{},circle:{}}},s),h.length>0}_queryCell(t,e,i,a,s,o,r,n){const{seenUids:l,hitTest:h,overlapMode:c}=r,u=this.boxCells[s];if(null!==u){const s=this.bboxes;for(const r of u)if(!l.box[r]){l.box[r]=!0;const u=4*r,d=this.boxKeys[r];if(t<=s[u+2]&&e<=s[u+3]&&i>=s[u+0]&&a>=s[u+1]&&(!n||n(d))&&(!h||!mt(c,d.overlapMode))&&(o.push({key:d,x1:s[u],y1:s[u+1],x2:s[u+2],y2:s[u+3]}),h))return !0}}const d=this.circleCells[s];if(null!==d){const s=this.circles;for(const r of d)if(!l.circle[r]){l.circle[r]=!0;const u=3*r,d=this.circleKeys[r];if(this._circleAndRectCollide(s[u],s[u+1],s[u+2],t,e,i,a)&&(!n||n(d))&&(!h||!mt(c,d.overlapMode))){const t=s[u],e=s[u+1],i=s[u+2];if(o.push({key:d,x1:t-i,y1:e-i,x2:t+i,y2:e+i}),h)return !0}}}return !1}_queryCellCircle(t,e,i,a,s,o,r,n){const{circle:l,seenUids:h,overlapMode:c}=r,u=this.boxCells[s];if(null!==u){const t=this.bboxes;for(const e of u)if(!h.box[e]){h.box[e]=!0;const i=4*e,a=this.boxKeys[e];if(this._circleAndRectCollide(l.x,l.y,l.radius,t[i+0],t[i+1],t[i+2],t[i+3])&&(!n||n(a))&&!mt(c,a.overlapMode))return o.push(!0),!0}}const d=this.circleCells[s];if(null!==d){const t=this.circles;for(const e of d)if(!h.circle[e]){h.circle[e]=!0;const i=3*e,a=this.circleKeys[e];if(this._circlesCollide(t[i],t[i+1],t[i+2],l.x,l.y,l.radius)&&(!n||n(a))&&!mt(c,a.overlapMode))return o.push(!0),!0}}}_forEachCell(t,e,i,a,s,o,r,n){const l=this._convertToXCellCoord(t),h=this._convertToYCellCoord(e),c=this._convertToXCellCoord(i),u=this._convertToYCellCoord(a);for(let d=l;d<=c;d++)for(let l=h;l<=u;l++)if(s.call(this,t,e,i,a,this.xCellCount*l+d,o,r,n))return}_convertToXCellCoord(t){return Math.max(0,Math.min(this.xCellCount-1,Math.floor(t*this.xScale)))}_convertToYCellCoord(t){return Math.max(0,Math.min(this.yCellCount-1,Math.floor(t*this.yScale)))}_circlesCollide(t,e,i,a,s,o){const r=a-t,n=s-e,l=i+o;return l*l>r*r+n*n}_circleAndRectCollide(t,e,i,a,s,o,r){const n=(o-a)/2,l=Math.abs(t-(a+n));if(l>n+i)return !1;const h=(r-s)/2,c=Math.abs(e-(s+h));if(c>h+i)return !1;if(l<=n||c<=h)return !0;const u=l-n,d=c-h;return u*u+d*d<=i*i}}function gt(t,i,a,s,o){const r=e.H();return i?(e.K(r,r,[1/o,1/o,1]),a||e.ad(r,r,s.angle)):e.L(r,s.labelPlaneMatrix,t),r}function vt(t,i,a,s,o){if(i){const i=e.ae(t);return e.K(i,i,[o,o,1]),a||e.ad(i,i,-s.angle),i}return s.glCoordMatrix}function xt(t,i,a,s){let o;s?(o=[t,i,s(t,i),1],e.af(o,o,a)):(o=[t,i,0,1],kt(o,o,a));const r=o[3];return {point:new e.P(o[0]/r,o[1]/r),signedDistanceFromCamera:r,isOccluded:!1}}function yt(t,e){return .5+t/e*.5}function bt(t,e){return t.x>=-e[0]&&t.x<=e[0]&&t.y>=-e[1]&&t.y<=e[1]}function wt(t,i,a,s,o,r,n,l,h,c,u,d,_,p,m){const f=s?t.textSizeData:t.iconSizeData,g=e.ag(f,a.transform.zoom),v=[256/a.width*2+1,256/a.height*2+1],x=s?t.text.dynamicLayoutVertexArray:t.icon.dynamicLayoutVertexArray;x.clear();const y=t.lineVertexArray,b=s?t.text.placedSymbolArray:t.icon.placedSymbolArray,w=a.transform.width/a.transform.height;let T=!1;for(let s=0;sMath.abs(a.x-i.x)*s?{useVertical:!0}:(t===e.ah.vertical?i.ya.x)?{needsFlipping:!0}:null}function Et(t,i,a,s,o,r,n,l,h,c,u){const d=a/24,_=i.lineOffsetX*d,p=i.lineOffsetY*d;let m;if(i.numGlyphs>1){const e=i.glyphStartIndex+i.numGlyphs,a=i.lineStartIndex,r=i.lineStartIndex+i.lineLength,h=Tt(d,l,_,p,s,i,u,t);if(!h)return {notEnoughRoom:!0};const f=xt(h.first.point.x,h.first.point.y,n,t.getElevation).point,g=xt(h.last.point.x,h.last.point.y,n,t.getElevation).point;if(o&&!s){const t=It(i.writingMode,f,g,c);if(t)return t}m=[h.first];for(let o=i.glyphStartIndex+1;o0?n.point:function(t,e,i,a,s,o){return Pt(t,e,i,1,s,o)}(t.tileAnchorPoint,o,a,0,r,t),h=It(i.writingMode,a,l,c);if(h)return h}const a=Mt(d*l.getoffsetX(i.glyphStartIndex),_,p,s,i.segment,i.lineStartIndex,i.lineStartIndex+i.lineLength,t,u);if(!a||t.projectionCache.anyProjectionOccluded)return {notEnoughRoom:!0};m=[a];}for(const t of m)e.aj(h,t.point,t.angle);return {}}function Pt(t,e,i,a,s,o){const r=t.add(t.sub(e)._unit()),n=void 0!==s?xt(r.x,r.y,s,o.getElevation).point:St(r.x,r.y,o).point,l=i.sub(n);return i.add(l._mult(a/l.mag()))}function Ct(t,i,a){const s=i.projectionCache;if(s.projections[t])return s.projections[t];const o=new e.P(i.lineVertexArray.getx(t),i.lineVertexArray.gety(t)),r=St(o.x,o.y,i);if(r.signedDistanceFromCamera>0)return s.projections[t]=r.point,s.anyProjectionOccluded=s.anyProjectionOccluded||r.isOccluded,r.point;const n=t-a.direction;return function(t,e,i,a,s){return Pt(t,e,i,a,void 0,s)}(0===a.distanceFromAnchor?i.tileAnchorPoint:new e.P(i.lineVertexArray.getx(n),i.lineVertexArray.gety(n)),o,a.previousVertex,a.absOffsetX-a.distanceFromAnchor+1,i)}function St(t,e,i){const a=t+i.translation[0],s=e+i.translation[1];let o;return !i.pitchWithMap&&i.projection.useSpecialProjectionForSymbols?(o=i.projection.projectTileCoordinates(a,s,i.unwrappedTileID,i.getElevation),o.point.x=(.5*o.point.x+.5)*i.width,o.point.y=(.5*-o.point.y+.5)*i.height):(o=xt(a,s,i.labelPlaneMatrix,i.getElevation),o.isOccluded=!1),o}function zt(t,e,i){return t._unit()._perp()._mult(e*i)}function Dt(t,i,a,s,o,r,n,l,h){if(l.projectionCache.offsets[t])return l.projectionCache.offsets[t];const c=a.add(i);if(t+h.direction=o)return l.projectionCache.offsets[t]=c,c;const u=Ct(t+h.direction,l,h),d=zt(u.sub(a),n,h.direction),_=a.add(d),p=u.add(d);return l.projectionCache.offsets[t]=e.ak(r,c,_,p)||c,l.projectionCache.offsets[t]}function Mt(t,e,i,a,s,o,r,n,l){const h=a?t-e:t+e;let c=h>0?1:-1,u=0;a&&(c*=-1,u=Math.PI),c<0&&(u+=Math.PI);let d,_=c>0?o+s:o+s+1;n.projectionCache.cachedAnchorPoint?d=n.projectionCache.cachedAnchorPoint:(d=St(n.tileAnchorPoint.x,n.tileAnchorPoint.y,n).point,n.projectionCache.cachedAnchorPoint=d);let p,m,f=d,g=d,v=0,x=0;const y=Math.abs(h),b=[];let w;for(;v+x<=y;){if(_+=c,_=r)return null;v+=x,g=f,m=p;const t={absOffsetX:y,direction:c,distanceFromAnchor:v,previousVertex:g};if(f=Ct(_,n,t),0===i)b.push(g),w=f.sub(g);else {let e;const a=f.sub(g);e=0===a.mag()?zt(Ct(_+c,n,t).sub(f),i,c):zt(a,i,c),m||(m=g.add(e)),p=Dt(_,e,f,o,r,m,i,n,t),b.push(m),w=p.sub(m);}x=w.mag();}const T=w._mult((y-v)/x)._add(m||g),I=u+Math.atan2(f.y-g.y,f.x-g.x);return b.push(T),{point:T,angle:l?I:0,path:b}}const At=new Float32Array([-1/0,-1/0,0,-1/0,-1/0,0,-1/0,-1/0,0,-1/0,-1/0,0]);function Rt(t,e){for(let i=0;i=1;t--)l.push(r.path[t]);for(let t=1;tt.signedDistanceFromCamera<=0))?[]:t.map((t=>t.point));}let m=[];if(l.length>0){const t=l[0].clone(),i=l[0].clone();for(let e=1;e=a.x&&i.x<=s.x&&t.y>=a.y&&i.y<=s.y?[l]:i.xs.x||i.ys.y?[]:e.al([l],a.x,a.y,s.x,s.y);}for(const e of m){o.reset(e,.25*i);let a=0;a=o.length<=.5*i?1:Math.ceil(o.paddedLength/u)+1;for(let e=0;ext(t.x,t.y,i,e.getElevation)))}queryRenderedSymbols(t){if(0===t.length||0===this.grid.keysLength()&&0===this.ignoredGrid.keysLength())return {};const i=[];let a=1/0,s=1/0,o=-1/0,r=-1/0;for(const n of t){const t=new e.P(n.x+Lt,n.y+Lt);a=Math.min(a,t.x),s=Math.min(s,t.y),o=Math.max(o,t.x),r=Math.max(r,t.y),i.push(t);}const n=this.grid.query(a,s,o,r).concat(this.ignoredGrid.query(a,s,o,r)),l={},h={};for(const t of n){const a=t.key;if(void 0===l[a.bucketInstanceId]&&(l[a.bucketInstanceId]={}),l[a.bucketInstanceId][a.featureIndex])continue;const s=[new e.P(t.x1,t.y1),new e.P(t.x2,t.y1),new e.P(t.x2,t.y2),new e.P(t.x1,t.y2)];e.am(i,s)&&(l[a.bucketInstanceId][a.featureIndex]=!0,void 0===h[a.bucketInstanceId]&&(h[a.bucketInstanceId]=[]),h[a.bucketInstanceId].push(a.featureIndex));}return h}insertCollisionBox(t,e,i,a,s,o){(i?this.ignoredGrid:this.grid).insert({bucketInstanceId:a,featureIndex:s,collisionGroupID:o,overlapMode:e},t[0],t[1],t[2],t[3]);}insertCollisionCircles(t,e,i,a,s,o){const r=i?this.ignoredGrid:this.grid,n={bucketInstanceId:a,featureIndex:s,collisionGroupID:o,overlapMode:e};for(let e=0;e=this.screenRightBoundary||athis.screenBottomBoundary}isInsideGrid(t,e,i,a){return i>=0&&t=0&&ethis.projectAndGetPerspectiveRatio(a,t.x,t.y,s,h)));I=t.some((t=>!t.isOccluded)),T=t.map((t=>t.point));}else I=!0;return {box:e.ao(T),allPointsOccluded:!I}}}function Bt(t,i,a){return i*(e.X/(t.tileSize*Math.pow(2,a-t.tileID.overscaledZ)))}class Ot{constructor(t,e,i,a){this.opacity=t?Math.max(0,Math.min(1,t.opacity+(t.placed?e:-e))):a&&i?1:0,this.placed=i;}isHidden(){return 0===this.opacity&&!this.placed}}class Nt{constructor(t,e,i,a,s){this.text=new Ot(t?t.text:null,e,i,s),this.icon=new Ot(t?t.icon:null,e,a,s);}isHidden(){return this.text.isHidden()&&this.icon.isHidden()}}class Ut{constructor(t,e,i){this.text=t,this.icon=e,this.skipFade=i;}}class jt{constructor(){this.invProjMatrix=e.H(),this.viewportMatrix=e.H(),this.circles=[];}}class Zt{constructor(t,e,i,a,s){this.bucketInstanceId=t,this.featureIndex=e,this.sourceLayerIndex=i,this.bucketIndex=a,this.tileID=s;}}class qt{constructor(t){this.crossSourceCollisions=t,this.maxGroupID=0,this.collisionGroups={};}get(t){if(this.crossSourceCollisions)return {ID:0,predicate:null};if(!this.collisionGroups[t]){const e=++this.maxGroupID;this.collisionGroups[t]={ID:e,predicate:t=>t.collisionGroupID===e};}return this.collisionGroups[t]}}function Vt(t,i,a,s,o){const{horizontalAlign:r,verticalAlign:n}=e.au(t);return new e.P(-(r-.5)*i+s[0]*o,-(n-.5)*a+s[1]*o)}class Gt{constructor(t,e,i,a,s,o){this.transform=t.clone(),this.terrain=i,this.collisionIndex=new Ft(this.transform,e),this.placements={},this.opacities={},this.variableOffsets={},this.stale=!1,this.commitTime=0,this.fadeDuration=a,this.retainedQueryData={},this.collisionGroups=new qt(s),this.collisionCircleArrays={},this.collisionBoxArrays=new Map,this.prevPlacement=o,o&&(o.prevPlacement=void 0),this.placedOrientations={};}_getTerrainElevationFunc(t){const e=this.terrain;return e?(i,a)=>e.getElevation(t,i,a):null}getBucketParts(t,i,a,s){const o=a.getBucket(i),r=a.latestFeatureIndex;if(!o||!r||i.id!==o.layerIds[0])return;const n=a.collisionBoxArray,l=o.layers[0].layout,h=o.layers[0].paint,c=Math.pow(2,this.transform.zoom-a.tileID.overscaledZ),u=a.tileSize/e.X,d=a.tileID.toUnwrapped(),_=this.transform.calculatePosMatrix(d),p="map"===l.get("text-pitch-alignment"),m="map"===l.get("text-rotation-alignment"),f=Bt(a,1,this.transform.zoom),g=this.collisionIndex.mapProjection.translatePosition(this.transform,a,h.get("text-translate"),h.get("text-translate-anchor")),v=this.collisionIndex.mapProjection.translatePosition(this.transform,a,h.get("icon-translate"),h.get("icon-translate-anchor")),x=gt(_,p,m,this.transform,f);let y=null;if(p){const t=vt(_,p,m,this.transform,f);y=e.L([],this.transform.labelPlaneMatrix,t);}this.retainedQueryData[o.bucketInstanceId]=new Zt(o.bucketInstanceId,r,o.sourceLayerIndex,o.index,a.tileID);const b={bucket:o,layout:l,translationText:g,translationIcon:v,posMatrix:_,unwrappedTileID:d,textLabelPlaneMatrix:x,labelToScreenMatrix:y,scale:c,textPixelRatio:u,holdingForFade:a.holdingForFade(),collisionBoxArray:n,partiallyEvaluatedTextSize:e.ag(o.textSizeData,this.transform.zoom),collisionGroup:this.collisionGroups.get(o.sourceID)};if(s)for(const e of o.sortKeyRanges){const{sortKey:i,symbolInstanceStart:a,symbolInstanceEnd:s}=e;t.push({sortKey:i,symbolInstanceStart:a,symbolInstanceEnd:s,parameters:b});}else t.push({symbolInstanceStart:0,symbolInstanceEnd:o.symbolInstances.length,parameters:b});}attemptAnchorPlacement(t,i,a,s,o,r,n,l,h,c,u,d,_,p,m,f,g,v,x){const y=e.aq[t.textAnchor],b=[t.textOffset0,t.textOffset1],w=Vt(y,a,s,b,o),T=this.collisionIndex.placeCollisionBox(i,d,l,h,c,n,r,f,u.predicate,x,w);if((!v||this.collisionIndex.placeCollisionBox(v,d,l,h,c,n,r,g,u.predicate,x,w).placeable)&&T.placeable){let t;if(this.prevPlacement&&this.prevPlacement.variableOffsets[_.crossTileID]&&this.prevPlacement.placements[_.crossTileID]&&this.prevPlacement.placements[_.crossTileID].text&&(t=this.prevPlacement.variableOffsets[_.crossTileID].anchor),0===_.crossTileID)throw new Error("symbolInstance.crossTileID can't be 0");return this.variableOffsets[_.crossTileID]={textOffset:b,width:a,height:s,anchor:y,textBoxScale:o,prevAnchor:t},this.markUsedJustification(p,y,_,m),p.allowVerticalPlacement&&(this.markUsedOrientation(p,m,_),this.placedOrientations[_.crossTileID]=m),{shift:w,placedGlyphBoxes:T}}}placeLayerBucketPart(t,i,a){const{bucket:s,layout:o,translationText:r,translationIcon:n,posMatrix:l,unwrappedTileID:h,textLabelPlaneMatrix:c,labelToScreenMatrix:u,textPixelRatio:d,holdingForFade:_,collisionBoxArray:p,partiallyEvaluatedTextSize:m,collisionGroup:f}=t.parameters,g=o.get("text-optional"),v=o.get("icon-optional"),x=e.ar(o,"text-overlap","text-allow-overlap"),y="always"===x,b=e.ar(o,"icon-overlap","icon-allow-overlap"),w="always"===b,T="map"===o.get("text-rotation-alignment"),I="map"===o.get("text-pitch-alignment"),E="none"!==o.get("icon-text-fit"),P="viewport-y"===o.get("symbol-z-order"),C=y&&(w||!s.hasIconData()||v),S=w&&(y||!s.hasTextData()||g);!s.collisionArrays&&p&&s.deserializeCollisionBoxes(p);const z=this._getTerrainElevationFunc(this.retainedQueryData[s.bucketInstanceId].tileID),D=(t,p,w)=>{var P,D;if(i[t.crossTileID])return;if(_)return void(this.placements[t.crossTileID]=new Ut(!1,!1,!1));let M=!1,A=!1,R=!0,k=null,L={box:null,placeable:!1,offscreen:null},F={box:null,placeable:!1,offscreen:null},B=null,O=null,N=null,U=0,j=0,Z=0;p.textFeatureIndex?U=p.textFeatureIndex:t.useRuntimeCollisionCircles&&(U=t.featureIndex),p.verticalTextFeatureIndex&&(j=p.verticalTextFeatureIndex);const q=p.textBox;if(q){const i=i=>{let a=e.ah.horizontal;if(s.allowVerticalPlacement&&!i&&this.prevPlacement){const e=this.prevPlacement.placedOrientations[t.crossTileID];e&&(this.placedOrientations[t.crossTileID]=e,a=e,this.markUsedOrientation(s,a,t));}return a},o=(i,a)=>{if(s.allowVerticalPlacement&&t.numVerticalGlyphVertices>0&&p.verticalTextBox){for(const t of s.writingModes)if(t===e.ah.vertical?(L=a(),F=L):L=i(),L&&L.placeable)break}else L=i();},c=t.textAnchorOffsetStartIndex,u=t.textAnchorOffsetEndIndex;if(u===c){const a=(e,i)=>{const a=this.collisionIndex.placeCollisionBox(e,x,d,l,h,I,T,r,f.predicate,z);return a&&a.placeable&&(this.markUsedOrientation(s,i,t),this.placedOrientations[t.crossTileID]=i),a};o((()=>a(q,e.ah.horizontal)),(()=>{const i=p.verticalTextBox;return s.allowVerticalPlacement&&t.numVerticalGlyphVertices>0&&i?a(i,e.ah.vertical):{box:null,offscreen:null}})),i(L&&L.placeable);}else {let _=e.aq[null===(D=null===(P=this.prevPlacement)||void 0===P?void 0:P.variableOffsets[t.crossTileID])||void 0===D?void 0:D.anchor];const m=(i,o,p)=>{const m=i.x2-i.x1,g=i.y2-i.y1,v=t.textBoxScale,y=E&&"never"===b?o:null;let w=null,P="never"===x?1:2,C="never";_&&P++;for(let e=0;em(q,p.iconBox,e.ah.horizontal)),(()=>{const i=p.verticalTextBox;return s.allowVerticalPlacement&&(!L||!L.placeable)&&t.numVerticalGlyphVertices>0&&i?m(i,p.verticalIconBox,e.ah.vertical):{box:null,occluded:!0,offscreen:null}})),L&&(M=L.placeable,R=L.offscreen);const g=i(L&&L.placeable);if(!M&&this.prevPlacement){const e=this.prevPlacement.variableOffsets[t.crossTileID];e&&(this.variableOffsets[t.crossTileID]=e,this.markUsedJustification(s,e.anchor,t,g));}}}if(B=L,M=B&&B.placeable,R=B&&B.offscreen,t.useRuntimeCollisionCircles){const i=s.text.placedSymbolArray.get(t.centerJustifiedTextSymbolIndex),n=e.ai(s.textSizeData,m,i),d=o.get("text-padding");O=this.collisionIndex.placeCollisionCircles(x,i,s.lineVertexArray,s.glyphOffsetArray,n,l,h,c,u,a,I,f.predicate,t.collisionCircleDiameter,d,r,z),O.circles.length&&O.collisionDetected&&!a&&e.w("Collisions detected, but collision boxes are not shown"),M=y||O.circles.length>0&&!O.collisionDetected,R=R&&O.offscreen;}if(p.iconFeatureIndex&&(Z=p.iconFeatureIndex),p.iconBox){const t=t=>this.collisionIndex.placeCollisionBox(t,b,d,l,h,I,T,n,f.predicate,z,E&&k?k:void 0);F&&F.placeable&&p.verticalIconBox?(N=t(p.verticalIconBox),A=N.placeable):(N=t(p.iconBox),A=N.placeable),R=R&&N.offscreen;}const V=g||0===t.numHorizontalGlyphVertices&&0===t.numVerticalGlyphVertices,G=v||0===t.numIconVertices;V||G?G?V||(A=A&&M):M=A&&M:A=M=A&&M;const H=A&&N.placeable;if(M&&B.placeable&&this.collisionIndex.insertCollisionBox(B.box,x,o.get("text-ignore-placement"),s.bucketInstanceId,F&&F.placeable&&j?j:U,f.ID),H&&this.collisionIndex.insertCollisionBox(N.box,b,o.get("icon-ignore-placement"),s.bucketInstanceId,Z,f.ID),O&&M&&this.collisionIndex.insertCollisionCircles(O.circles,x,o.get("text-ignore-placement"),s.bucketInstanceId,U,f.ID),a&&this.storeCollisionData(s.bucketInstanceId,w,p,B,N,O),0===t.crossTileID)throw new Error("symbolInstance.crossTileID can't be 0");if(0===s.bucketInstanceId)throw new Error("bucket.bucketInstanceId can't be 0");this.placements[t.crossTileID]=new Ut(M||C,A||S,R||s.justReloaded),i[t.crossTileID]=!0;};if(P){if(0!==t.symbolInstanceStart)throw new Error("bucket.bucketInstanceId should be 0");const e=s.getSortedSymbolIndexes(this.transform.angle);for(let t=e.length-1;t>=0;--t){const i=e[t];D(s.symbolInstances.get(i),s.collisionArrays[i],i);}}else for(let e=t.symbolInstanceStart;e=0&&(t.text.placedSymbolArray.get(e).crossTileID=o>=0&&e!==o?0:a.crossTileID);}markUsedOrientation(t,i,a){const s=i===e.ah.horizontal||i===e.ah.horizontalOnly?i:0,o=i===e.ah.vertical?i:0,r=[a.leftJustifiedTextSymbolIndex,a.centerJustifiedTextSymbolIndex,a.rightJustifiedTextSymbolIndex];for(const e of r)t.text.placedSymbolArray.get(e).placedOrientation=s;a.verticalPlacedTextSymbolIndex&&(t.text.placedSymbolArray.get(a.verticalPlacedTextSymbolIndex).placedOrientation=o);}commit(t){this.commitTime=t,this.zoomAtLastRecencyCheck=this.transform.zoom;const e=this.prevPlacement;let i=!1;this.prevZoomAdjustment=e?e.zoomAdjustment(this.transform.zoom):0;const a=e?e.symbolFadeChange(t):1,s=e?e.opacities:{},o=e?e.variableOffsets:{},r=e?e.placedOrientations:{};for(const t in this.placements){const e=this.placements[t],o=s[t];o?(this.opacities[t]=new Nt(o,a,e.text,e.icon),i=i||e.text!==o.text.placed||e.icon!==o.icon.placed):(this.opacities[t]=new Nt(null,a,e.text,e.icon,e.skipFade),i=i||e.text||e.icon);}for(const t in s){const e=s[t];if(!this.opacities[t]){const s=new Nt(e,a,!1,!1);s.isHidden()||(this.opacities[t]=s,i=i||e.text.placed||e.icon.placed);}}for(const t in o)this.variableOffsets[t]||!this.opacities[t]||this.opacities[t].isHidden()||(this.variableOffsets[t]=o[t]);for(const t in r)this.placedOrientations[t]||!this.opacities[t]||this.opacities[t].isHidden()||(this.placedOrientations[t]=r[t]);if(e&&void 0===e.lastPlacementChangeTime)throw new Error("Last placement time for previous placement is not defined");i?this.lastPlacementChangeTime=t:"number"!=typeof this.lastPlacementChangeTime&&(this.lastPlacementChangeTime=e?e.lastPlacementChangeTime:t);}updateLayerOpacities(t,e){const i={};for(const a of e){const e=a.getBucket(t);e&&a.latestFeatureIndex&&t.id===e.layerIds[0]&&this.updateBucketOpacities(e,a.tileID,i,a.collisionBoxArray);}}updateBucketOpacities(t,i,a,s){t.hasTextData()&&(t.text.opacityVertexArray.clear(),t.text.hasVisibleVertices=!1),t.hasIconData()&&(t.icon.opacityVertexArray.clear(),t.icon.hasVisibleVertices=!1),t.hasIconCollisionBoxData()&&t.iconCollisionBox.collisionVertexArray.clear(),t.hasTextCollisionBoxData()&&t.textCollisionBox.collisionVertexArray.clear();const o=t.layers[0],r=o.layout,n=new Nt(null,0,!1,!1,!0),l=r.get("text-allow-overlap"),h=r.get("icon-allow-overlap"),c=o._unevaluatedLayout.hasValue("text-variable-anchor")||o._unevaluatedLayout.hasValue("text-variable-anchor-offset"),u="map"===r.get("text-rotation-alignment"),d="map"===r.get("text-pitch-alignment"),_="none"!==r.get("icon-text-fit"),p=new Nt(null,0,l&&(h||!t.hasIconData()||r.get("icon-optional")),h&&(l||!t.hasTextData()||r.get("text-optional")),!0);!t.collisionArrays&&s&&(t.hasIconCollisionBoxData()||t.hasTextCollisionBoxData())&&t.deserializeCollisionBoxes(s);const m=(t,e,i)=>{for(let a=0;a0,v=this.placedOrientations[s.crossTileID],x=v===e.ah.vertical,y=v===e.ah.horizontal||v===e.ah.horizontalOnly;if(o>0||r>0){const e=te(h.text);m(t.text,o,x?ee:e),m(t.text,r,y?ee:e);const i=h.text.isHidden();[s.rightJustifiedTextSymbolIndex,s.centerJustifiedTextSymbolIndex,s.leftJustifiedTextSymbolIndex].forEach((e=>{e>=0&&(t.text.placedSymbolArray.get(e).hidden=i||x?1:0);})),s.verticalPlacedTextSymbolIndex>=0&&(t.text.placedSymbolArray.get(s.verticalPlacedTextSymbolIndex).hidden=i||y?1:0);const a=this.variableOffsets[s.crossTileID];a&&this.markUsedJustification(t,a.anchor,s,v);const n=this.placedOrientations[s.crossTileID];n&&(this.markUsedJustification(t,"left",s,n),this.markUsedOrientation(t,n,s));}if(g){const e=te(h.icon),i=!(_&&s.verticalPlacedIconSymbolIndex&&x);s.placedIconSymbolIndex>=0&&(m(t.icon,s.numIconVertices,i?e:ee),t.icon.placedSymbolArray.get(s.placedIconSymbolIndex).hidden=h.icon.isHidden()),s.verticalPlacedIconSymbolIndex>=0&&(m(t.icon,s.numVerticalIconVertices,i?ee:e),t.icon.placedSymbolArray.get(s.verticalPlacedIconSymbolIndex).hidden=h.icon.isHidden());}const b=f&&f.has(i)?f.get(i):{text:null,icon:null};if(t.hasIconCollisionBoxData()||t.hasTextCollisionBoxData()){const a=t.collisionArrays[i];if(a){let i=new e.P(0,0);if(a.textBox||a.verticalTextBox){let e=!0;if(c){const t=this.variableOffsets[l];t?(i=Vt(t.anchor,t.width,t.height,t.textOffset,t.textBoxScale),u&&i._rotate(d?this.transform.angle:-this.transform.angle)):e=!1;}if(a.textBox||a.verticalTextBox){let s;a.textBox&&(s=x),a.verticalTextBox&&(s=y),Ht(t.textCollisionBox.collisionVertexArray,h.text.placed,!e||s,b.text,i.x,i.y);}}if(a.iconBox||a.verticalIconBox){const e=Boolean(!y&&a.verticalIconBox);let s;a.iconBox&&(s=e),a.verticalIconBox&&(s=!e),Ht(t.iconCollisionBox.collisionVertexArray,h.icon.placed,s,b.icon,_?i.x:0,_?i.y:0);}}}}if(t.sortFeatures(this.transform.angle),this.retainedQueryData[t.bucketInstanceId]&&(this.retainedQueryData[t.bucketInstanceId].featureSortOrder=t.featureSortOrder),t.hasTextData()&&t.text.opacityVertexBuffer&&t.text.opacityVertexBuffer.updateData(t.text.opacityVertexArray),t.hasIconData()&&t.icon.opacityVertexBuffer&&t.icon.opacityVertexBuffer.updateData(t.icon.opacityVertexArray),t.hasIconCollisionBoxData()&&t.iconCollisionBox.collisionVertexBuffer&&t.iconCollisionBox.collisionVertexBuffer.updateData(t.iconCollisionBox.collisionVertexArray),t.hasTextCollisionBoxData()&&t.textCollisionBox.collisionVertexBuffer&&t.textCollisionBox.collisionVertexBuffer.updateData(t.textCollisionBox.collisionVertexArray),t.text.opacityVertexArray.length!==t.text.layoutVertexArray.length/4)throw new Error(`bucket.text.opacityVertexArray.length (= ${t.text.opacityVertexArray.length}) !== bucket.text.layoutVertexArray.length (= ${t.text.layoutVertexArray.length}) / 4`);if(t.icon.opacityVertexArray.length!==t.icon.layoutVertexArray.length/4)throw new Error(`bucket.icon.opacityVertexArray.length (= ${t.icon.opacityVertexArray.length}) !== bucket.icon.layoutVertexArray.length (= ${t.icon.layoutVertexArray.length}) / 4`);if(t.bucketInstanceId in this.collisionCircleArrays){const e=this.collisionCircleArrays[t.bucketInstanceId];t.placementInvProjMatrix=e.invProjMatrix,t.placementViewportMatrix=e.viewportMatrix,t.collisionCircleArray=e.circles,delete this.collisionCircleArrays[t.bucketInstanceId];}}symbolFadeChange(t){return 0===this.fadeDuration?1:(t-this.commitTime)/this.fadeDuration+this.prevZoomAdjustment}zoomAdjustment(t){return Math.max(0,(this.transform.zoom-t)/1.5)}hasTransitions(t){return this.stale||t-this.lastPlacementChangeTimet}setStale(){this.stale=!0;}}function Ht(t,e,i,a,s,o){a&&0!==a.length||(a=[0,0,0,0]);const r=a[0]-Lt,n=a[1]-Lt,l=a[2]-Lt,h=a[3]-Lt;t.emplaceBack(e?1:0,i?1:0,s||0,o||0,r,n),t.emplaceBack(e?1:0,i?1:0,s||0,o||0,l,n),t.emplaceBack(e?1:0,i?1:0,s||0,o||0,l,h),t.emplaceBack(e?1:0,i?1:0,s||0,o||0,r,h);}const Wt=Math.pow(2,25),$t=Math.pow(2,24),Xt=Math.pow(2,17),Kt=Math.pow(2,16),Jt=Math.pow(2,9),Yt=Math.pow(2,8),Qt=Math.pow(2,1);function te(t){if(0===t.opacity&&!t.placed)return 0;if(1===t.opacity&&t.placed)return 4294967295;const e=t.placed?1:0,i=Math.floor(127*t.opacity);return i*Wt+e*$t+i*Xt+e*Kt+i*Jt+e*Yt+i*Qt+e}const ee=0;function ie(){return {isOccluded:(t,e,i)=>!1,getPitchedTextCorrection:(t,e,i)=>1,get useSpecialProjectionForSymbols(){return !1},projectTileCoordinates(t,e,i,a){throw new Error("Not implemented.")},translatePosition:(t,e,i,a)=>function(t,e,i,a,s=!1){if(!i[0]&&!i[1])return [0,0];const o=s?"map"===a?t.angle:0:"viewport"===a?-t.angle:0;if(o){const t=Math.sin(o),e=Math.cos(o);i=[i[0]*e-i[1]*t,i[0]*t+i[1]*e];}return [s?i[0]:Bt(e,i[0],t.zoom),s?i[1]:Bt(e,i[1],t.zoom)]}(t,e,i,a),getCircleRadiusCorrection:t=>1}}class ae{constructor(t){this._sortAcrossTiles="viewport-y"!==t.layout.get("symbol-z-order")&&!t.layout.get("symbol-sort-key").isConstant(),this._currentTileIndex=0,this._currentPartIndex=0,this._seenCrossTileIDs={},this._bucketParts=[];}continuePlacement(t,e,i,a,s){const o=this._bucketParts;for(;this._currentTileIndext.sortKey-e.sortKey)));this._currentPartIndex!this._forceFullPlacement&&o.now()-a>2;for(;this._currentPlacementIndex>=0;){const a=e[t[this._currentPlacementIndex]],o=this.placement.collisionIndex.transform.zoom;if("symbol"===a.type&&(!a.minzoom||a.minzoom<=o)&&(!a.maxzoom||a.maxzoom>o)){if(this._inProgressLayer||(this._inProgressLayer=new ae(a)),this._inProgressLayer.continuePlacement(i[a.source],this.placement,this._showCollisionBoxes,a,s))return;delete this._inProgressLayer;}this._currentPlacementIndex--;}this._done=!0;}commit(t){return this.placement.commit(t),this.placement}}const oe=512/e.X/2;class re{constructor(t,i,a){this.tileID=t,this.bucketInstanceId=a,this._symbolsByKey={};const s=new Map;for(let t=0;t({x:Math.floor(t.anchorX*oe),y:Math.floor(t.anchorY*oe)}))),crossTileIDs:i.map((t=>t.crossTileID))};if(a.positions.length>128){const t=new e.av(a.positions.length,16,Uint16Array);for(const{x:e,y:i}of a.positions)t.add(e,i);t.finish(),delete a.positions,a.index=t;}this._symbolsByKey[t]=a;}}getScaledCoordinates(t,i){const{x:a,y:s,z:o}=this.tileID.canonical,{x:r,y:n,z:l}=i.canonical,h=oe/Math.pow(2,l-o),c=(n*e.X+t.anchorY)*h,u=s*e.X*oe;return {x:Math.floor((r*e.X+t.anchorX)*h-a*e.X*oe),y:Math.floor(c-u)}}findMatches(t,e,i){const a=this.tileID.canonical.zt))}}class ne{constructor(){this.maxCrossTileID=0;}generate(){return ++this.maxCrossTileID}}class le{constructor(){this.indexes={},this.usedCrossTileIDs={},this.lng=0;}handleWrapJump(t){const e=Math.round((t-this.lng)/360);if(0!==e)for(const t in this.indexes){const i=this.indexes[t],a={};for(const t in i){const s=i[t];s.tileID=s.tileID.unwrapTo(s.tileID.wrap+e),a[s.tileID.key]=s;}this.indexes[t]=a;}this.lng=t;}addBucket(t,e,i){if(this.indexes[t.overscaledZ]&&this.indexes[t.overscaledZ][t.key]){if(this.indexes[t.overscaledZ][t.key].bucketInstanceId===e.bucketInstanceId)return !1;this.removeBucketCrossTileIDs(t.overscaledZ,this.indexes[t.overscaledZ][t.key]);}for(let t=0;tt.overscaledZ)for(const i in s){const o=s[i];o.tileID.isChildOf(t)&&o.findMatches(e.symbolInstances,t,a);}else {const o=s[t.scaledTo(Number(i)).key];o&&o.findMatches(e.symbolInstances,t,a);}}for(let t=0;t{e[t]=!0;}));for(const t in this.layerIndexes)e[t]||delete this.layerIndexes[t];}}const ce=(t,i)=>e.t(t,i&&i.filter((t=>"source.canvas"!==t.identifier))),ue=e.aw();class de extends e.E{constructor(t,i={}){super(),this._rtlPluginLoaded=()=>{for(const t in this.sourceCaches){const e=this.sourceCaches[t].getSource().type;"vector"!==e&&"geojson"!==e||this.sourceCaches[t].reload();}},this.map=t,this.dispatcher=new U(N(),t._getMapId()),this.dispatcher.registerMessageHandler("GG",((t,e)=>this.getGlyphs(t,e))),this.dispatcher.registerMessageHandler("GI",((t,e)=>this.getImages(t,e))),this.imageManager=new T,this.imageManager.setEventedParent(this),this.glyphManager=new C(t._requestManager,i.localIdeographFontFamily),this.lineAtlas=new R(256,512),this.crossTileSymbolIndex=new he,this._spritesImagesIds={},this._layers={},this._order=[],this.sourceCaches={},this.zoomHistory=new e.ax,this._loaded=!1,this._availableImages=[],this._resetUpdates(),this.dispatcher.broadcast("SR",e.ay()),nt().on(st,this._rtlPluginLoaded),this.on("data",(t=>{if("source"!==t.dataType||"metadata"!==t.sourceDataType)return;const e=this.sourceCaches[t.sourceId];if(!e)return;const i=e.getSource();if(i&&i.vectorLayerIds)for(const t in this._layers){const e=this._layers[t];e.source===i.id&&this._validateLayer(e);}}));}loadURL(t,i={},a){this.fire(new e.k("dataloading",{dataType:"style"})),i.validate="boolean"!=typeof i.validate||i.validate;const s=this.map._requestManager.transformRequest(t,"Style");this._loadStyleRequest=new AbortController;const o=this._loadStyleRequest;e.h(s,this._loadStyleRequest).then((t=>{this._loadStyleRequest=null,this._load(t.data,i,a);})).catch((t=>{this._loadStyleRequest=null,t&&!o.signal.aborted&&this.fire(new e.j(t));}));}loadJSON(t,i={},a){this.fire(new e.k("dataloading",{dataType:"style"})),this._frameRequest=new AbortController,o.frameAsync(this._frameRequest).then((()=>{this._frameRequest=null,i.validate=!1!==i.validate,this._load(t,i,a);})).catch((()=>{}));}loadEmpty(){this.fire(new e.k("dataloading",{dataType:"style"})),this._load(ue,{validate:!1});}_load(t,i,a){var s;const o=i.transformStyle?i.transformStyle(a,t):t;if(!i.validate||!ce(this,e.u(o))){this._loaded=!0,this.stylesheet=o;for(const t in o.sources)this.addSource(t,o.sources[t],{validate:!1});o.sprite?this._loadSprite(o.sprite):this.imageManager.setLoaded(!0),this.glyphManager.setURL(o.glyphs),this._createLayers(),this.light=new D(this.stylesheet.light),this.sky=new A(this.stylesheet.sky),this.map.setTerrain(null!==(s=this.stylesheet.terrain)&&void 0!==s?s:null),this.fire(new e.k("data",{dataType:"style"})),this.fire(new e.k("style.load"));}}_createLayers(){const t=e.az(this.stylesheet.layers);this.dispatcher.broadcast("SL",t),this._order=t.map((t=>t.id)),this._layers={},this._serializedLayers=null;for(const i of t){const t=e.aA(i);t.setEventedParent(this,{layer:{id:i.id}}),this._layers[i.id]=t;}}_loadSprite(t,i=!1,a=void 0){let s;this.imageManager.setLoaded(!1),this._spriteRequest=new AbortController,function(t,i,a,s){return e._(this,void 0,void 0,(function*(){const r=x(t),n=a>1?"@2x":"",l={},h={};for(const{id:t,url:a}of r){const o=i.transformRequest(y(a,n,".json"),"SpriteJSON");l[t]=e.h(o,s);const r=i.transformRequest(y(a,n,".png"),"SpriteImage");h[t]=_.getImage(r,s);}return yield Promise.all([...Object.values(l),...Object.values(h)]),function(t,i){return e._(this,void 0,void 0,(function*(){const e={};for(const a in t){e[a]={};const s=o.getImageCanvasContext((yield i[a]).data),r=(yield t[a]).data;for(const t in r){const{width:i,height:o,x:n,y:l,sdf:h,pixelRatio:c,stretchX:u,stretchY:d,content:_,textFitWidth:p,textFitHeight:m}=r[t];e[a][t]={data:null,pixelRatio:c,sdf:h,stretchX:u,stretchY:d,content:_,textFitWidth:p,textFitHeight:m,spriteData:{width:i,height:o,x:n,y:l,context:s}};}}return e}))}(l,h)}))}(t,this.map._requestManager,this.map.getPixelRatio(),this._spriteRequest).then((t=>{if(this._spriteRequest=null,t)for(const e in t){this._spritesImagesIds[e]=[];const a=this._spritesImagesIds[e]?this._spritesImagesIds[e].filter((e=>!(e in t))):[];for(const t of a)this.imageManager.removeImage(t),this._changedImages[t]=!0;for(const a in t[e]){const s="default"===e?a:`${e}:${a}`;this._spritesImagesIds[e].push(s),s in this.imageManager.images?this.imageManager.updateImage(s,t[e][a],!1):this.imageManager.addImage(s,t[e][a]),i&&(this._changedImages[s]=!0);}}})).catch((t=>{this._spriteRequest=null,s=t,this.fire(new e.j(s));})).finally((()=>{this.imageManager.setLoaded(!0),this._availableImages=this.imageManager.listImages(),i&&(this._changed=!0),this.dispatcher.broadcast("SI",this._availableImages),this.fire(new e.k("data",{dataType:"style"})),a&&a(s);}));}_unloadSprite(){for(const t of Object.values(this._spritesImagesIds).flat())this.imageManager.removeImage(t),this._changedImages[t]=!0;this._spritesImagesIds={},this._availableImages=this.imageManager.listImages(),this._changed=!0,this.dispatcher.broadcast("SI",this._availableImages),this.fire(new e.k("data",{dataType:"style"}));}_validateLayer(t){const i=this.sourceCaches[t.source];if(!i)return;const a=t.sourceLayer;if(!a)return;const s=i.getSource();("geojson"===s.type||s.vectorLayerIds&&-1===s.vectorLayerIds.indexOf(a))&&this.fire(new e.j(new Error(`Source layer "${a}" does not exist on source "${s.id}" as specified by style layer "${t.id}".`)));}loaded(){if(!this._loaded)return !1;if(Object.keys(this._updatedSources).length)return !1;for(const t in this.sourceCaches)if(!this.sourceCaches[t].loaded())return !1;return !!this.imageManager.isLoaded()}_serializeByIds(t,i=!1){const a=this._serializedAllLayers();if(!t||0===t.length)return Object.values(i?e.aB(a):a);const s=[];for(const o of t)if(a[o]){const t=i?e.aB(a[o]):a[o];s.push(t);}return s}_serializedAllLayers(){let t=this._serializedLayers;if(t)return t;t=this._serializedLayers={};const e=Object.keys(this._layers);for(const i of e){const e=this._layers[i];"custom"!==e.type&&(t[i]=e.serialize());}return t}hasTransitions(){if(this.light&&this.light.hasTransition())return !0;if(this.sky&&this.sky.hasTransition())return !0;for(const t in this.sourceCaches)if(this.sourceCaches[t].hasTransition())return !0;for(const t in this._layers)if(this._layers[t].hasTransition())return !0;return !1}_checkLoaded(){if(!this._loaded)throw new Error("Style is not done loading.")}update(t){if(!this._loaded)return;const i=this._changed;if(i){const e=Object.keys(this._updatedLayers),i=Object.keys(this._removedLayers);(e.length||i.length)&&this._updateWorkerLayers(e,i);for(const t in this._updatedSources){const e=this._updatedSources[t];if("reload"===e)this._reloadSource(t);else {if("clear"!==e)throw new Error(`Invalid action ${e}`);this._clearSource(t);}}this._updateTilesForChangedImages(),this._updateTilesForChangedGlyphs();for(const e in this._updatedPaintProps)this._layers[e].updateTransitions(t);this.light.updateTransitions(t),this.sky.updateTransitions(t),this._resetUpdates();}const a={};for(const t in this.sourceCaches){const e=this.sourceCaches[t];a[t]=e.used,e.used=!1;}for(const e of this._order){const i=this._layers[e];i.recalculate(t,this._availableImages),!i.isHidden(t.zoom)&&i.source&&(this.sourceCaches[i.source].used=!0);}for(const t in a){const i=this.sourceCaches[t];!!a[t]!=!!i.used&&i.fire(new e.k("data",{sourceDataType:"visibility",dataType:"source",sourceId:t}));}this.light.recalculate(t),this.sky.recalculate(t),this.z=t.zoom,i&&this.fire(new e.k("data",{dataType:"style"}));}_updateTilesForChangedImages(){const t=Object.keys(this._changedImages);if(t.length){for(const e in this.sourceCaches)this.sourceCaches[e].reloadTilesForDependencies(["icons","patterns"],t);this._changedImages={};}}_updateTilesForChangedGlyphs(){if(this._glyphsDidChange){for(const t in this.sourceCaches)this.sourceCaches[t].reloadTilesForDependencies(["glyphs"],[""]);this._glyphsDidChange=!1;}}_updateWorkerLayers(t,e){this.dispatcher.broadcast("UL",{layers:this._serializeByIds(t,!1),removedIds:e});}_resetUpdates(){this._changed=!1,this._updatedLayers={},this._removedLayers={},this._updatedSources={},this._updatedPaintProps={},this._changedImages={},this._glyphsDidChange=!1;}setState(t,i={}){var a;this._checkLoaded();const s=this.serialize();if(t=i.transformStyle?i.transformStyle(s,t):t,(null===(a=i.validate)||void 0===a||a)&&ce(this,e.u(t)))return !1;(t=e.aB(t)).layers=e.az(t.layers);const o=e.aC(s,t),r=this._getOperationsToPerform(o);if(r.unimplemented.length>0)throw new Error(`Unimplemented: ${r.unimplemented.join(", ")}.`);if(0===r.operations.length)return !1;for(const t of r.operations)t();return this.stylesheet=t,this._serializedLayers=null,!0}_getOperationsToPerform(t){const e=[],i=[];for(const a of t)switch(a.command){case"setCenter":case"setZoom":case"setBearing":case"setPitch":continue;case"addLayer":e.push((()=>this.addLayer.apply(this,a.args)));break;case"removeLayer":e.push((()=>this.removeLayer.apply(this,a.args)));break;case"setPaintProperty":e.push((()=>this.setPaintProperty.apply(this,a.args)));break;case"setLayoutProperty":e.push((()=>this.setLayoutProperty.apply(this,a.args)));break;case"setFilter":e.push((()=>this.setFilter.apply(this,a.args)));break;case"addSource":e.push((()=>this.addSource.apply(this,a.args)));break;case"removeSource":e.push((()=>this.removeSource.apply(this,a.args)));break;case"setLayerZoomRange":e.push((()=>this.setLayerZoomRange.apply(this,a.args)));break;case"setLight":e.push((()=>this.setLight.apply(this,a.args)));break;case"setGeoJSONSourceData":e.push((()=>this.setGeoJSONSourceData.apply(this,a.args)));break;case"setGlyphs":e.push((()=>this.setGlyphs.apply(this,a.args)));break;case"setSprite":e.push((()=>this.setSprite.apply(this,a.args)));break;case"setSky":e.push((()=>this.setSky.apply(this,a.args)));break;case"setTerrain":e.push((()=>this.map.setTerrain.apply(this,a.args)));break;case"setTransition":e.push((()=>{}));break;default:i.push(a.command);}return {operations:e,unimplemented:i}}addImage(t,i){if(this.getImage(t))return this.fire(new e.j(new Error(`An image named "${t}" already exists.`)));this.imageManager.addImage(t,i),this._afterImageUpdated(t);}updateImage(t,e){this.imageManager.updateImage(t,e);}getImage(t){return this.imageManager.getImage(t)}removeImage(t){if(!this.getImage(t))return this.fire(new e.j(new Error(`An image named "${t}" does not exist.`)));this.imageManager.removeImage(t),this._afterImageUpdated(t);}_afterImageUpdated(t){this._availableImages=this.imageManager.listImages(),this._changedImages[t]=!0,this._changed=!0,this.dispatcher.broadcast("SI",this._availableImages),this.fire(new e.k("data",{dataType:"style"}));}listImages(){return this._checkLoaded(),this.imageManager.listImages()}addSource(t,i,a={}){if(this._checkLoaded(),void 0!==this.sourceCaches[t])throw new Error(`Source "${t}" already exists.`);if(!i.type)throw new Error(`The type property must be defined, but only the following properties were given: ${Object.keys(i).join(", ")}.`);if(["vector","raster","geojson","video","image"].indexOf(i.type)>=0&&this._validate(e.u.source,`sources.${t}`,i,null,a))return;this.map&&this.map._collectResourceTiming&&(i.collectResourceTiming=!0);const s=this.sourceCaches[t]=new ut(t,i,this.dispatcher);s.style=this,s.setEventedParent(this,(()=>({isSourceLoaded:s.loaded(),source:s.serialize(),sourceId:t}))),s.onAdd(this.map),this._changed=!0;}removeSource(t){if(this._checkLoaded(),void 0===this.sourceCaches[t])throw new Error("There is no source with this ID");for(const i in this._layers)if(this._layers[i].source===t)return this.fire(new e.j(new Error(`Source "${t}" cannot be removed while layer "${i}" is using it.`)));const i=this.sourceCaches[t];delete this.sourceCaches[t],delete this._updatedSources[t],i.fire(new e.k("data",{sourceDataType:"metadata",dataType:"source",sourceId:t})),i.setEventedParent(null),i.onRemove(this.map),this._changed=!0;}setGeoJSONSourceData(t,e){if(this._checkLoaded(),void 0===this.sourceCaches[t])throw new Error(`There is no source with this ID=${t}`);const i=this.sourceCaches[t].getSource();if("geojson"!==i.type)throw new Error(`geojsonSource.type is ${i.type}, which is !== 'geojson`);i.setData(e),this._changed=!0;}getSource(t){return this.sourceCaches[t]&&this.sourceCaches[t].getSource()}addLayer(t,i,a={}){this._checkLoaded();const s=t.id;if(this.getLayer(s))return void this.fire(new e.j(new Error(`Layer "${s}" already exists on this map.`)));let o;if("custom"===t.type){if(ce(this,e.aD(t)))return;o=e.aA(t);}else {if("source"in t&&"object"==typeof t.source&&(this.addSource(s,t.source),t=e.aB(t),t=e.e(t,{source:s})),this._validate(e.u.layer,`layers.${s}`,t,{arrayIndex:-1},a))return;o=e.aA(t),this._validateLayer(o),o.setEventedParent(this,{layer:{id:s}});}const r=i?this._order.indexOf(i):this._order.length;if(i&&-1===r)this.fire(new e.j(new Error(`Cannot add layer "${s}" before non-existing layer "${i}".`)));else {if(this._order.splice(r,0,s),this._layerOrderChanged=!0,this._layers[s]=o,this._removedLayers[s]&&o.source&&"custom"!==o.type){const t=this._removedLayers[s];delete this._removedLayers[s],t.type!==o.type?this._updatedSources[o.source]="clear":(this._updatedSources[o.source]="reload",this.sourceCaches[o.source].pause());}this._updateLayer(o),o.onAdd&&o.onAdd(this.map);}}moveLayer(t,i){if(this._checkLoaded(),this._changed=!0,!this._layers[t])return void this.fire(new e.j(new Error(`The layer '${t}' does not exist in the map's style and cannot be moved.`)));if(t===i)return;const a=this._order.indexOf(t);this._order.splice(a,1);const s=i?this._order.indexOf(i):this._order.length;i&&-1===s?this.fire(new e.j(new Error(`Cannot move layer "${t}" before non-existing layer "${i}".`))):(this._order.splice(s,0,t),this._layerOrderChanged=!0);}removeLayer(t){this._checkLoaded();const i=this._layers[t];if(!i)return void this.fire(new e.j(new Error(`Cannot remove non-existing layer "${t}".`)));i.setEventedParent(null);const a=this._order.indexOf(t);this._order.splice(a,1),this._layerOrderChanged=!0,this._changed=!0,this._removedLayers[t]=i,delete this._layers[t],this._serializedLayers&&delete this._serializedLayers[t],delete this._updatedLayers[t],delete this._updatedPaintProps[t],i.onRemove&&i.onRemove(this.map);}getLayer(t){return this._layers[t]}getLayersOrder(){return [...this._order]}hasLayer(t){return t in this._layers}setLayerZoomRange(t,i,a){this._checkLoaded();const s=this.getLayer(t);s?s.minzoom===i&&s.maxzoom===a||(null!=i&&(s.minzoom=i),null!=a&&(s.maxzoom=a),this._updateLayer(s)):this.fire(new e.j(new Error(`Cannot set the zoom range of non-existing layer "${t}".`)));}setFilter(t,i,a={}){this._checkLoaded();const s=this.getLayer(t);if(s){if(!e.aE(s.filter,i))return null==i?(s.filter=void 0,void this._updateLayer(s)):void(this._validate(e.u.filter,`layers.${s.id}.filter`,i,null,a)||(s.filter=e.aB(i),this._updateLayer(s)))}else this.fire(new e.j(new Error(`Cannot filter non-existing layer "${t}".`)));}getFilter(t){return e.aB(this.getLayer(t).filter)}setLayoutProperty(t,i,a,s={}){this._checkLoaded();const o=this.getLayer(t);o?e.aE(o.getLayoutProperty(i),a)||(o.setLayoutProperty(i,a,s),this._updateLayer(o)):this.fire(new e.j(new Error(`Cannot style non-existing layer "${t}".`)));}getLayoutProperty(t,i){const a=this.getLayer(t);if(a)return a.getLayoutProperty(i);this.fire(new e.j(new Error(`Cannot get style of non-existing layer "${t}".`)));}setPaintProperty(t,i,a,s={}){this._checkLoaded();const o=this.getLayer(t);o?e.aE(o.getPaintProperty(i),a)||(o.setPaintProperty(i,a,s)&&this._updateLayer(o),this._changed=!0,this._updatedPaintProps[t]=!0,this._serializedLayers=null):this.fire(new e.j(new Error(`Cannot style non-existing layer "${t}".`)));}getPaintProperty(t,e){return this.getLayer(t).getPaintProperty(e)}setFeatureState(t,i){this._checkLoaded();const a=t.source,s=t.sourceLayer,o=this.sourceCaches[a];if(void 0===o)return void this.fire(new e.j(new Error(`The source '${a}' does not exist in the map's style.`)));const r=o.getSource().type;"geojson"===r&&s?this.fire(new e.j(new Error("GeoJSON sources cannot have a sourceLayer parameter."))):"vector"!==r||s?(void 0===t.id&&this.fire(new e.j(new Error("The feature id parameter must be provided."))),o.setFeatureState(s,t.id,i)):this.fire(new e.j(new Error("The sourceLayer parameter must be provided for vector source types.")));}removeFeatureState(t,i){this._checkLoaded();const a=t.source,s=this.sourceCaches[a];if(void 0===s)return void this.fire(new e.j(new Error(`The source '${a}' does not exist in the map's style.`)));const o=s.getSource().type,r="vector"===o?t.sourceLayer:void 0;"vector"!==o||r?i&&"string"!=typeof t.id&&"number"!=typeof t.id?this.fire(new e.j(new Error("A feature id is required to remove its specific state property."))):s.removeFeatureState(r,t.id,i):this.fire(new e.j(new Error("The sourceLayer parameter must be provided for vector source types.")));}getFeatureState(t){this._checkLoaded();const i=t.source,a=t.sourceLayer,s=this.sourceCaches[i];if(void 0!==s)return "vector"!==s.getSource().type||a?(void 0===t.id&&this.fire(new e.j(new Error("The feature id parameter must be provided."))),s.getFeatureState(a,t.id)):void this.fire(new e.j(new Error("The sourceLayer parameter must be provided for vector source types.")));this.fire(new e.j(new Error(`The source '${i}' does not exist in the map's style.`)));}getTransition(){return e.e({duration:300,delay:0},this.stylesheet&&this.stylesheet.transition)}serialize(){if(!this._loaded)return;const t=e.aF(this.sourceCaches,(t=>t.serialize())),i=this._serializeByIds(this._order,!0),a=this.map.getTerrain()||void 0,s=this.stylesheet;return e.aG({version:s.version,name:s.name,metadata:s.metadata,light:s.light,sky:s.sky,center:s.center,zoom:s.zoom,bearing:s.bearing,pitch:s.pitch,sprite:s.sprite,glyphs:s.glyphs,transition:s.transition,sources:t,layers:i,terrain:a},(t=>void 0!==t))}_updateLayer(t){this._updatedLayers[t.id]=!0,t.source&&!this._updatedSources[t.source]&&"raster"!==this.sourceCaches[t.source].getSource().type&&(this._updatedSources[t.source]="reload",this.sourceCaches[t.source].pause()),this._serializedLayers=null,this._changed=!0;}_flattenAndSortRenderedFeatures(t){const e=t=>"fill-extrusion"===this._layers[t].type,i={},a=[];for(let s=this._order.length-1;s>=0;s--){const o=this._order[s];if(e(o)){i[o]=s;for(const e of t){const t=e[o];if(t)for(const e of t)a.push(e);}}}a.sort(((t,e)=>e.intersectionZ-t.intersectionZ));const s=[];for(let o=this._order.length-1;o>=0;o--){const r=this._order[o];if(e(r))for(let t=a.length-1;t>=0;t--){const e=a[t].feature;if(i[e.layer.id]{const a=i.featureSortOrder;if(a){const i=a.indexOf(t.featureIndex);return a.indexOf(e.featureIndex)-i}return e.featureIndex-t.featureIndex}));for(const t of s)e.push(t);}}for(const e in n)n[e].forEach((a=>{const s=a.feature,o=i[t[e].source].getFeatureState(s.layer["source-layer"],s.id);s.source=s.layer.source,s.layer["source-layer"]&&(s.sourceLayer=s.layer["source-layer"]),s.state=o;}));return n}(this._layers,r,this.sourceCaches,t,i,this.placement.collisionIndex,this.placement.retainedQueryData)),this._flattenAndSortRenderedFeatures(o)}querySourceFeatures(t,i){i&&i.filter&&this._validate(e.u.filter,"querySourceFeatures.filter",i.filter,null,i);const a=this.sourceCaches[t];return a?function(t,e){const i=t.getRenderableIds().map((e=>t.getTileByID(e))),a=[],s={};for(let t=0;tt.getTileByID(e))).sort(((t,e)=>e.tileID.overscaledZ-t.tileID.overscaledZ||(t.tileID.isLessThan(e.tileID)?-1:1)));}const a=this.crossTileSymbolIndex.addLayer(i,l[i.source],t.center.lng);r=r||a;}if(this.crossTileSymbolIndex.pruneUnusedLayers(this._order),((s=s||this._layerOrderChanged||0===i)||!this.pauseablePlacement||this.pauseablePlacement.isDone()&&!this.placement.stillRecent(o.now(),t.zoom))&&(this.pauseablePlacement=new se(t,this.map.terrain,this._order,s,e,i,a,this.placement),this._layerOrderChanged=!1),this.pauseablePlacement.isDone()?this.placement.setStale():(this.pauseablePlacement.continuePlacement(this._order,this._layers,l),this.pauseablePlacement.isDone()&&(this.placement=this.pauseablePlacement.commit(o.now()),n=!0),r&&this.pauseablePlacement.placement.setStale()),n||r)for(const t of this._order){const e=this._layers[t];"symbol"===e.type&&this.placement.updateLayerOpacities(e,l[e.source]);}return !this.pauseablePlacement.isDone()||this.placement.hasTransitions(o.now())}_releaseSymbolFadeTiles(){for(const t in this.sourceCaches)this.sourceCaches[t].releaseSymbolFadeTiles();}getImages(t,i){return e._(this,void 0,void 0,(function*(){const t=yield this.imageManager.getImages(i.icons);this._updateTilesForChangedImages();const e=this.sourceCaches[i.source];return e&&e.setDependencies(i.tileID.key,i.type,i.icons),t}))}getGlyphs(t,i){return e._(this,void 0,void 0,(function*(){const t=yield this.glyphManager.getGlyphs(i.stacks),e=this.sourceCaches[i.source];return e&&e.setDependencies(i.tileID.key,i.type,[""]),t}))}getGlyphsUrl(){return this.stylesheet.glyphs||null}setGlyphs(t,i={}){this._checkLoaded(),t&&this._validate(e.u.glyphs,"glyphs",t,null,i)||(this._glyphsDidChange=!0,this.stylesheet.glyphs=t,this.glyphManager.entries={},this.glyphManager.setURL(t));}addSprite(t,i,a={},s){this._checkLoaded();const o=[{id:t,url:i}],r=[...x(this.stylesheet.sprite),...o];this._validate(e.u.sprite,"sprite",r,null,a)||(this.stylesheet.sprite=r,this._loadSprite(o,!0,s));}removeSprite(t){this._checkLoaded();const i=x(this.stylesheet.sprite);if(i.find((e=>e.id===t))){if(this._spritesImagesIds[t])for(const e of this._spritesImagesIds[t])this.imageManager.removeImage(e),this._changedImages[e]=!0;i.splice(i.findIndex((e=>e.id===t)),1),this.stylesheet.sprite=i.length>0?i:void 0,delete this._spritesImagesIds[t],this._availableImages=this.imageManager.listImages(),this._changed=!0,this.dispatcher.broadcast("SI",this._availableImages),this.fire(new e.k("data",{dataType:"style"}));}else this.fire(new e.j(new Error(`Sprite "${t}" doesn't exists on this map.`)));}getSprite(){return x(this.stylesheet.sprite)}setSprite(t,i={},a){this._checkLoaded(),t&&this._validate(e.u.sprite,"sprite",t,null,i)||(this.stylesheet.sprite=t,t?this._loadSprite(t,!0,a):(this._unloadSprite(),a&&a(null)));}}var _e=e.Y([{name:"a_pos",type:"Int16",components:2}]);const pe={prelude:me("#ifdef GL_ES\nprecision mediump float;\n#else\n#if !defined(lowp)\n#define lowp\n#endif\n#if !defined(mediump)\n#define mediump\n#endif\n#if !defined(highp)\n#define highp\n#endif\n#endif\n","#ifdef GL_ES\nprecision highp float;\n#else\n#if !defined(lowp)\n#define lowp\n#endif\n#if !defined(mediump)\n#define mediump\n#endif\n#if !defined(highp)\n#define highp\n#endif\n#endif\nvec2 unpack_float(const float packedValue) {int packedIntValue=int(packedValue);int v0=packedIntValue/256;return vec2(v0,packedIntValue-v0*256);}vec2 unpack_opacity(const float packedOpacity) {int intOpacity=int(packedOpacity)/2;return vec2(float(intOpacity)/127.0,mod(packedOpacity,2.0));}vec4 decode_color(const vec2 encodedColor) {return vec4(unpack_float(encodedColor[0])/255.0,unpack_float(encodedColor[1])/255.0\n);}float unpack_mix_vec2(const vec2 packedValue,const float t) {return mix(packedValue[0],packedValue[1],t);}vec4 unpack_mix_color(const vec4 packedColors,const float t) {vec4 minColor=decode_color(vec2(packedColors[0],packedColors[1]));vec4 maxColor=decode_color(vec2(packedColors[2],packedColors[3]));return mix(minColor,maxColor,t);}vec2 get_pattern_pos(const vec2 pixel_coord_upper,const vec2 pixel_coord_lower,const vec2 pattern_size,const float tile_units_to_pixels,const vec2 pos) {vec2 offset=mod(mod(mod(pixel_coord_upper,pattern_size)*256.0,pattern_size)*256.0+pixel_coord_lower,pattern_size);return (tile_units_to_pixels*pos+offset)/pattern_size;}\n#ifdef TERRAIN3D\nuniform sampler2D u_terrain;uniform float u_terrain_dim;uniform mat4 u_terrain_matrix;uniform vec4 u_terrain_unpack;uniform float u_terrain_exaggeration;uniform highp sampler2D u_depth;\n#endif\nconst highp vec4 bitSh=vec4(256.*256.*256.,256.*256.,256.,1.);const highp vec4 bitShifts=vec4(1.)/bitSh;highp float unpack(highp vec4 color) {return dot(color,bitShifts);}highp float depthOpacity(vec3 frag) {\n#ifdef TERRAIN3D\nhighp float d=unpack(texture2D(u_depth,frag.xy*0.5+0.5))+0.0001-frag.z;return 1.0-max(0.0,min(1.0,-d*500.0));\n#else\nreturn 1.0;\n#endif\n}float calculate_visibility(vec4 pos) {\n#ifdef TERRAIN3D\nvec3 frag=pos.xyz/pos.w;highp float d=depthOpacity(frag);if (d > 0.95) return 1.0;return (d+depthOpacity(frag+vec3(0.0,0.01,0.0)))/2.0;\n#else\nreturn 1.0;\n#endif\n}float ele(vec2 pos) {\n#ifdef TERRAIN3D\nvec4 rgb=(texture2D(u_terrain,pos)*255.0)*u_terrain_unpack;return rgb.r+rgb.g+rgb.b-u_terrain_unpack.a;\n#else\nreturn 0.0;\n#endif\n}float get_elevation(vec2 pos) {\n#ifdef TERRAIN3D\nvec2 coord=(u_terrain_matrix*vec4(pos,0.0,1.0)).xy*u_terrain_dim+1.0;vec2 f=fract(coord);vec2 c=(floor(coord)+0.5)/(u_terrain_dim+2.0);float d=1.0/(u_terrain_dim+2.0);float tl=ele(c);float tr=ele(c+vec2(d,0.0));float bl=ele(c+vec2(0.0,d));float br=ele(c+vec2(d,d));float elevation=mix(mix(tl,tr,f.x),mix(bl,br,f.x),f.y);return elevation*u_terrain_exaggeration;\n#else\nreturn 0.0;\n#endif\n}"),background:me("uniform vec4 u_color;uniform float u_opacity;void main() {gl_FragColor=u_color*u_opacity;\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","attribute vec2 a_pos;uniform mat4 u_matrix;void main() {gl_Position=u_matrix*vec4(a_pos,0,1);}"),backgroundPattern:me("uniform vec2 u_pattern_tl_a;uniform vec2 u_pattern_br_a;uniform vec2 u_pattern_tl_b;uniform vec2 u_pattern_br_b;uniform vec2 u_texsize;uniform float u_mix;uniform float u_opacity;uniform sampler2D u_image;varying vec2 v_pos_a;varying vec2 v_pos_b;void main() {vec2 imagecoord=mod(v_pos_a,1.0);vec2 pos=mix(u_pattern_tl_a/u_texsize,u_pattern_br_a/u_texsize,imagecoord);vec4 color1=texture2D(u_image,pos);vec2 imagecoord_b=mod(v_pos_b,1.0);vec2 pos2=mix(u_pattern_tl_b/u_texsize,u_pattern_br_b/u_texsize,imagecoord_b);vec4 color2=texture2D(u_image,pos2);gl_FragColor=mix(color1,color2,u_mix)*u_opacity;\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","uniform mat4 u_matrix;uniform vec2 u_pattern_size_a;uniform vec2 u_pattern_size_b;uniform vec2 u_pixel_coord_upper;uniform vec2 u_pixel_coord_lower;uniform float u_scale_a;uniform float u_scale_b;uniform float u_tile_units_to_pixels;attribute vec2 a_pos;varying vec2 v_pos_a;varying vec2 v_pos_b;void main() {gl_Position=u_matrix*vec4(a_pos,0,1);v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,u_scale_a*u_pattern_size_a,u_tile_units_to_pixels,a_pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,u_scale_b*u_pattern_size_b,u_tile_units_to_pixels,a_pos);}"),circle:me("varying vec3 v_data;varying float v_visibility;\n#pragma mapbox: define highp vec4 color\n#pragma mapbox: define mediump float radius\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define highp vec4 stroke_color\n#pragma mapbox: define mediump float stroke_width\n#pragma mapbox: define lowp float stroke_opacity\nvoid main() {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize mediump float radius\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize highp vec4 stroke_color\n#pragma mapbox: initialize mediump float stroke_width\n#pragma mapbox: initialize lowp float stroke_opacity\nvec2 extrude=v_data.xy;float extrude_length=length(extrude);float antialiased_blur=v_data.z;float opacity_t=smoothstep(0.0,antialiased_blur,extrude_length-1.0);float color_t=stroke_width < 0.01 ? 0.0 : smoothstep(antialiased_blur,0.0,extrude_length-radius/(radius+stroke_width));gl_FragColor=v_visibility*opacity_t*mix(color*opacity,stroke_color*stroke_opacity,color_t);\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","uniform mat4 u_matrix;uniform bool u_scale_with_map;uniform bool u_pitch_with_map;uniform vec2 u_extrude_scale;uniform lowp float u_device_pixel_ratio;uniform highp float u_camera_to_center_distance;attribute vec2 a_pos;varying vec3 v_data;varying float v_visibility;\n#pragma mapbox: define highp vec4 color\n#pragma mapbox: define mediump float radius\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define highp vec4 stroke_color\n#pragma mapbox: define mediump float stroke_width\n#pragma mapbox: define lowp float stroke_opacity\nvoid main(void) {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize mediump float radius\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize highp vec4 stroke_color\n#pragma mapbox: initialize mediump float stroke_width\n#pragma mapbox: initialize lowp float stroke_opacity\nvec2 extrude=vec2(mod(a_pos,2.0)*2.0-1.0);vec2 circle_center=floor(a_pos*0.5);float ele=get_elevation(circle_center);v_visibility=calculate_visibility(u_matrix*vec4(circle_center,ele,1.0));if (u_pitch_with_map) {vec2 corner_position=circle_center;if (u_scale_with_map) {corner_position+=extrude*(radius+stroke_width)*u_extrude_scale;} else {vec4 projected_center=u_matrix*vec4(circle_center,0,1);corner_position+=extrude*(radius+stroke_width)*u_extrude_scale*(projected_center.w/u_camera_to_center_distance);}gl_Position=u_matrix*vec4(corner_position,ele,1);} else {gl_Position=u_matrix*vec4(circle_center,ele,1);if (u_scale_with_map) {gl_Position.xy+=extrude*(radius+stroke_width)*u_extrude_scale*u_camera_to_center_distance;} else {gl_Position.xy+=extrude*(radius+stroke_width)*u_extrude_scale*gl_Position.w;}}float antialiasblur=-max(1.0/u_device_pixel_ratio/(radius+stroke_width),blur);v_data=vec3(extrude.x,extrude.y,antialiasblur);}"),clippingMask:me("void main() {gl_FragColor=vec4(1.0);}","attribute vec2 a_pos;uniform mat4 u_matrix;void main() {gl_Position=u_matrix*vec4(a_pos,0,1);}"),heatmap:me("uniform highp float u_intensity;varying vec2 v_extrude;\n#pragma mapbox: define highp float weight\n#define GAUSS_COEF 0.3989422804014327\nvoid main() {\n#pragma mapbox: initialize highp float weight\nfloat d=-0.5*3.0*3.0*dot(v_extrude,v_extrude);float val=weight*u_intensity*GAUSS_COEF*exp(d);gl_FragColor=vec4(val,1.0,1.0,1.0);\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","uniform mat4 u_matrix;uniform float u_extrude_scale;uniform float u_opacity;uniform float u_intensity;attribute vec2 a_pos;varying vec2 v_extrude;\n#pragma mapbox: define highp float weight\n#pragma mapbox: define mediump float radius\nconst highp float ZERO=1.0/255.0/16.0;\n#define GAUSS_COEF 0.3989422804014327\nvoid main(void) {\n#pragma mapbox: initialize highp float weight\n#pragma mapbox: initialize mediump float radius\nvec2 unscaled_extrude=vec2(mod(a_pos,2.0)*2.0-1.0);float S=sqrt(-2.0*log(ZERO/weight/u_intensity/GAUSS_COEF))/3.0;v_extrude=S*unscaled_extrude;vec2 extrude=v_extrude*radius*u_extrude_scale;vec4 pos=vec4(floor(a_pos*0.5)+extrude,get_elevation(floor(a_pos*0.5)),1);gl_Position=u_matrix*pos;}"),heatmapTexture:me("uniform sampler2D u_image;uniform sampler2D u_color_ramp;uniform float u_opacity;varying vec2 v_pos;void main() {float t=texture2D(u_image,v_pos).r;vec4 color=texture2D(u_color_ramp,vec2(t,0.5));gl_FragColor=color*u_opacity;\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(0.0);\n#endif\n}","uniform mat4 u_matrix;uniform vec2 u_world;attribute vec2 a_pos;varying vec2 v_pos;void main() {gl_Position=u_matrix*vec4(a_pos*u_world,0,1);v_pos.x=a_pos.x;v_pos.y=1.0-a_pos.y;}"),collisionBox:me("varying float v_placed;varying float v_notUsed;void main() {float alpha=0.5;gl_FragColor=vec4(1.0,0.0,0.0,1.0)*alpha;if (v_placed > 0.5) {gl_FragColor=vec4(0.0,0.0,1.0,0.5)*alpha;}if (v_notUsed > 0.5) {gl_FragColor*=.1;}}","attribute vec2 a_anchor_pos;attribute vec2 a_placed;attribute vec2 a_box_real;uniform mat4 u_matrix;uniform vec2 u_pixel_extrude_scale;varying float v_placed;varying float v_notUsed;vec4 projectTileWithElevation(vec2 posInTile,float elevation) {return u_matrix*vec4(posInTile,elevation,1.0);}void main() {gl_Position=projectTileWithElevation(a_anchor_pos,get_elevation(a_anchor_pos));gl_Position.xy=((a_box_real+0.5)*u_pixel_extrude_scale*2.0-1.0)*vec2(1.0,-1.0)*gl_Position.w;if (gl_Position.z/gl_Position.w < 1.1) {gl_Position.z=0.5;}v_placed=a_placed.x;v_notUsed=a_placed.y;}"),collisionCircle:me("varying float v_radius;varying vec2 v_extrude;varying float v_perspective_ratio;varying float v_collision;void main() {float alpha=0.5*min(v_perspective_ratio,1.0);float stroke_radius=0.9*max(v_perspective_ratio,1.0);float distance_to_center=length(v_extrude);float distance_to_edge=abs(distance_to_center-v_radius);float opacity_t=smoothstep(-stroke_radius,0.0,-distance_to_edge);vec4 color=mix(vec4(0.0,0.0,1.0,0.5),vec4(1.0,0.0,0.0,1.0),v_collision);gl_FragColor=color*alpha*opacity_t;}","attribute vec2 a_pos;attribute float a_radius;attribute vec2 a_flags;uniform mat4 u_matrix;uniform mat4 u_inv_matrix;uniform vec2 u_viewport_size;uniform float u_camera_to_center_distance;varying float v_radius;varying vec2 v_extrude;varying float v_perspective_ratio;varying float v_collision;vec3 toTilePosition(vec2 screenPos) {vec4 rayStart=u_inv_matrix*vec4(screenPos,-1.0,1.0);vec4 rayEnd =u_inv_matrix*vec4(screenPos, 1.0,1.0);rayStart.xyz/=rayStart.w;rayEnd.xyz /=rayEnd.w;highp float t=(0.0-rayStart.z)/(rayEnd.z-rayStart.z);return mix(rayStart.xyz,rayEnd.xyz,t);}void main() {vec2 quadCenterPos=a_pos;float radius=a_radius;float collision=a_flags.x;float vertexIdx=a_flags.y;vec2 quadVertexOffset=vec2(mix(-1.0,1.0,float(vertexIdx >=2.0)),mix(-1.0,1.0,float(vertexIdx >=1.0 && vertexIdx <=2.0)));vec2 quadVertexExtent=quadVertexOffset*radius;vec3 tilePos=toTilePosition(quadCenterPos);vec4 clipPos=u_matrix*vec4(tilePos,1.0);highp float camera_to_anchor_distance=clipPos.w;highp float collision_perspective_ratio=clamp(0.5+0.5*(u_camera_to_center_distance/camera_to_anchor_distance),0.0,4.0);float padding_factor=1.2;v_radius=radius;v_extrude=quadVertexExtent*padding_factor;v_perspective_ratio=collision_perspective_ratio;v_collision=collision;gl_Position=vec4(clipPos.xyz/clipPos.w,1.0)+vec4(quadVertexExtent*padding_factor/u_viewport_size*2.0,0.0,0.0);}"),debug:me("uniform highp vec4 u_color;uniform sampler2D u_overlay;varying vec2 v_uv;void main() {vec4 overlay_color=texture2D(u_overlay,v_uv);gl_FragColor=mix(u_color,overlay_color,overlay_color.a);}","attribute vec2 a_pos;varying vec2 v_uv;uniform mat4 u_matrix;uniform float u_overlay_scale;void main() {v_uv=a_pos/8192.0;gl_Position=u_matrix*vec4(a_pos*u_overlay_scale,get_elevation(a_pos),1);}"),fill:me("#pragma mapbox: define highp vec4 color\n#pragma mapbox: define lowp float opacity\nvoid main() {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize lowp float opacity\ngl_FragColor=color*opacity;\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","attribute vec2 a_pos;uniform mat4 u_matrix;\n#pragma mapbox: define highp vec4 color\n#pragma mapbox: define lowp float opacity\nvoid main() {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize lowp float opacity\ngl_Position=u_matrix*vec4(a_pos,0,1);}"),fillOutline:me("varying vec2 v_pos;\n#pragma mapbox: define highp vec4 outline_color\n#pragma mapbox: define lowp float opacity\nvoid main() {\n#pragma mapbox: initialize highp vec4 outline_color\n#pragma mapbox: initialize lowp float opacity\nfloat dist=length(v_pos-gl_FragCoord.xy);float alpha=1.0-smoothstep(0.0,1.0,dist);gl_FragColor=outline_color*(alpha*opacity);\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","attribute vec2 a_pos;uniform mat4 u_matrix;uniform vec2 u_world;varying vec2 v_pos;\n#pragma mapbox: define highp vec4 outline_color\n#pragma mapbox: define lowp float opacity\nvoid main() {\n#pragma mapbox: initialize highp vec4 outline_color\n#pragma mapbox: initialize lowp float opacity\ngl_Position=u_matrix*vec4(a_pos,0,1);v_pos=(gl_Position.xy/gl_Position.w+1.0)/2.0*u_world;}"),fillOutlinePattern:me("uniform vec2 u_texsize;uniform sampler2D u_image;uniform float u_fade;varying vec2 v_pos_a;varying vec2 v_pos_b;varying vec2 v_pos;\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define lowp vec4 pattern_from\n#pragma mapbox: define lowp vec4 pattern_to\nvoid main() {\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump vec4 pattern_from\n#pragma mapbox: initialize mediump vec4 pattern_to\nvec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;vec2 imagecoord=mod(v_pos_a,1.0);vec2 pos=mix(pattern_tl_a/u_texsize,pattern_br_a/u_texsize,imagecoord);vec4 color1=texture2D(u_image,pos);vec2 imagecoord_b=mod(v_pos_b,1.0);vec2 pos2=mix(pattern_tl_b/u_texsize,pattern_br_b/u_texsize,imagecoord_b);vec4 color2=texture2D(u_image,pos2);float dist=length(v_pos-gl_FragCoord.xy);float alpha=1.0-smoothstep(0.0,1.0,dist);gl_FragColor=mix(color1,color2,u_fade)*alpha*opacity;\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","uniform mat4 u_matrix;uniform vec2 u_world;uniform vec2 u_pixel_coord_upper;uniform vec2 u_pixel_coord_lower;uniform vec3 u_scale;attribute vec2 a_pos;varying vec2 v_pos_a;varying vec2 v_pos_b;varying vec2 v_pos;\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define lowp vec4 pattern_from\n#pragma mapbox: define lowp vec4 pattern_to\n#pragma mapbox: define lowp float pixel_ratio_from\n#pragma mapbox: define lowp float pixel_ratio_to\nvoid main() {\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump vec4 pattern_from\n#pragma mapbox: initialize mediump vec4 pattern_to\n#pragma mapbox: initialize lowp float pixel_ratio_from\n#pragma mapbox: initialize lowp float pixel_ratio_to\nvec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;float tileRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;gl_Position=u_matrix*vec4(a_pos,0,1);vec2 display_size_a=(pattern_br_a-pattern_tl_a)/pixel_ratio_from;vec2 display_size_b=(pattern_br_b-pattern_tl_b)/pixel_ratio_to;v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,fromScale*display_size_a,tileRatio,a_pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,toScale*display_size_b,tileRatio,a_pos);v_pos=(gl_Position.xy/gl_Position.w+1.0)/2.0*u_world;}"),fillPattern:me("#ifdef GL_ES\nprecision highp float;\n#endif\nuniform vec2 u_texsize;uniform float u_fade;uniform sampler2D u_image;varying vec2 v_pos_a;varying vec2 v_pos_b;\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define lowp vec4 pattern_from\n#pragma mapbox: define lowp vec4 pattern_to\nvoid main() {\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump vec4 pattern_from\n#pragma mapbox: initialize mediump vec4 pattern_to\nvec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;vec2 imagecoord=mod(v_pos_a,1.0);vec2 pos=mix(pattern_tl_a/u_texsize,pattern_br_a/u_texsize,imagecoord);vec4 color1=texture2D(u_image,pos);vec2 imagecoord_b=mod(v_pos_b,1.0);vec2 pos2=mix(pattern_tl_b/u_texsize,pattern_br_b/u_texsize,imagecoord_b);vec4 color2=texture2D(u_image,pos2);gl_FragColor=mix(color1,color2,u_fade)*opacity;\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","uniform mat4 u_matrix;uniform vec2 u_pixel_coord_upper;uniform vec2 u_pixel_coord_lower;uniform vec3 u_scale;attribute vec2 a_pos;varying vec2 v_pos_a;varying vec2 v_pos_b;\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define lowp vec4 pattern_from\n#pragma mapbox: define lowp vec4 pattern_to\n#pragma mapbox: define lowp float pixel_ratio_from\n#pragma mapbox: define lowp float pixel_ratio_to\nvoid main() {\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump vec4 pattern_from\n#pragma mapbox: initialize mediump vec4 pattern_to\n#pragma mapbox: initialize lowp float pixel_ratio_from\n#pragma mapbox: initialize lowp float pixel_ratio_to\nvec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;float tileZoomRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;vec2 display_size_a=(pattern_br_a-pattern_tl_a)/pixel_ratio_from;vec2 display_size_b=(pattern_br_b-pattern_tl_b)/pixel_ratio_to;gl_Position=u_matrix*vec4(a_pos,0,1);v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,fromScale*display_size_a,tileZoomRatio,a_pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,toScale*display_size_b,tileZoomRatio,a_pos);}"),fillExtrusion:me("varying vec4 v_color;void main() {gl_FragColor=v_color;\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","uniform mat4 u_matrix;uniform vec3 u_lightcolor;uniform lowp vec3 u_lightpos;uniform lowp float u_lightintensity;uniform float u_vertical_gradient;uniform lowp float u_opacity;attribute vec2 a_pos;attribute vec4 a_normal_ed;\n#ifdef TERRAIN3D\nattribute vec2 a_centroid;\n#endif\nvarying vec4 v_color;\n#pragma mapbox: define highp float base\n#pragma mapbox: define highp float height\n#pragma mapbox: define highp vec4 color\nvoid main() {\n#pragma mapbox: initialize highp float base\n#pragma mapbox: initialize highp float height\n#pragma mapbox: initialize highp vec4 color\nvec3 normal=a_normal_ed.xyz;\n#ifdef TERRAIN3D\nfloat height_terrain3d_offset=get_elevation(a_centroid);float base_terrain3d_offset=height_terrain3d_offset-(base > 0.0 ? 0.0 : 10.0);\n#else\nfloat height_terrain3d_offset=0.0;float base_terrain3d_offset=0.0;\n#endif\nbase=max(0.0,base)+base_terrain3d_offset;height=max(0.0,height)+height_terrain3d_offset;float t=mod(normal.x,2.0);gl_Position=u_matrix*vec4(a_pos,t > 0.0 ? height : base,1);float colorvalue=color.r*0.2126+color.g*0.7152+color.b*0.0722;v_color=vec4(0.0,0.0,0.0,1.0);vec4 ambientlight=vec4(0.03,0.03,0.03,1.0);color+=ambientlight;float directional=clamp(dot(normal/16384.0,u_lightpos),0.0,1.0);directional=mix((1.0-u_lightintensity),max((1.0-colorvalue+u_lightintensity),1.0),directional);if (normal.y !=0.0) {directional*=((1.0-u_vertical_gradient)+(u_vertical_gradient*clamp((t+base)*pow(height/150.0,0.5),mix(0.7,0.98,1.0-u_lightintensity),1.0)));}v_color.r+=clamp(color.r*directional*u_lightcolor.r,mix(0.0,0.3,1.0-u_lightcolor.r),1.0);v_color.g+=clamp(color.g*directional*u_lightcolor.g,mix(0.0,0.3,1.0-u_lightcolor.g),1.0);v_color.b+=clamp(color.b*directional*u_lightcolor.b,mix(0.0,0.3,1.0-u_lightcolor.b),1.0);v_color*=u_opacity;}"),fillExtrusionPattern:me("uniform vec2 u_texsize;uniform float u_fade;uniform sampler2D u_image;varying vec2 v_pos_a;varying vec2 v_pos_b;varying vec4 v_lighting;\n#pragma mapbox: define lowp float base\n#pragma mapbox: define lowp float height\n#pragma mapbox: define lowp vec4 pattern_from\n#pragma mapbox: define lowp vec4 pattern_to\n#pragma mapbox: define lowp float pixel_ratio_from\n#pragma mapbox: define lowp float pixel_ratio_to\nvoid main() {\n#pragma mapbox: initialize lowp float base\n#pragma mapbox: initialize lowp float height\n#pragma mapbox: initialize mediump vec4 pattern_from\n#pragma mapbox: initialize mediump vec4 pattern_to\n#pragma mapbox: initialize lowp float pixel_ratio_from\n#pragma mapbox: initialize lowp float pixel_ratio_to\nvec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;vec2 imagecoord=mod(v_pos_a,1.0);vec2 pos=mix(pattern_tl_a/u_texsize,pattern_br_a/u_texsize,imagecoord);vec4 color1=texture2D(u_image,pos);vec2 imagecoord_b=mod(v_pos_b,1.0);vec2 pos2=mix(pattern_tl_b/u_texsize,pattern_br_b/u_texsize,imagecoord_b);vec4 color2=texture2D(u_image,pos2);vec4 mixedColor=mix(color1,color2,u_fade);gl_FragColor=mixedColor*v_lighting;\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","uniform mat4 u_matrix;uniform vec2 u_pixel_coord_upper;uniform vec2 u_pixel_coord_lower;uniform float u_height_factor;uniform vec3 u_scale;uniform float u_vertical_gradient;uniform lowp float u_opacity;uniform vec3 u_lightcolor;uniform lowp vec3 u_lightpos;uniform lowp float u_lightintensity;attribute vec2 a_pos;attribute vec4 a_normal_ed;\n#ifdef TERRAIN3D\nattribute vec2 a_centroid;\n#endif\nvarying vec2 v_pos_a;varying vec2 v_pos_b;varying vec4 v_lighting;\n#pragma mapbox: define lowp float base\n#pragma mapbox: define lowp float height\n#pragma mapbox: define lowp vec4 pattern_from\n#pragma mapbox: define lowp vec4 pattern_to\n#pragma mapbox: define lowp float pixel_ratio_from\n#pragma mapbox: define lowp float pixel_ratio_to\nvoid main() {\n#pragma mapbox: initialize lowp float base\n#pragma mapbox: initialize lowp float height\n#pragma mapbox: initialize mediump vec4 pattern_from\n#pragma mapbox: initialize mediump vec4 pattern_to\n#pragma mapbox: initialize lowp float pixel_ratio_from\n#pragma mapbox: initialize lowp float pixel_ratio_to\nvec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;float tileRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;vec3 normal=a_normal_ed.xyz;float edgedistance=a_normal_ed.w;vec2 display_size_a=(pattern_br_a-pattern_tl_a)/pixel_ratio_from;vec2 display_size_b=(pattern_br_b-pattern_tl_b)/pixel_ratio_to;\n#ifdef TERRAIN3D\nfloat height_terrain3d_offset=get_elevation(a_centroid);float base_terrain3d_offset=height_terrain3d_offset-(base > 0.0 ? 0.0 : 10.0);\n#else\nfloat height_terrain3d_offset=0.0;float base_terrain3d_offset=0.0;\n#endif\nbase=max(0.0,base)+base_terrain3d_offset;height=max(0.0,height)+height_terrain3d_offset;float t=mod(normal.x,2.0);float z=t > 0.0 ? height : base;gl_Position=u_matrix*vec4(a_pos,z,1);vec2 pos=normal.x==1.0 && normal.y==0.0 && normal.z==16384.0\n? a_pos\n: vec2(edgedistance,z*u_height_factor);v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,fromScale*display_size_a,tileRatio,pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,toScale*display_size_b,tileRatio,pos);v_lighting=vec4(0.0,0.0,0.0,1.0);float directional=clamp(dot(normal/16383.0,u_lightpos),0.0,1.0);directional=mix((1.0-u_lightintensity),max((0.5+u_lightintensity),1.0),directional);if (normal.y !=0.0) {directional*=((1.0-u_vertical_gradient)+(u_vertical_gradient*clamp((t+base)*pow(height/150.0,0.5),mix(0.7,0.98,1.0-u_lightintensity),1.0)));}v_lighting.rgb+=clamp(directional*u_lightcolor,mix(vec3(0.0),vec3(0.3),1.0-u_lightcolor),vec3(1.0));v_lighting*=u_opacity;}"),hillshadePrepare:me("#ifdef GL_ES\nprecision highp float;\n#endif\nuniform sampler2D u_image;varying vec2 v_pos;uniform vec2 u_dimension;uniform float u_zoom;uniform vec4 u_unpack;float getElevation(vec2 coord,float bias) {vec4 data=texture2D(u_image,coord)*255.0;data.a=-1.0;return dot(data,u_unpack)/4.0;}void main() {vec2 epsilon=1.0/u_dimension;float a=getElevation(v_pos+vec2(-epsilon.x,-epsilon.y),0.0);float b=getElevation(v_pos+vec2(0,-epsilon.y),0.0);float c=getElevation(v_pos+vec2(epsilon.x,-epsilon.y),0.0);float d=getElevation(v_pos+vec2(-epsilon.x,0),0.0);float e=getElevation(v_pos,0.0);float f=getElevation(v_pos+vec2(epsilon.x,0),0.0);float g=getElevation(v_pos+vec2(-epsilon.x,epsilon.y),0.0);float h=getElevation(v_pos+vec2(0,epsilon.y),0.0);float i=getElevation(v_pos+vec2(epsilon.x,epsilon.y),0.0);float exaggerationFactor=u_zoom < 2.0 ? 0.4 : u_zoom < 4.5 ? 0.35 : 0.3;float exaggeration=u_zoom < 15.0 ? (u_zoom-15.0)*exaggerationFactor : 0.0;vec2 deriv=vec2((c+f+f+i)-(a+d+d+g),(g+h+h+i)-(a+b+b+c))/pow(2.0,exaggeration+(19.2562-u_zoom));gl_FragColor=clamp(vec4(deriv.x/2.0+0.5,deriv.y/2.0+0.5,1.0,1.0),0.0,1.0);\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","uniform mat4 u_matrix;uniform vec2 u_dimension;attribute vec2 a_pos;attribute vec2 a_texture_pos;varying vec2 v_pos;void main() {gl_Position=u_matrix*vec4(a_pos,0,1);highp vec2 epsilon=1.0/u_dimension;float scale=(u_dimension.x-2.0)/u_dimension.x;v_pos=(a_texture_pos/8192.0)*scale+epsilon;}"),hillshade:me("uniform sampler2D u_image;varying vec2 v_pos;uniform vec2 u_latrange;uniform vec2 u_light;uniform vec4 u_shadow;uniform vec4 u_highlight;uniform vec4 u_accent;\n#define PI 3.141592653589793\nvoid main() {vec4 pixel=texture2D(u_image,v_pos);vec2 deriv=((pixel.rg*2.0)-1.0);float scaleFactor=cos(radians((u_latrange[0]-u_latrange[1])*(1.0-v_pos.y)+u_latrange[1]));float slope=atan(1.25*length(deriv)/scaleFactor);float aspect=deriv.x !=0.0 ? atan(deriv.y,-deriv.x) : PI/2.0*(deriv.y > 0.0 ? 1.0 :-1.0);float intensity=u_light.x;float azimuth=u_light.y+PI;float base=1.875-intensity*1.75;float maxValue=0.5*PI;float scaledSlope=intensity !=0.5 ? ((pow(base,slope)-1.0)/(pow(base,maxValue)-1.0))*maxValue : slope;float accent=cos(scaledSlope);vec4 accent_color=(1.0-accent)*u_accent*clamp(intensity*2.0,0.0,1.0);float shade=abs(mod((aspect+azimuth)/PI+0.5,2.0)-1.0);vec4 shade_color=mix(u_shadow,u_highlight,shade)*sin(scaledSlope)*clamp(intensity*2.0,0.0,1.0);gl_FragColor=accent_color*(1.0-shade_color.a)+shade_color;\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","uniform mat4 u_matrix;attribute vec2 a_pos;attribute vec2 a_texture_pos;varying vec2 v_pos;void main() {gl_Position=u_matrix*vec4(a_pos,0,1);v_pos=a_texture_pos/8192.0;}"),line:me("uniform lowp float u_device_pixel_ratio;varying vec2 v_width2;varying vec2 v_normal;varying float v_gamma_scale;\n#pragma mapbox: define highp vec4 color\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\nvoid main() {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\nfloat dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);gl_FragColor=color*(alpha*opacity);\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","\n#define scale 0.015873016\nattribute vec2 a_pos_normal;attribute vec4 a_data;uniform mat4 u_matrix;uniform mediump float u_ratio;uniform vec2 u_units_to_pixels;uniform lowp float u_device_pixel_ratio;varying vec2 v_normal;varying vec2 v_width2;varying float v_gamma_scale;varying highp float v_linesofar;\n#pragma mapbox: define highp vec4 color\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define mediump float gapwidth\n#pragma mapbox: define lowp float offset\n#pragma mapbox: define mediump float width\nvoid main() {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump float gapwidth\n#pragma mapbox: initialize lowp float offset\n#pragma mapbox: initialize mediump float width\nfloat ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=a_data.xy-128.0;float a_direction=mod(a_data.z,4.0)-1.0;v_linesofar=(floor(a_data.z/4.0)+a_data.w*64.0)*2.0;vec2 pos=floor(a_pos_normal*0.5);mediump vec2 normal=a_pos_normal-2.0*pos;normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);vec4 projected_extrude=u_matrix*vec4(dist/u_ratio,0.0,0.0);gl_Position=u_matrix*vec4(pos+offset2/u_ratio,0.0,1.0)+projected_extrude;\n#ifdef TERRAIN3D\nv_gamma_scale=1.0;\n#else\nfloat extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length(projected_extrude.xy/gl_Position.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective;\n#endif\nv_width2=vec2(outset,inset);}"),lineGradient:me("uniform lowp float u_device_pixel_ratio;uniform sampler2D u_image;varying vec2 v_width2;varying vec2 v_normal;varying float v_gamma_scale;varying highp vec2 v_uv;\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\nvoid main() {\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\nfloat dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);vec4 color=texture2D(u_image,v_uv);gl_FragColor=color*(alpha*opacity);\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","\n#define scale 0.015873016\nattribute vec2 a_pos_normal;attribute vec4 a_data;attribute float a_uv_x;attribute float a_split_index;uniform mat4 u_matrix;uniform mediump float u_ratio;uniform lowp float u_device_pixel_ratio;uniform vec2 u_units_to_pixels;uniform float u_image_height;varying vec2 v_normal;varying vec2 v_width2;varying float v_gamma_scale;varying highp vec2 v_uv;\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define mediump float gapwidth\n#pragma mapbox: define lowp float offset\n#pragma mapbox: define mediump float width\nvoid main() {\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump float gapwidth\n#pragma mapbox: initialize lowp float offset\n#pragma mapbox: initialize mediump float width\nfloat ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=a_data.xy-128.0;float a_direction=mod(a_data.z,4.0)-1.0;highp float texel_height=1.0/u_image_height;highp float half_texel_height=0.5*texel_height;v_uv=vec2(a_uv_x,a_split_index*texel_height-half_texel_height);vec2 pos=floor(a_pos_normal*0.5);mediump vec2 normal=a_pos_normal-2.0*pos;normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);vec4 projected_extrude=u_matrix*vec4(dist/u_ratio,0.0,0.0);gl_Position=u_matrix*vec4(pos+offset2/u_ratio,0.0,1.0)+projected_extrude;\n#ifdef TERRAIN3D\nv_gamma_scale=1.0;\n#else\nfloat extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length(projected_extrude.xy/gl_Position.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective;\n#endif\nv_width2=vec2(outset,inset);}"),linePattern:me("#ifdef GL_ES\nprecision highp float;\n#endif\nuniform lowp float u_device_pixel_ratio;uniform vec2 u_texsize;uniform float u_fade;uniform mediump vec3 u_scale;uniform sampler2D u_image;varying vec2 v_normal;varying vec2 v_width2;varying float v_linesofar;varying float v_gamma_scale;varying float v_width;\n#pragma mapbox: define lowp vec4 pattern_from\n#pragma mapbox: define lowp vec4 pattern_to\n#pragma mapbox: define lowp float pixel_ratio_from\n#pragma mapbox: define lowp float pixel_ratio_to\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\nvoid main() {\n#pragma mapbox: initialize mediump vec4 pattern_from\n#pragma mapbox: initialize mediump vec4 pattern_to\n#pragma mapbox: initialize lowp float pixel_ratio_from\n#pragma mapbox: initialize lowp float pixel_ratio_to\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\nvec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;float tileZoomRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;vec2 display_size_a=(pattern_br_a-pattern_tl_a)/pixel_ratio_from;vec2 display_size_b=(pattern_br_b-pattern_tl_b)/pixel_ratio_to;vec2 pattern_size_a=vec2(display_size_a.x*fromScale/tileZoomRatio,display_size_a.y);vec2 pattern_size_b=vec2(display_size_b.x*toScale/tileZoomRatio,display_size_b.y);float aspect_a=display_size_a.y/v_width;float aspect_b=display_size_b.y/v_width;float dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);float x_a=mod(v_linesofar/pattern_size_a.x*aspect_a,1.0);float x_b=mod(v_linesofar/pattern_size_b.x*aspect_b,1.0);float y=0.5*v_normal.y+0.5;vec2 texel_size=1.0/u_texsize;vec2 pos_a=mix(pattern_tl_a*texel_size-texel_size,pattern_br_a*texel_size+texel_size,vec2(x_a,y));vec2 pos_b=mix(pattern_tl_b*texel_size-texel_size,pattern_br_b*texel_size+texel_size,vec2(x_b,y));vec4 color=mix(texture2D(u_image,pos_a),texture2D(u_image,pos_b),u_fade);gl_FragColor=color*alpha*opacity;\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","\n#define scale 0.015873016\n#define LINE_DISTANCE_SCALE 2.0\nattribute vec2 a_pos_normal;attribute vec4 a_data;uniform mat4 u_matrix;uniform vec2 u_units_to_pixels;uniform mediump float u_ratio;uniform lowp float u_device_pixel_ratio;varying vec2 v_normal;varying vec2 v_width2;varying float v_linesofar;varying float v_gamma_scale;varying float v_width;\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define lowp float offset\n#pragma mapbox: define mediump float gapwidth\n#pragma mapbox: define mediump float width\n#pragma mapbox: define lowp float floorwidth\n#pragma mapbox: define lowp vec4 pattern_from\n#pragma mapbox: define lowp vec4 pattern_to\n#pragma mapbox: define lowp float pixel_ratio_from\n#pragma mapbox: define lowp float pixel_ratio_to\nvoid main() {\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize lowp float offset\n#pragma mapbox: initialize mediump float gapwidth\n#pragma mapbox: initialize mediump float width\n#pragma mapbox: initialize lowp float floorwidth\n#pragma mapbox: initialize mediump vec4 pattern_from\n#pragma mapbox: initialize mediump vec4 pattern_to\n#pragma mapbox: initialize lowp float pixel_ratio_from\n#pragma mapbox: initialize lowp float pixel_ratio_to\nfloat ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=a_data.xy-128.0;float a_direction=mod(a_data.z,4.0)-1.0;float a_linesofar=(floor(a_data.z/4.0)+a_data.w*64.0)*LINE_DISTANCE_SCALE;vec2 pos=floor(a_pos_normal*0.5);mediump vec2 normal=a_pos_normal-2.0*pos;normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);vec4 projected_extrude=u_matrix*vec4(dist/u_ratio,0.0,0.0);gl_Position=u_matrix*vec4(pos+offset2/u_ratio,0.0,1.0)+projected_extrude;\n#ifdef TERRAIN3D\nv_gamma_scale=1.0;\n#else\nfloat extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length(projected_extrude.xy/gl_Position.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective;\n#endif\nv_linesofar=a_linesofar;v_width2=vec2(outset,inset);v_width=floorwidth;}"),lineSDF:me("uniform lowp float u_device_pixel_ratio;uniform sampler2D u_image;uniform float u_sdfgamma;uniform float u_mix;varying vec2 v_normal;varying vec2 v_width2;varying vec2 v_tex_a;varying vec2 v_tex_b;varying float v_gamma_scale;\n#pragma mapbox: define highp vec4 color\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define mediump float width\n#pragma mapbox: define lowp float floorwidth\nvoid main() {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump float width\n#pragma mapbox: initialize lowp float floorwidth\nfloat dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);float sdfdist_a=texture2D(u_image,v_tex_a).a;float sdfdist_b=texture2D(u_image,v_tex_b).a;float sdfdist=mix(sdfdist_a,sdfdist_b,u_mix);alpha*=smoothstep(0.5-u_sdfgamma/floorwidth,0.5+u_sdfgamma/floorwidth,sdfdist);gl_FragColor=color*(alpha*opacity);\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","\n#define scale 0.015873016\n#define LINE_DISTANCE_SCALE 2.0\nattribute vec2 a_pos_normal;attribute vec4 a_data;uniform mat4 u_matrix;uniform mediump float u_ratio;uniform lowp float u_device_pixel_ratio;uniform vec2 u_patternscale_a;uniform float u_tex_y_a;uniform vec2 u_patternscale_b;uniform float u_tex_y_b;uniform vec2 u_units_to_pixels;varying vec2 v_normal;varying vec2 v_width2;varying vec2 v_tex_a;varying vec2 v_tex_b;varying float v_gamma_scale;\n#pragma mapbox: define highp vec4 color\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define mediump float gapwidth\n#pragma mapbox: define lowp float offset\n#pragma mapbox: define mediump float width\n#pragma mapbox: define lowp float floorwidth\nvoid main() {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump float gapwidth\n#pragma mapbox: initialize lowp float offset\n#pragma mapbox: initialize mediump float width\n#pragma mapbox: initialize lowp float floorwidth\nfloat ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=a_data.xy-128.0;float a_direction=mod(a_data.z,4.0)-1.0;float a_linesofar=(floor(a_data.z/4.0)+a_data.w*64.0)*LINE_DISTANCE_SCALE;vec2 pos=floor(a_pos_normal*0.5);mediump vec2 normal=a_pos_normal-2.0*pos;normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);vec4 projected_extrude=u_matrix*vec4(dist/u_ratio,0.0,0.0);gl_Position=u_matrix*vec4(pos+offset2/u_ratio,0.0,1.0)+projected_extrude;\n#ifdef TERRAIN3D\nv_gamma_scale=1.0;\n#else\nfloat extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length(projected_extrude.xy/gl_Position.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective;\n#endif\nv_tex_a=vec2(a_linesofar*u_patternscale_a.x/floorwidth,normal.y*u_patternscale_a.y+u_tex_y_a);v_tex_b=vec2(a_linesofar*u_patternscale_b.x/floorwidth,normal.y*u_patternscale_b.y+u_tex_y_b);v_width2=vec2(outset,inset);}"),raster:me("uniform float u_fade_t;uniform float u_opacity;uniform sampler2D u_image0;uniform sampler2D u_image1;varying vec2 v_pos0;varying vec2 v_pos1;uniform float u_brightness_low;uniform float u_brightness_high;uniform float u_saturation_factor;uniform float u_contrast_factor;uniform vec3 u_spin_weights;void main() {vec4 color0=texture2D(u_image0,v_pos0);vec4 color1=texture2D(u_image1,v_pos1);if (color0.a > 0.0) {color0.rgb=color0.rgb/color0.a;}if (color1.a > 0.0) {color1.rgb=color1.rgb/color1.a;}vec4 color=mix(color0,color1,u_fade_t);color.a*=u_opacity;vec3 rgb=color.rgb;rgb=vec3(dot(rgb,u_spin_weights.xyz),dot(rgb,u_spin_weights.zxy),dot(rgb,u_spin_weights.yzx));float average=(color.r+color.g+color.b)/3.0;rgb+=(average-rgb)*u_saturation_factor;rgb=(rgb-0.5)*u_contrast_factor+0.5;vec3 u_high_vec=vec3(u_brightness_low,u_brightness_low,u_brightness_low);vec3 u_low_vec=vec3(u_brightness_high,u_brightness_high,u_brightness_high);gl_FragColor=vec4(mix(u_high_vec,u_low_vec,rgb)*color.a,color.a);\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","uniform mat4 u_matrix;uniform vec2 u_tl_parent;uniform float u_scale_parent;uniform float u_buffer_scale;attribute vec2 a_pos;attribute vec2 a_texture_pos;varying vec2 v_pos0;varying vec2 v_pos1;void main() {gl_Position=u_matrix*vec4(a_pos,0,1);v_pos0=(((a_texture_pos/8192.0)-0.5)/u_buffer_scale )+0.5;v_pos1=(v_pos0*u_scale_parent)+u_tl_parent;}"),symbolIcon:me("uniform sampler2D u_texture;varying vec2 v_tex;varying float v_fade_opacity;\n#pragma mapbox: define lowp float opacity\nvoid main() {\n#pragma mapbox: initialize lowp float opacity\nlowp float alpha=opacity*v_fade_opacity;gl_FragColor=texture2D(u_texture,v_tex)*alpha;\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","attribute vec4 a_pos_offset;attribute vec4 a_data;attribute vec4 a_pixeloffset;attribute vec3 a_projected_pos;attribute float a_fade_opacity;uniform bool u_is_size_zoom_constant;uniform bool u_is_size_feature_constant;uniform highp float u_size_t;uniform highp float u_size;uniform highp float u_camera_to_center_distance;uniform highp float u_pitch;uniform bool u_rotate_symbol;uniform highp float u_aspect_ratio;uniform float u_fade_change;uniform mat4 u_matrix;uniform mat4 u_label_plane_matrix;uniform mat4 u_coord_matrix;uniform bool u_is_text;uniform bool u_pitch_with_map;uniform vec2 u_texsize;uniform bool u_is_along_line;uniform bool u_is_variable_anchor;uniform vec2 u_translation;uniform float u_pitched_scale;varying vec2 v_tex;varying float v_fade_opacity;vec4 projectTileWithElevation(vec2 posInTile,float elevation) {return u_matrix*vec4(posInTile,elevation,1.0);}\n#pragma mapbox: define lowp float opacity\nvoid main() {\n#pragma mapbox: initialize lowp float opacity\nvec2 a_pos=a_pos_offset.xy;vec2 a_offset=a_pos_offset.zw;vec2 a_tex=a_data.xy;vec2 a_size=a_data.zw;float a_size_min=floor(a_size[0]*0.5);vec2 a_pxoffset=a_pixeloffset.xy;vec2 a_minFontScale=a_pixeloffset.zw/256.0;float ele=get_elevation(a_pos);highp float segment_angle=-a_projected_pos[2];float size;if (!u_is_size_zoom_constant && !u_is_size_feature_constant) {size=mix(a_size_min,a_size[1],u_size_t)/128.0;} else if (u_is_size_zoom_constant && !u_is_size_feature_constant) {size=a_size_min/128.0;} else {size=u_size;}vec2 translated_a_pos=a_pos+u_translation;vec4 projectedPoint=projectTileWithElevation(translated_a_pos,ele);highp float camera_to_anchor_distance=projectedPoint.w;highp float distance_ratio=u_pitch_with_map ?\ncamera_to_anchor_distance/u_camera_to_center_distance :\nu_camera_to_center_distance/camera_to_anchor_distance;highp float perspective_ratio=clamp(0.5+0.5*distance_ratio,0.0,4.0);size*=perspective_ratio;float fontScale=u_is_text ? size/24.0 : size;highp float symbol_rotation=0.0;if (u_rotate_symbol) {vec4 offsetProjectedPoint=projectTileWithElevation(translated_a_pos+vec2(1,0),ele);vec2 a=projectedPoint.xy/projectedPoint.w;vec2 b=offsetProjectedPoint.xy/offsetProjectedPoint.w;symbol_rotation=atan((b.y-a.y)/u_aspect_ratio,b.x-a.x);}highp float angle_sin=sin(segment_angle+symbol_rotation);highp float angle_cos=cos(segment_angle+symbol_rotation);mat2 rotation_matrix=mat2(angle_cos,-1.0*angle_sin,angle_sin,angle_cos);vec4 projected_pos;if (u_is_along_line || u_is_variable_anchor) {projected_pos=vec4(a_projected_pos.xy,ele,1.0);} else if (u_pitch_with_map) {projected_pos=u_label_plane_matrix*vec4(a_projected_pos.xy+u_translation,ele,1.0);} else {projected_pos=u_label_plane_matrix*projectTileWithElevation(a_projected_pos.xy+u_translation,ele);}float z=float(u_pitch_with_map)*projected_pos.z/projected_pos.w;float projectionScaling=1.0;vec4 finalPos=u_coord_matrix*vec4(projected_pos.xy/projected_pos.w+rotation_matrix*(a_offset/32.0*max(a_minFontScale,fontScale)+a_pxoffset/16.0)*projectionScaling,z,1.0);if(u_pitch_with_map) {finalPos=projectTileWithElevation(finalPos.xy,finalPos.z);}gl_Position=finalPos;v_tex=a_tex/u_texsize;vec2 fade_opacity=unpack_opacity(a_fade_opacity);float fade_change=fade_opacity[1] > 0.5 ? u_fade_change :-u_fade_change;float visibility=calculate_visibility(projectedPoint);v_fade_opacity=max(0.0,min(visibility,fade_opacity[0]+fade_change));}"),symbolSDF:me("#define SDF_PX 8.0\nuniform bool u_is_halo;uniform sampler2D u_texture;uniform highp float u_gamma_scale;uniform lowp float u_device_pixel_ratio;uniform bool u_is_text;varying vec2 v_data0;varying vec3 v_data1;\n#pragma mapbox: define highp vec4 fill_color\n#pragma mapbox: define highp vec4 halo_color\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define lowp float halo_width\n#pragma mapbox: define lowp float halo_blur\nvoid main() {\n#pragma mapbox: initialize highp vec4 fill_color\n#pragma mapbox: initialize highp vec4 halo_color\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize lowp float halo_width\n#pragma mapbox: initialize lowp float halo_blur\nfloat EDGE_GAMMA=0.105/u_device_pixel_ratio;vec2 tex=v_data0.xy;float gamma_scale=v_data1.x;float size=v_data1.y;float fade_opacity=v_data1[2];float fontScale=u_is_text ? size/24.0 : size;lowp vec4 color=fill_color;highp float gamma=EDGE_GAMMA/(fontScale*u_gamma_scale);lowp float inner_edge=(256.0-64.0)/256.0;if (u_is_halo) {color=halo_color;gamma=(halo_blur*1.19/SDF_PX+EDGE_GAMMA)/(fontScale*u_gamma_scale);inner_edge=inner_edge+gamma*gamma_scale;}lowp float dist=texture2D(u_texture,tex).a;highp float gamma_scaled=gamma*gamma_scale;highp float alpha=smoothstep(inner_edge-gamma_scaled,inner_edge+gamma_scaled,dist);if (u_is_halo) {lowp float halo_edge=(6.0-halo_width/fontScale)/SDF_PX;alpha=min(smoothstep(halo_edge-gamma_scaled,halo_edge+gamma_scaled,dist),1.0-alpha);}gl_FragColor=color*(alpha*opacity*fade_opacity);\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","attribute vec4 a_pos_offset;attribute vec4 a_data;attribute vec4 a_pixeloffset;attribute vec3 a_projected_pos;attribute float a_fade_opacity;uniform bool u_is_size_zoom_constant;uniform bool u_is_size_feature_constant;uniform highp float u_size_t;uniform highp float u_size;uniform mat4 u_matrix;uniform mat4 u_label_plane_matrix;uniform mat4 u_coord_matrix;uniform bool u_is_text;uniform bool u_pitch_with_map;uniform bool u_is_along_line;uniform bool u_is_variable_anchor;uniform highp float u_pitch;uniform bool u_rotate_symbol;uniform highp float u_aspect_ratio;uniform highp float u_camera_to_center_distance;uniform float u_fade_change;uniform vec2 u_texsize;uniform vec2 u_translation;uniform float u_pitched_scale;varying vec2 v_data0;varying vec3 v_data1;vec4 projectTileWithElevation(vec2 posInTile,float elevation) {return u_matrix*vec4(posInTile,elevation,1.0);}\n#pragma mapbox: define highp vec4 fill_color\n#pragma mapbox: define highp vec4 halo_color\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define lowp float halo_width\n#pragma mapbox: define lowp float halo_blur\nvoid main() {\n#pragma mapbox: initialize highp vec4 fill_color\n#pragma mapbox: initialize highp vec4 halo_color\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize lowp float halo_width\n#pragma mapbox: initialize lowp float halo_blur\nvec2 a_pos=a_pos_offset.xy;vec2 a_offset=a_pos_offset.zw;vec2 a_tex=a_data.xy;vec2 a_size=a_data.zw;float a_size_min=floor(a_size[0]*0.5);vec2 a_pxoffset=a_pixeloffset.xy;float ele=get_elevation(a_pos);highp float segment_angle=-a_projected_pos[2];float size;if (!u_is_size_zoom_constant && !u_is_size_feature_constant) {size=mix(a_size_min,a_size[1],u_size_t)/128.0;} else if (u_is_size_zoom_constant && !u_is_size_feature_constant) {size=a_size_min/128.0;} else {size=u_size;}vec2 translated_a_pos=a_pos+u_translation;vec4 projectedPoint=projectTileWithElevation(translated_a_pos,ele);highp float camera_to_anchor_distance=projectedPoint.w;highp float distance_ratio=u_pitch_with_map ?\ncamera_to_anchor_distance/u_camera_to_center_distance :\nu_camera_to_center_distance/camera_to_anchor_distance;highp float perspective_ratio=clamp(0.5+0.5*distance_ratio,0.0,4.0);size*=perspective_ratio;float fontScale=u_is_text ? size/24.0 : size;highp float symbol_rotation=0.0;if (u_rotate_symbol) {vec4 offsetProjectedPoint=projectTileWithElevation(translated_a_pos+vec2(1,0),ele);vec2 a=projectedPoint.xy/projectedPoint.w;vec2 b=offsetProjectedPoint.xy/offsetProjectedPoint.w;symbol_rotation=atan((b.y-a.y)/u_aspect_ratio,b.x-a.x);}highp float angle_sin=sin(segment_angle+symbol_rotation);highp float angle_cos=cos(segment_angle+symbol_rotation);mat2 rotation_matrix=mat2(angle_cos,-1.0*angle_sin,angle_sin,angle_cos);vec4 projected_pos;if (u_is_along_line || u_is_variable_anchor) {projected_pos=vec4(a_projected_pos.xy,ele,1.0);} else if (u_pitch_with_map) {projected_pos=u_label_plane_matrix*vec4(a_projected_pos.xy+u_translation,ele,1.0);} else {projected_pos=u_label_plane_matrix*projectTileWithElevation(a_projected_pos.xy+u_translation,ele);}float z=float(u_pitch_with_map)*projected_pos.z/projected_pos.w;float projectionScaling=1.0;vec4 finalPos=u_coord_matrix*vec4(projected_pos.xy/projected_pos.w+rotation_matrix*(a_offset/32.0*fontScale+a_pxoffset)*projectionScaling,z,1.0);if(u_pitch_with_map) {finalPos=projectTileWithElevation(finalPos.xy,finalPos.z);}float gamma_scale=finalPos.w;gl_Position=finalPos;vec2 fade_opacity=unpack_opacity(a_fade_opacity);float visibility=calculate_visibility(projectedPoint);float fade_change=fade_opacity[1] > 0.5 ? u_fade_change :-u_fade_change;float interpolated_fade_opacity=max(0.0,min(visibility,fade_opacity[0]+fade_change));v_data0=a_tex/u_texsize;v_data1=vec3(gamma_scale,size,interpolated_fade_opacity);}"),symbolTextAndIcon:me("#define SDF_PX 8.0\n#define SDF 1.0\n#define ICON 0.0\nuniform bool u_is_halo;uniform sampler2D u_texture;uniform sampler2D u_texture_icon;uniform highp float u_gamma_scale;uniform lowp float u_device_pixel_ratio;varying vec4 v_data0;varying vec4 v_data1;\n#pragma mapbox: define highp vec4 fill_color\n#pragma mapbox: define highp vec4 halo_color\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define lowp float halo_width\n#pragma mapbox: define lowp float halo_blur\nvoid main() {\n#pragma mapbox: initialize highp vec4 fill_color\n#pragma mapbox: initialize highp vec4 halo_color\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize lowp float halo_width\n#pragma mapbox: initialize lowp float halo_blur\nfloat fade_opacity=v_data1[2];if (v_data1.w==ICON) {vec2 tex_icon=v_data0.zw;lowp float alpha=opacity*fade_opacity;gl_FragColor=texture2D(u_texture_icon,tex_icon)*alpha;\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\nreturn;}vec2 tex=v_data0.xy;float EDGE_GAMMA=0.105/u_device_pixel_ratio;float gamma_scale=v_data1.x;float size=v_data1.y;float fontScale=size/24.0;lowp vec4 color=fill_color;highp float gamma=EDGE_GAMMA/(fontScale*u_gamma_scale);lowp float buff=(256.0-64.0)/256.0;if (u_is_halo) {color=halo_color;gamma=(halo_blur*1.19/SDF_PX+EDGE_GAMMA)/(fontScale*u_gamma_scale);buff=(6.0-halo_width/fontScale)/SDF_PX;}lowp float dist=texture2D(u_texture,tex).a;highp float gamma_scaled=gamma*gamma_scale;highp float alpha=smoothstep(buff-gamma_scaled,buff+gamma_scaled,dist);gl_FragColor=color*(alpha*opacity*fade_opacity);\n#ifdef OVERDRAW_INSPECTOR\ngl_FragColor=vec4(1.0);\n#endif\n}","attribute vec4 a_pos_offset;attribute vec4 a_data;attribute vec3 a_projected_pos;attribute float a_fade_opacity;uniform bool u_is_size_zoom_constant;uniform bool u_is_size_feature_constant;uniform highp float u_size_t;uniform highp float u_size;uniform mat4 u_matrix;uniform mat4 u_label_plane_matrix;uniform mat4 u_coord_matrix;uniform bool u_is_text;uniform bool u_pitch_with_map;uniform highp float u_pitch;uniform bool u_rotate_symbol;uniform highp float u_aspect_ratio;uniform highp float u_camera_to_center_distance;uniform float u_fade_change;uniform vec2 u_texsize;uniform vec2 u_texsize_icon;uniform bool u_is_along_line;uniform bool u_is_variable_anchor;uniform vec2 u_translation;uniform float u_pitched_scale;varying vec4 v_data0;varying vec4 v_data1;vec4 projectTileWithElevation(vec2 posInTile,float elevation) {return u_matrix*vec4(posInTile,elevation,1.0);}\n#pragma mapbox: define highp vec4 fill_color\n#pragma mapbox: define highp vec4 halo_color\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define lowp float halo_width\n#pragma mapbox: define lowp float halo_blur\nvoid main() {\n#pragma mapbox: initialize highp vec4 fill_color\n#pragma mapbox: initialize highp vec4 halo_color\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize lowp float halo_width\n#pragma mapbox: initialize lowp float halo_blur\nvec2 a_pos=a_pos_offset.xy;vec2 a_offset=a_pos_offset.zw;vec2 a_tex=a_data.xy;vec2 a_size=a_data.zw;float a_size_min=floor(a_size[0]*0.5);float is_sdf=a_size[0]-2.0*a_size_min;float ele=get_elevation(a_pos);highp float segment_angle=-a_projected_pos[2];float size;if (!u_is_size_zoom_constant && !u_is_size_feature_constant) {size=mix(a_size_min,a_size[1],u_size_t)/128.0;} else if (u_is_size_zoom_constant && !u_is_size_feature_constant) {size=a_size_min/128.0;} else {size=u_size;}vec2 translated_a_pos=a_pos+u_translation;vec4 projectedPoint=projectTileWithElevation(translated_a_pos,ele);highp float camera_to_anchor_distance=projectedPoint.w;highp float distance_ratio=u_pitch_with_map ?\ncamera_to_anchor_distance/u_camera_to_center_distance :\nu_camera_to_center_distance/camera_to_anchor_distance;highp float perspective_ratio=clamp(0.5+0.5*distance_ratio,0.0,4.0);size*=perspective_ratio;float fontScale=size/24.0;highp float symbol_rotation=0.0;if (u_rotate_symbol) {vec4 offsetProjectedPoint=projectTileWithElevation(translated_a_pos+vec2(1,0),ele);vec2 a=projectedPoint.xy/projectedPoint.w;vec2 b=offsetProjectedPoint.xy/offsetProjectedPoint.w;symbol_rotation=atan((b.y-a.y)/u_aspect_ratio,b.x-a.x);}highp float angle_sin=sin(segment_angle+symbol_rotation);highp float angle_cos=cos(segment_angle+symbol_rotation);mat2 rotation_matrix=mat2(angle_cos,-1.0*angle_sin,angle_sin,angle_cos);vec4 projected_pos;if (u_is_along_line || u_is_variable_anchor) {projected_pos=vec4(a_projected_pos.xy,ele,1.0);} else if (u_pitch_with_map) {projected_pos=u_label_plane_matrix*vec4(a_projected_pos.xy+u_translation,ele,1.0);} else {projected_pos=u_label_plane_matrix*projectTileWithElevation(a_projected_pos.xy+u_translation,ele);}float z=float(u_pitch_with_map)*projected_pos.z/projected_pos.w;float projectionScaling=1.0;vec4 finalPos=u_coord_matrix*vec4(projected_pos.xy/projected_pos.w+rotation_matrix*(a_offset/32.0*fontScale)*projectionScaling,z,1.0);if(u_pitch_with_map) {finalPos=projectTileWithElevation(finalPos.xy,finalPos.z);}float gamma_scale=finalPos.w;gl_Position=finalPos;vec2 fade_opacity=unpack_opacity(a_fade_opacity);float visibility=calculate_visibility(projectedPoint);float fade_change=fade_opacity[1] > 0.5 ? u_fade_change :-u_fade_change;float interpolated_fade_opacity=max(0.0,min(visibility,fade_opacity[0]+fade_change));v_data0.xy=a_tex/u_texsize;v_data0.zw=a_tex/u_texsize_icon;v_data1=vec4(gamma_scale,size,interpolated_fade_opacity,is_sdf);}"),terrain:me("uniform sampler2D u_texture;uniform vec4 u_fog_color;uniform vec4 u_horizon_color;uniform float u_fog_ground_blend;uniform float u_fog_ground_blend_opacity;uniform float u_horizon_fog_blend;varying vec2 v_texture_pos;varying float v_fog_depth;const float gamma=2.2;vec4 gammaToLinear(vec4 color) {return pow(color,vec4(gamma));}vec4 linearToGamma(vec4 color) {return pow(color,vec4(1.0/gamma));}void main() {vec4 surface_color=texture2D(u_texture,v_texture_pos);if (v_fog_depth > u_fog_ground_blend) {vec4 surface_color_linear=gammaToLinear(surface_color);float blend_color=smoothstep(0.0,1.0,max((v_fog_depth-u_horizon_fog_blend)/(1.0-u_horizon_fog_blend),0.0));vec4 fog_horizon_color_linear=mix(gammaToLinear(u_fog_color),gammaToLinear(u_horizon_color),blend_color);float factor_fog=max(v_fog_depth-u_fog_ground_blend,0.0)/(1.0-u_fog_ground_blend);gl_FragColor=linearToGamma(mix(surface_color_linear,fog_horizon_color_linear,pow(factor_fog,2.0)*u_fog_ground_blend_opacity));} else {gl_FragColor=surface_color;}}","attribute vec3 a_pos3d;uniform mat4 u_matrix;uniform mat4 u_fog_matrix;uniform float u_ele_delta;varying vec2 v_texture_pos;varying float v_fog_depth;void main() {float ele=get_elevation(a_pos3d.xy);float ele_delta=a_pos3d.z==1.0 ? u_ele_delta : 0.0;v_texture_pos=a_pos3d.xy/8192.0;gl_Position=u_matrix*vec4(a_pos3d.xy,ele-ele_delta,1.0);vec4 pos=u_fog_matrix*vec4(a_pos3d.xy,ele,1.0);v_fog_depth=pos.z/pos.w*0.5+0.5;}"),terrainDepth:me("varying float v_depth;const highp vec4 bitSh=vec4(256.*256.*256.,256.*256.,256.,1.);const highp vec4 bitMsk=vec4(0.,vec3(1./256.0));highp vec4 pack(highp float value) {highp vec4 comp=fract(value*bitSh);comp-=comp.xxyz*bitMsk;return comp;}void main() {gl_FragColor=pack(v_depth);}","attribute vec3 a_pos3d;uniform mat4 u_matrix;uniform float u_ele_delta;varying float v_depth;void main() {float ele=get_elevation(a_pos3d.xy);float ele_delta=a_pos3d.z==1.0 ? u_ele_delta : 0.0;gl_Position=u_matrix*vec4(a_pos3d.xy,ele-ele_delta,1.0);v_depth=gl_Position.z/gl_Position.w;}"),terrainCoords:me("precision mediump float;uniform sampler2D u_texture;uniform float u_terrain_coords_id;varying vec2 v_texture_pos;void main() {vec4 rgba=texture2D(u_texture,v_texture_pos);gl_FragColor=vec4(rgba.r,rgba.g,rgba.b,u_terrain_coords_id);}","attribute vec3 a_pos3d;uniform mat4 u_matrix;uniform float u_ele_delta;varying vec2 v_texture_pos;void main() {float ele=get_elevation(a_pos3d.xy);float ele_delta=a_pos3d.z==1.0 ? u_ele_delta : 0.0;v_texture_pos=a_pos3d.xy/8192.0;gl_Position=u_matrix*vec4(a_pos3d.xy,ele-ele_delta,1.0);}"),sky:me("uniform vec4 u_sky_color;uniform vec4 u_horizon_color;uniform float u_horizon;uniform float u_sky_horizon_blend;void main() {float y=gl_FragCoord.y;if (y > u_horizon) {float blend=y-u_horizon;if (blend < u_sky_horizon_blend) {gl_FragColor=mix(u_sky_color,u_horizon_color,pow(1.0-blend/u_sky_horizon_blend,2.0));} else {gl_FragColor=u_sky_color;}}}","attribute vec2 a_pos;void main() {gl_Position=vec4(a_pos,1.0,1.0);}")};function me(t,e){const i=/#pragma mapbox: ([\w]+) ([\w]+) ([\w]+) ([\w]+)/g,a=e.match(/attribute ([\w]+) ([\w]+)/g),s=t.match(/uniform ([\w]+) ([\w]+)([\s]*)([\w]*)/g),o=e.match(/uniform ([\w]+) ([\w]+)([\s]*)([\w]*)/g),r=o?o.concat(s):s,n={};return {fragmentSource:t=t.replace(i,((t,e,i,a,s)=>(n[s]=!0,"define"===e?`\n#ifndef HAS_UNIFORM_u_${s}\nvarying ${i} ${a} ${s};\n#else\nuniform ${i} ${a} u_${s};\n#endif\n`:`\n#ifdef HAS_UNIFORM_u_${s}\n ${i} ${a} ${s} = u_${s};\n#endif\n`))),vertexSource:e=e.replace(i,((t,e,i,a,s)=>{const o="float"===a?"vec2":"vec4",r=s.match(/color/)?"color":o;return n[s]?"define"===e?`\n#ifndef HAS_UNIFORM_u_${s}\nuniform lowp float u_${s}_t;\nattribute ${i} ${o} a_${s};\nvarying ${i} ${a} ${s};\n#else\nuniform ${i} ${a} u_${s};\n#endif\n`:"vec4"===r?`\n#ifndef HAS_UNIFORM_u_${s}\n ${s} = a_${s};\n#else\n ${i} ${a} ${s} = u_${s};\n#endif\n`:`\n#ifndef HAS_UNIFORM_u_${s}\n ${s} = unpack_mix_${r}(a_${s}, u_${s}_t);\n#else\n ${i} ${a} ${s} = u_${s};\n#endif\n`:"define"===e?`\n#ifndef HAS_UNIFORM_u_${s}\nuniform lowp float u_${s}_t;\nattribute ${i} ${o} a_${s};\n#else\nuniform ${i} ${a} u_${s};\n#endif\n`:"vec4"===r?`\n#ifndef HAS_UNIFORM_u_${s}\n ${i} ${a} ${s} = a_${s};\n#else\n ${i} ${a} ${s} = u_${s};\n#endif\n`:`\n#ifndef HAS_UNIFORM_u_${s}\n ${i} ${a} ${s} = unpack_mix_${r}(a_${s}, u_${s}_t);\n#else\n ${i} ${a} ${s} = u_${s};\n#endif\n`})),staticAttributes:a,staticUniforms:r}}class fe{constructor(){this.boundProgram=null,this.boundLayoutVertexBuffer=null,this.boundPaintVertexBuffers=[],this.boundIndexBuffer=null,this.boundVertexOffset=null,this.boundDynamicVertexBuffer=null,this.vao=null;}bind(t,e,i,a,s,o,r,n,l){this.context=t;let h=this.boundPaintVertexBuffers.length!==a.length;for(let t=0;!h&&t({u_matrix:t,u_texture:0,u_ele_delta:i,u_fog_matrix:a,u_fog_color:s?s.properties.get("fog-color"):e.aM.white,u_fog_ground_blend:s?s.properties.get("fog-ground-blend"):1,u_fog_ground_blend_opacity:s?s.calculateFogBlendOpacity(o):0,u_horizon_color:s?s.properties.get("horizon-color"):e.aM.white,u_horizon_fog_blend:s?s.properties.get("horizon-fog-blend"):1});function ve(t){const e=[];for(let i=0;i({u_depth:new e.aH(t,i.u_depth),u_terrain:new e.aH(t,i.u_terrain),u_terrain_dim:new e.aI(t,i.u_terrain_dim),u_terrain_matrix:new e.aJ(t,i.u_terrain_matrix),u_terrain_unpack:new e.aK(t,i.u_terrain_unpack),u_terrain_exaggeration:new e.aI(t,i.u_terrain_exaggeration)}))(t,b),this.binderUniforms=a?a.getUniforms(t,b):[];}draw(t,e,i,a,s,o,r,n,l,h,c,u,d,_,p,m,f,g){const v=t.gl;if(this.failedToCreate)return;if(t.program.set(this.program),t.setDepthMode(i),t.setStencilMode(a),t.setColorMode(s),t.setCullFace(o),n){t.activeTexture.set(v.TEXTURE2),v.bindTexture(v.TEXTURE_2D,n.depthTexture),t.activeTexture.set(v.TEXTURE3),v.bindTexture(v.TEXTURE_2D,n.texture);for(const t in this.terrainUniforms)this.terrainUniforms[t].set(n[t]);}for(const t in this.fixedUniforms)this.fixedUniforms[t].set(r[t]);p&&p.setUniforms(t,this.binderUniforms,d,{zoom:_});let x=0;switch(e){case v.LINES:x=2;break;case v.TRIANGLES:x=3;break;case v.LINE_STRIP:x=1;}for(const i of u.get()){const a=i.vaos||(i.vaos={});(a[l]||(a[l]=new fe)).bind(t,this,h,p?p.getPaintVertexBuffers():[],c,i.vertexOffset,m,f,g),v.drawElements(e,i.primitiveLength*x,v.UNSIGNED_SHORT,i.primitiveOffset*x*2);}}}function ye(t,e,i){const a=1/Bt(i,1,e.transform.tileZoom),s=Math.pow(2,i.tileID.overscaledZ),o=i.tileSize*Math.pow(2,e.transform.tileZoom)/s,r=o*(i.tileID.canonical.x+i.tileID.wrap*s),n=o*i.tileID.canonical.y;return {u_image:0,u_texsize:i.imageAtlasTexture.size,u_scale:[a,t.fromScale,t.toScale],u_fade:t.t,u_pixel_coord_upper:[r>>16,n>>16],u_pixel_coord_lower:[65535&r,65535&n]}}const be=(t,i,a,s)=>{const o=i.style.light,r=o.properties.get("position"),n=[r.x,r.y,r.z],l=function(){var t=new e.A(9);return e.A!=Float32Array&&(t[1]=0,t[2]=0,t[3]=0,t[5]=0,t[6]=0,t[7]=0),t[0]=1,t[4]=1,t[8]=1,t}();"viewport"===o.properties.get("anchor")&&function(t,e){var i=Math.sin(e),a=Math.cos(e);t[0]=a,t[1]=i,t[2]=0,t[3]=-i,t[4]=a,t[5]=0,t[6]=0,t[7]=0,t[8]=1;}(l,-i.transform.angle),function(t,e,i){var a=e[0],s=e[1],o=e[2];t[0]=a*i[0]+s*i[3]+o*i[6],t[1]=a*i[1]+s*i[4]+o*i[7],t[2]=a*i[2]+s*i[5]+o*i[8];}(n,n,l);const h=o.properties.get("color");return {u_matrix:t,u_lightpos:n,u_lightintensity:o.properties.get("intensity"),u_lightcolor:[h.r,h.g,h.b],u_vertical_gradient:+a,u_opacity:s}},we=(t,i,a,s,o,r,n)=>e.e(be(t,i,a,s),ye(r,i,n),{u_height_factor:-Math.pow(2,o.overscaledZ)/n.tileSize/8}),Te=t=>({u_matrix:t}),Ie=(t,i,a,s)=>e.e(Te(t),ye(a,i,s)),Ee=(t,e)=>({u_matrix:t,u_world:e}),Pe=(t,i,a,s,o)=>e.e(Ie(t,i,a,s),{u_world:o}),Ce=(t,e,i,a)=>{const s=t.transform;let o,r;if("map"===a.paint.get("circle-pitch-alignment")){const t=Bt(i,1,s.zoom);o=!0,r=[t,t];}else o=!1,r=s.pixelsToGLUnits;return {u_camera_to_center_distance:s.cameraToCenterDistance,u_scale_with_map:+("map"===a.paint.get("circle-pitch-scale")),u_matrix:t.translatePosMatrix(e.posMatrix,i,a.paint.get("circle-translate"),a.paint.get("circle-translate-anchor")),u_pitch_with_map:+o,u_device_pixel_ratio:t.pixelRatio,u_extrude_scale:r}},Se=(t,e,i)=>({u_matrix:t,u_inv_matrix:e,u_camera_to_center_distance:i.cameraToCenterDistance,u_viewport_size:[i.width,i.height]}),ze=(t,e,i=1)=>({u_matrix:t,u_color:e,u_overlay:0,u_overlay_scale:i}),De=t=>({u_matrix:t}),Me=(t,e,i,a)=>({u_matrix:t,u_extrude_scale:Bt(e,1,i),u_intensity:a}),Ae=(t,i,a,s)=>{const o=e.H();e.aP(o,0,t.width,t.height,0,0,1);const r=t.context.gl;return {u_matrix:o,u_world:[r.drawingBufferWidth,r.drawingBufferHeight],u_image:a,u_color_ramp:s,u_opacity:i.paint.get("heatmap-opacity")}};function Re(t,i){const a=Math.pow(2,i.canonical.z),s=i.canonical.y;return [new e.Z(0,s/a).toLngLat().lat,new e.Z(0,(s+1)/a).toLngLat().lat]}const ke=(t,e,i,a)=>{const s=t.transform;return {u_matrix:Ne(t,e,i,a),u_ratio:1/Bt(e,1,s.zoom),u_device_pixel_ratio:t.pixelRatio,u_units_to_pixels:[1/s.pixelsToGLUnits[0],1/s.pixelsToGLUnits[1]]}},Le=(t,i,a,s,o)=>e.e(ke(t,i,a,o),{u_image:0,u_image_height:s}),Fe=(t,e,i,a,s)=>{const o=t.transform,r=Oe(e,o);return {u_matrix:Ne(t,e,i,s),u_texsize:e.imageAtlasTexture.size,u_ratio:1/Bt(e,1,o.zoom),u_device_pixel_ratio:t.pixelRatio,u_image:0,u_scale:[r,a.fromScale,a.toScale],u_fade:a.t,u_units_to_pixels:[1/o.pixelsToGLUnits[0],1/o.pixelsToGLUnits[1]]}},Be=(t,i,a,s,o,r)=>{const n=t.lineAtlas,l=Oe(i,t.transform),h="round"===a.layout.get("line-cap"),c=n.getDash(s.from,h),u=n.getDash(s.to,h),d=c.width*o.fromScale,_=u.width*o.toScale;return e.e(ke(t,i,a,r),{u_patternscale_a:[l/d,-c.height/2],u_patternscale_b:[l/_,-u.height/2],u_sdfgamma:n.width/(256*Math.min(d,_)*t.pixelRatio)/2,u_image:0,u_tex_y_a:c.y,u_tex_y_b:u.y,u_mix:o.t})};function Oe(t,e){return 1/Bt(t,1,e.tileZoom)}function Ne(t,e,i,a){return t.translatePosMatrix(a?a.posMatrix:e.tileID.posMatrix,e,i.paint.get("line-translate"),i.paint.get("line-translate-anchor"))}const Ue=(t,e,i,a,s)=>{return {u_matrix:t,u_tl_parent:e,u_scale_parent:i,u_buffer_scale:1,u_fade_t:a.mix,u_opacity:a.opacity*s.paint.get("raster-opacity"),u_image0:0,u_image1:1,u_brightness_low:s.paint.get("raster-brightness-min"),u_brightness_high:s.paint.get("raster-brightness-max"),u_saturation_factor:(r=s.paint.get("raster-saturation"),r>0?1-1/(1.001-r):-r),u_contrast_factor:(o=s.paint.get("raster-contrast"),o>0?1/(1-o):1+o),u_spin_weights:je(s.paint.get("raster-hue-rotate"))};var o,r;};function je(t){t*=Math.PI/180;const e=Math.sin(t),i=Math.cos(t);return [(2*i+1)/3,(-Math.sqrt(3)*e-i+1)/3,(Math.sqrt(3)*e-i+1)/3]}const Ze=(t,e,i,a,s,o,r,n,l,h,c,u,d,_)=>{const p=r.transform;return {u_is_size_zoom_constant:+("constant"===t||"source"===t),u_is_size_feature_constant:+("constant"===t||"camera"===t),u_size_t:e?e.uSizeT:0,u_size:e?e.uSize:0,u_camera_to_center_distance:p.cameraToCenterDistance,u_pitch:p.pitch/360*2*Math.PI,u_rotate_symbol:+i,u_aspect_ratio:p.width/p.height,u_fade_change:r.options.fadeDuration?r.symbolFadeChange:1,u_matrix:n,u_label_plane_matrix:l,u_coord_matrix:h,u_is_text:+u,u_pitch_with_map:+a,u_is_along_line:s,u_is_variable_anchor:o,u_texsize:d,u_texture:0,u_translation:c,u_pitched_scale:_}},qe=(t,i,a,s,o,r,n,l,h,c,u,d,_,p,m)=>{const f=n.transform;return e.e(Ze(t,i,a,s,o,r,n,l,h,c,u,d,_,m),{u_gamma_scale:s?Math.cos(f._pitch)*f.cameraToCenterDistance:1,u_device_pixel_ratio:n.pixelRatio,u_is_halo:+p})},Ve=(t,i,a,s,o,r,n,l,h,c,u,d,_,p)=>e.e(qe(t,i,a,s,o,r,n,l,h,c,u,!0,d,!0,p),{u_texsize_icon:_,u_texture_icon:1}),Ge=(t,e,i)=>({u_matrix:t,u_opacity:e,u_color:i}),He=(t,i,a,s,o,r)=>e.e(function(t,e,i,a){const s=i.imageManager.getPattern(t.from.toString()),o=i.imageManager.getPattern(t.to.toString()),{width:r,height:n}=i.imageManager.getPixelSize(),l=Math.pow(2,a.tileID.overscaledZ),h=a.tileSize*Math.pow(2,i.transform.tileZoom)/l,c=h*(a.tileID.canonical.x+a.tileID.wrap*l),u=h*a.tileID.canonical.y;return {u_image:0,u_pattern_tl_a:s.tl,u_pattern_br_a:s.br,u_pattern_tl_b:o.tl,u_pattern_br_b:o.br,u_texsize:[r,n],u_mix:e.t,u_pattern_size_a:s.displaySize,u_pattern_size_b:o.displaySize,u_scale_a:e.fromScale,u_scale_b:e.toScale,u_tile_units_to_pixels:1/Bt(a,1,i.transform.tileZoom),u_pixel_coord_upper:[c>>16,u>>16],u_pixel_coord_lower:[65535&c,65535&u]}}(s,r,a,o),{u_matrix:t,u_opacity:i}),We={fillExtrusion:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_lightpos:new e.aN(t,i.u_lightpos),u_lightintensity:new e.aI(t,i.u_lightintensity),u_lightcolor:new e.aN(t,i.u_lightcolor),u_vertical_gradient:new e.aI(t,i.u_vertical_gradient),u_opacity:new e.aI(t,i.u_opacity)}),fillExtrusionPattern:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_lightpos:new e.aN(t,i.u_lightpos),u_lightintensity:new e.aI(t,i.u_lightintensity),u_lightcolor:new e.aN(t,i.u_lightcolor),u_vertical_gradient:new e.aI(t,i.u_vertical_gradient),u_height_factor:new e.aI(t,i.u_height_factor),u_image:new e.aH(t,i.u_image),u_texsize:new e.aO(t,i.u_texsize),u_pixel_coord_upper:new e.aO(t,i.u_pixel_coord_upper),u_pixel_coord_lower:new e.aO(t,i.u_pixel_coord_lower),u_scale:new e.aN(t,i.u_scale),u_fade:new e.aI(t,i.u_fade),u_opacity:new e.aI(t,i.u_opacity)}),fill:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix)}),fillPattern:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_image:new e.aH(t,i.u_image),u_texsize:new e.aO(t,i.u_texsize),u_pixel_coord_upper:new e.aO(t,i.u_pixel_coord_upper),u_pixel_coord_lower:new e.aO(t,i.u_pixel_coord_lower),u_scale:new e.aN(t,i.u_scale),u_fade:new e.aI(t,i.u_fade)}),fillOutline:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_world:new e.aO(t,i.u_world)}),fillOutlinePattern:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_world:new e.aO(t,i.u_world),u_image:new e.aH(t,i.u_image),u_texsize:new e.aO(t,i.u_texsize),u_pixel_coord_upper:new e.aO(t,i.u_pixel_coord_upper),u_pixel_coord_lower:new e.aO(t,i.u_pixel_coord_lower),u_scale:new e.aN(t,i.u_scale),u_fade:new e.aI(t,i.u_fade)}),circle:(t,i)=>({u_camera_to_center_distance:new e.aI(t,i.u_camera_to_center_distance),u_scale_with_map:new e.aH(t,i.u_scale_with_map),u_pitch_with_map:new e.aH(t,i.u_pitch_with_map),u_extrude_scale:new e.aO(t,i.u_extrude_scale),u_device_pixel_ratio:new e.aI(t,i.u_device_pixel_ratio),u_matrix:new e.aJ(t,i.u_matrix)}),collisionBox:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_pixel_extrude_scale:new e.aO(t,i.u_pixel_extrude_scale)}),collisionCircle:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_inv_matrix:new e.aJ(t,i.u_inv_matrix),u_camera_to_center_distance:new e.aI(t,i.u_camera_to_center_distance),u_viewport_size:new e.aO(t,i.u_viewport_size)}),debug:(t,i)=>({u_color:new e.aL(t,i.u_color),u_matrix:new e.aJ(t,i.u_matrix),u_overlay:new e.aH(t,i.u_overlay),u_overlay_scale:new e.aI(t,i.u_overlay_scale)}),clippingMask:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix)}),heatmap:(t,i)=>({u_extrude_scale:new e.aI(t,i.u_extrude_scale),u_intensity:new e.aI(t,i.u_intensity),u_matrix:new e.aJ(t,i.u_matrix)}),heatmapTexture:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_world:new e.aO(t,i.u_world),u_image:new e.aH(t,i.u_image),u_color_ramp:new e.aH(t,i.u_color_ramp),u_opacity:new e.aI(t,i.u_opacity)}),hillshade:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_image:new e.aH(t,i.u_image),u_latrange:new e.aO(t,i.u_latrange),u_light:new e.aO(t,i.u_light),u_shadow:new e.aL(t,i.u_shadow),u_highlight:new e.aL(t,i.u_highlight),u_accent:new e.aL(t,i.u_accent)}),hillshadePrepare:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_image:new e.aH(t,i.u_image),u_dimension:new e.aO(t,i.u_dimension),u_zoom:new e.aI(t,i.u_zoom),u_unpack:new e.aK(t,i.u_unpack)}),line:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_ratio:new e.aI(t,i.u_ratio),u_device_pixel_ratio:new e.aI(t,i.u_device_pixel_ratio),u_units_to_pixels:new e.aO(t,i.u_units_to_pixels)}),lineGradient:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_ratio:new e.aI(t,i.u_ratio),u_device_pixel_ratio:new e.aI(t,i.u_device_pixel_ratio),u_units_to_pixels:new e.aO(t,i.u_units_to_pixels),u_image:new e.aH(t,i.u_image),u_image_height:new e.aI(t,i.u_image_height)}),linePattern:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_texsize:new e.aO(t,i.u_texsize),u_ratio:new e.aI(t,i.u_ratio),u_device_pixel_ratio:new e.aI(t,i.u_device_pixel_ratio),u_image:new e.aH(t,i.u_image),u_units_to_pixels:new e.aO(t,i.u_units_to_pixels),u_scale:new e.aN(t,i.u_scale),u_fade:new e.aI(t,i.u_fade)}),lineSDF:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_ratio:new e.aI(t,i.u_ratio),u_device_pixel_ratio:new e.aI(t,i.u_device_pixel_ratio),u_units_to_pixels:new e.aO(t,i.u_units_to_pixels),u_patternscale_a:new e.aO(t,i.u_patternscale_a),u_patternscale_b:new e.aO(t,i.u_patternscale_b),u_sdfgamma:new e.aI(t,i.u_sdfgamma),u_image:new e.aH(t,i.u_image),u_tex_y_a:new e.aI(t,i.u_tex_y_a),u_tex_y_b:new e.aI(t,i.u_tex_y_b),u_mix:new e.aI(t,i.u_mix)}),raster:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_tl_parent:new e.aO(t,i.u_tl_parent),u_scale_parent:new e.aI(t,i.u_scale_parent),u_buffer_scale:new e.aI(t,i.u_buffer_scale),u_fade_t:new e.aI(t,i.u_fade_t),u_opacity:new e.aI(t,i.u_opacity),u_image0:new e.aH(t,i.u_image0),u_image1:new e.aH(t,i.u_image1),u_brightness_low:new e.aI(t,i.u_brightness_low),u_brightness_high:new e.aI(t,i.u_brightness_high),u_saturation_factor:new e.aI(t,i.u_saturation_factor),u_contrast_factor:new e.aI(t,i.u_contrast_factor),u_spin_weights:new e.aN(t,i.u_spin_weights)}),symbolIcon:(t,i)=>({u_is_size_zoom_constant:new e.aH(t,i.u_is_size_zoom_constant),u_is_size_feature_constant:new e.aH(t,i.u_is_size_feature_constant),u_size_t:new e.aI(t,i.u_size_t),u_size:new e.aI(t,i.u_size),u_camera_to_center_distance:new e.aI(t,i.u_camera_to_center_distance),u_pitch:new e.aI(t,i.u_pitch),u_rotate_symbol:new e.aH(t,i.u_rotate_symbol),u_aspect_ratio:new e.aI(t,i.u_aspect_ratio),u_fade_change:new e.aI(t,i.u_fade_change),u_matrix:new e.aJ(t,i.u_matrix),u_label_plane_matrix:new e.aJ(t,i.u_label_plane_matrix),u_coord_matrix:new e.aJ(t,i.u_coord_matrix),u_is_text:new e.aH(t,i.u_is_text),u_pitch_with_map:new e.aH(t,i.u_pitch_with_map),u_is_along_line:new e.aH(t,i.u_is_along_line),u_is_variable_anchor:new e.aH(t,i.u_is_variable_anchor),u_texsize:new e.aO(t,i.u_texsize),u_texture:new e.aH(t,i.u_texture),u_translation:new e.aO(t,i.u_translation),u_pitched_scale:new e.aI(t,i.u_pitched_scale)}),symbolSDF:(t,i)=>({u_is_size_zoom_constant:new e.aH(t,i.u_is_size_zoom_constant),u_is_size_feature_constant:new e.aH(t,i.u_is_size_feature_constant),u_size_t:new e.aI(t,i.u_size_t),u_size:new e.aI(t,i.u_size),u_camera_to_center_distance:new e.aI(t,i.u_camera_to_center_distance),u_pitch:new e.aI(t,i.u_pitch),u_rotate_symbol:new e.aH(t,i.u_rotate_symbol),u_aspect_ratio:new e.aI(t,i.u_aspect_ratio),u_fade_change:new e.aI(t,i.u_fade_change),u_matrix:new e.aJ(t,i.u_matrix),u_label_plane_matrix:new e.aJ(t,i.u_label_plane_matrix),u_coord_matrix:new e.aJ(t,i.u_coord_matrix),u_is_text:new e.aH(t,i.u_is_text),u_pitch_with_map:new e.aH(t,i.u_pitch_with_map),u_is_along_line:new e.aH(t,i.u_is_along_line),u_is_variable_anchor:new e.aH(t,i.u_is_variable_anchor),u_texsize:new e.aO(t,i.u_texsize),u_texture:new e.aH(t,i.u_texture),u_gamma_scale:new e.aI(t,i.u_gamma_scale),u_device_pixel_ratio:new e.aI(t,i.u_device_pixel_ratio),u_is_halo:new e.aH(t,i.u_is_halo),u_translation:new e.aO(t,i.u_translation),u_pitched_scale:new e.aI(t,i.u_pitched_scale)}),symbolTextAndIcon:(t,i)=>({u_is_size_zoom_constant:new e.aH(t,i.u_is_size_zoom_constant),u_is_size_feature_constant:new e.aH(t,i.u_is_size_feature_constant),u_size_t:new e.aI(t,i.u_size_t),u_size:new e.aI(t,i.u_size),u_camera_to_center_distance:new e.aI(t,i.u_camera_to_center_distance),u_pitch:new e.aI(t,i.u_pitch),u_rotate_symbol:new e.aH(t,i.u_rotate_symbol),u_aspect_ratio:new e.aI(t,i.u_aspect_ratio),u_fade_change:new e.aI(t,i.u_fade_change),u_matrix:new e.aJ(t,i.u_matrix),u_label_plane_matrix:new e.aJ(t,i.u_label_plane_matrix),u_coord_matrix:new e.aJ(t,i.u_coord_matrix),u_is_text:new e.aH(t,i.u_is_text),u_pitch_with_map:new e.aH(t,i.u_pitch_with_map),u_is_along_line:new e.aH(t,i.u_is_along_line),u_is_variable_anchor:new e.aH(t,i.u_is_variable_anchor),u_texsize:new e.aO(t,i.u_texsize),u_texsize_icon:new e.aO(t,i.u_texsize_icon),u_texture:new e.aH(t,i.u_texture),u_texture_icon:new e.aH(t,i.u_texture_icon),u_gamma_scale:new e.aI(t,i.u_gamma_scale),u_device_pixel_ratio:new e.aI(t,i.u_device_pixel_ratio),u_is_halo:new e.aH(t,i.u_is_halo),u_translation:new e.aO(t,i.u_translation),u_pitched_scale:new e.aI(t,i.u_pitched_scale)}),background:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_opacity:new e.aI(t,i.u_opacity),u_color:new e.aL(t,i.u_color)}),backgroundPattern:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_opacity:new e.aI(t,i.u_opacity),u_image:new e.aH(t,i.u_image),u_pattern_tl_a:new e.aO(t,i.u_pattern_tl_a),u_pattern_br_a:new e.aO(t,i.u_pattern_br_a),u_pattern_tl_b:new e.aO(t,i.u_pattern_tl_b),u_pattern_br_b:new e.aO(t,i.u_pattern_br_b),u_texsize:new e.aO(t,i.u_texsize),u_mix:new e.aI(t,i.u_mix),u_pattern_size_a:new e.aO(t,i.u_pattern_size_a),u_pattern_size_b:new e.aO(t,i.u_pattern_size_b),u_scale_a:new e.aI(t,i.u_scale_a),u_scale_b:new e.aI(t,i.u_scale_b),u_pixel_coord_upper:new e.aO(t,i.u_pixel_coord_upper),u_pixel_coord_lower:new e.aO(t,i.u_pixel_coord_lower),u_tile_units_to_pixels:new e.aI(t,i.u_tile_units_to_pixels)}),terrain:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_texture:new e.aH(t,i.u_texture),u_ele_delta:new e.aI(t,i.u_ele_delta),u_fog_matrix:new e.aJ(t,i.u_fog_matrix),u_fog_color:new e.aL(t,i.u_fog_color),u_fog_ground_blend:new e.aI(t,i.u_fog_ground_blend),u_fog_ground_blend_opacity:new e.aI(t,i.u_fog_ground_blend_opacity),u_horizon_color:new e.aL(t,i.u_horizon_color),u_horizon_fog_blend:new e.aI(t,i.u_horizon_fog_blend)}),terrainDepth:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_ele_delta:new e.aI(t,i.u_ele_delta)}),terrainCoords:(t,i)=>({u_matrix:new e.aJ(t,i.u_matrix),u_texture:new e.aH(t,i.u_texture),u_terrain_coords_id:new e.aI(t,i.u_terrain_coords_id),u_ele_delta:new e.aI(t,i.u_ele_delta)}),sky:(t,i)=>({u_sky_color:new e.aL(t,i.u_sky_color),u_horizon_color:new e.aL(t,i.u_horizon_color),u_horizon:new e.aI(t,i.u_horizon),u_sky_horizon_blend:new e.aI(t,i.u_sky_horizon_blend)})};class $e{constructor(t,e,i){this.context=t;const a=t.gl;this.buffer=a.createBuffer(),this.dynamicDraw=Boolean(i),this.context.unbindVAO(),t.bindElementBuffer.set(this.buffer),a.bufferData(a.ELEMENT_ARRAY_BUFFER,e.arrayBuffer,this.dynamicDraw?a.DYNAMIC_DRAW:a.STATIC_DRAW),this.dynamicDraw||delete e.arrayBuffer;}bind(){this.context.bindElementBuffer.set(this.buffer);}updateData(t){const e=this.context.gl;if(!this.dynamicDraw)throw new Error("Attempted to update data while not in dynamic mode.");this.context.unbindVAO(),this.bind(),e.bufferSubData(e.ELEMENT_ARRAY_BUFFER,0,t.arrayBuffer);}destroy(){this.buffer&&(this.context.gl.deleteBuffer(this.buffer),delete this.buffer);}}const Xe={Int8:"BYTE",Uint8:"UNSIGNED_BYTE",Int16:"SHORT",Uint16:"UNSIGNED_SHORT",Int32:"INT",Uint32:"UNSIGNED_INT",Float32:"FLOAT"};class Ke{constructor(t,e,i,a){this.length=e.length,this.attributes=i,this.itemSize=e.bytesPerElement,this.dynamicDraw=a,this.context=t;const s=t.gl;this.buffer=s.createBuffer(),t.bindVertexBuffer.set(this.buffer),s.bufferData(s.ARRAY_BUFFER,e.arrayBuffer,this.dynamicDraw?s.DYNAMIC_DRAW:s.STATIC_DRAW),this.dynamicDraw||delete e.arrayBuffer;}bind(){this.context.bindVertexBuffer.set(this.buffer);}updateData(t){if(t.length!==this.length)throw new Error(`Length of new data is ${t.length}, which doesn't match current length of ${this.length}`);const e=this.context.gl;this.bind(),e.bufferSubData(e.ARRAY_BUFFER,0,t.arrayBuffer);}enableAttributes(t,e){for(let i=0;i0){const i=e.H();e.aQ(i,m.placementInvProjMatrix,t.transform.glCoordMatrix),e.aQ(i,i,m.placementViewportMatrix),h.push({circleArray:g,circleOffset:u,transform:p.posMatrix,invTransform:i,coord:p}),c+=g.length/4,u=c;}f&&l.draw(r,n.LINES,Oi.disabled,Ui.disabled,t.colorModeForRenderPass(),ji.disabled,{u_matrix:p.posMatrix,u_pixel_extrude_scale:[1/(d=t.transform).width,1/d.height]},t.style.map.terrain&&t.style.map.terrain.getTerrainData(p),a.id,f.layoutVertexBuffer,f.indexBuffer,f.segments,null,t.transform.zoom,null,null,f.collisionVertexBuffer);}var d;if(!o||!h.length)return;const _=t.useProgram("collisionCircle"),p=new e.aR;p.resize(4*c),p._trim();let m=0;for(const t of h)for(let e=0;e=0&&(v[x.associatedIconIndex]={shiftedAnchor:D,angle:M});}else Rt(x.numGlyphs,f);}if(c){g.clear();const i=t.icon.placedSymbolArray;for(let t=0;tt.style.map.terrain.getElevation(l,e,i):null,i="map"===a.layout.get("text-rotation-alignment");wt(h,l.posMatrix,t,o,U,Z,v,c,i,f,l.toUnwrapped(),m.width,m.height,q,e);}const H=l.posMatrix,W=o&&E||G,$=x||W?Vi:U,X=j,K=_&&0!==a.paint.get(o?"text-halo-width":"icon-halo-width").constantOr(1);let J;J=_?h.iconsInText?Ve(I.kind,D,y,v,x,W,t,H,$,X,q,A,F,C):qe(I.kind,D,y,v,x,W,t,H,$,X,q,o,A,!0,C):Ze(I.kind,D,y,v,x,W,t,H,$,X,q,o,A,C);const Y={program:z,buffers:u,uniformValues:J,atlasTexture:R,atlasTextureIcon:B,atlasInterpolation:k,atlasInterpolationIcon:L,isSDF:_,hasHalo:K};if(w&&h.canOverlap){T=!0;const t=u.segments.get();for(const i of t)P.push({segments:new e.a0([i]),sortKey:i.sortKey,state:Y,terrainData:M});}else P.push({segments:u.segments,sortKey:0,state:Y,terrainData:M});}T&&P.sort(((t,e)=>t.sortKey-e.sortKey));for(const e of P){const i=e.state;if(_.activeTexture.set(p.TEXTURE0),i.atlasTexture.bind(i.atlasInterpolation,p.CLAMP_TO_EDGE),i.atlasTextureIcon&&(_.activeTexture.set(p.TEXTURE1),i.atlasTextureIcon&&i.atlasTextureIcon.bind(i.atlasInterpolationIcon,p.CLAMP_TO_EDGE)),i.isSDF){const s=i.uniformValues;i.hasHalo&&(s.u_is_halo=1,Ki(i.buffers,e.segments,a,t,i.program,I,u,d,s,e.terrainData)),s.u_is_halo=0;}Ki(i.buffers,e.segments,a,t,i.program,I,u,d,i.uniformValues,e.terrainData);}}function Ki(t,e,i,a,s,o,r,n,l,h){const c=a.context;s.draw(c,c.gl.TRIANGLES,o,r,n,ji.disabled,l,h,i.id,t.layoutVertexBuffer,t.indexBuffer,e,i.paint,a.transform.zoom,t.programConfigurations.get(i.id),t.dynamicLayoutVertexBuffer,t.opacityVertexBuffer);}function Ji(t,i,a,s){const o=t.context,r=o.gl,n=Ui.disabled,l=new Fi([r.ONE,r.ONE],e.aM.transparent,[!0,!0,!0,!0]),h=i.getBucket(a);if(!h)return;const c=s.key;let u=a.heatmapFbos.get(c);u||(u=Qi(o,i.tileSize,i.tileSize),a.heatmapFbos.set(c,u)),o.bindFramebuffer.set(u.framebuffer),o.viewport.set([0,0,i.tileSize,i.tileSize]),o.clear({color:e.aM.transparent});const d=h.programConfigurations.get(a.id),_=t.useProgram("heatmap",d),p=t.style.map.terrain.getTerrainData(s);_.draw(o,r.TRIANGLES,Oi.disabled,n,l,ji.disabled,Me(s.posMatrix,i,t.transform.zoom,a.paint.get("heatmap-intensity")),p,a.id,h.layoutVertexBuffer,h.indexBuffer,h.segments,a.paint,t.transform.zoom,d);}function Yi(t,e,i){const a=t.context,s=a.gl;a.setColorMode(t.colorModeForRenderPass());const o=ta(a,e),r=i.key,n=e.heatmapFbos.get(r);n&&(a.activeTexture.set(s.TEXTURE0),s.bindTexture(s.TEXTURE_2D,n.colorAttachment.get()),a.activeTexture.set(s.TEXTURE1),o.bind(s.LINEAR,s.CLAMP_TO_EDGE),t.useProgram("heatmapTexture").draw(a,s.TRIANGLES,Oi.disabled,Ui.disabled,t.colorModeForRenderPass(),ji.disabled,Ae(t,e,0,1),null,e.id,t.rasterBoundsBuffer,t.quadTriangleIndexBuffer,t.rasterBoundsSegments,e.paint,t.transform.zoom),n.destroy(),e.heatmapFbos.delete(r));}function Qi(t,e,i){var a,s;const o=t.gl,r=o.createTexture();o.bindTexture(o.TEXTURE_2D,r),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_WRAP_S,o.CLAMP_TO_EDGE),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_WRAP_T,o.CLAMP_TO_EDGE),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_MIN_FILTER,o.LINEAR),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_MAG_FILTER,o.LINEAR);const n=null!==(a=t.HALF_FLOAT)&&void 0!==a?a:o.UNSIGNED_BYTE,l=null!==(s=t.RGBA16F)&&void 0!==s?s:o.RGBA;o.texImage2D(o.TEXTURE_2D,0,l,e,i,0,o.RGBA,n,null);const h=t.createFramebuffer(e,i,!1,!1);return h.colorAttachment.set(r),h}function ta(t,e){return e.colorRampTexture||(e.colorRampTexture=new b(t,e.colorRamp,t.gl.RGBA)),e.colorRampTexture}function ea(t,e,i,a,s){if(!i||!a||!a.imageAtlas)return;const o=a.imageAtlas.patternPositions;let r=o[i.to.toString()],n=o[i.from.toString()];if(!r&&n&&(r=n),!n&&r&&(n=r),!r||!n){const t=s.getPaintProperty(e);r=o[t],n=o[t];}r&&n&&t.setConstantPatternPositions(r,n);}function ia(t,e,i,a,s,o,r){const n=t.context.gl,l="fill-pattern",h=i.paint.get(l),c=h&&h.constantOr(1),u=i.getCrossfadeParameters();let d,_,p,m,f;r?(_=c&&!i.getPaintProperty("fill-outline-color")?"fillOutlinePattern":"fillOutline",d=n.LINES):(_=c?"fillPattern":"fill",d=n.TRIANGLES);const g=h.constantOr(null);for(const h of a){const a=e.getTile(h);if(c&&!a.patternsLoaded())continue;const v=a.getBucket(i);if(!v)continue;const x=v.programConfigurations.get(i.id),y=t.useProgram(_,x),b=t.style.map.terrain&&t.style.map.terrain.getTerrainData(h);c&&(t.context.activeTexture.set(n.TEXTURE0),a.imageAtlasTexture.bind(n.LINEAR,n.CLAMP_TO_EDGE),x.updatePaintBuffers(u)),ea(x,l,g,a,i);const w=b?h:null,T=t.translatePosMatrix(w?w.posMatrix:h.posMatrix,a,i.paint.get("fill-translate"),i.paint.get("fill-translate-anchor"));if(r){m=v.indexBuffer2,f=v.segments2;const e=[n.drawingBufferWidth,n.drawingBufferHeight];p="fillOutlinePattern"===_&&c?Pe(T,t,u,a,e):Ee(T,e);}else m=v.indexBuffer,f=v.segments,p=c?Ie(T,t,u,a):Te(T);y.draw(t.context,d,s,t.stencilModeForClipping(h),o,ji.disabled,p,b,i.id,v.layoutVertexBuffer,m,f,i.paint,t.transform.zoom,x);}}function aa(t,e,i,a,s,o,r){const n=t.context,l=n.gl,h="fill-extrusion-pattern",c=i.paint.get(h),u=c.constantOr(1),d=i.getCrossfadeParameters(),_=i.paint.get("fill-extrusion-opacity"),p=c.constantOr(null);for(const c of a){const a=e.getTile(c),m=a.getBucket(i);if(!m)continue;const f=t.style.map.terrain&&t.style.map.terrain.getTerrainData(c),g=m.programConfigurations.get(i.id),v=t.useProgram(u?"fillExtrusionPattern":"fillExtrusion",g);u&&(t.context.activeTexture.set(l.TEXTURE0),a.imageAtlasTexture.bind(l.LINEAR,l.CLAMP_TO_EDGE),g.updatePaintBuffers(d)),ea(g,h,p,a,i);const x=t.translatePosMatrix(c.posMatrix,a,i.paint.get("fill-extrusion-translate"),i.paint.get("fill-extrusion-translate-anchor")),y=i.paint.get("fill-extrusion-vertical-gradient"),b=u?we(x,t,y,_,c,d,a):be(x,t,y,_);v.draw(n,n.gl.TRIANGLES,s,o,r,ji.backCCW,b,f,i.id,m.layoutVertexBuffer,m.indexBuffer,m.segments,i.paint,t.transform.zoom,g,t.style.map.terrain&&m.centroidVertexBuffer);}}function sa(t,e,i,a,s,o,r){const n=t.context,l=n.gl,h=i.fbo;if(!h)return;const c=t.useProgram("hillshade"),u=t.style.map.terrain&&t.style.map.terrain.getTerrainData(e);n.activeTexture.set(l.TEXTURE0),l.bindTexture(l.TEXTURE_2D,h.colorAttachment.get()),c.draw(n,l.TRIANGLES,s,o,r,ji.disabled,((t,e,i,a)=>{const s=i.paint.get("hillshade-shadow-color"),o=i.paint.get("hillshade-highlight-color"),r=i.paint.get("hillshade-accent-color");let n=i.paint.get("hillshade-illumination-direction")*(Math.PI/180);"viewport"===i.paint.get("hillshade-illumination-anchor")&&(n-=t.transform.angle);const l=!t.options.moving;return {u_matrix:a?a.posMatrix:t.transform.calculatePosMatrix(e.tileID.toUnwrapped(),l),u_image:0,u_latrange:Re(0,e.tileID),u_light:[i.paint.get("hillshade-exaggeration"),n],u_shadow:s,u_highlight:o,u_accent:r}})(t,i,a,u?e:null),u,a.id,t.rasterBoundsBuffer,t.quadTriangleIndexBuffer,t.rasterBoundsSegments);}function oa(t,i,a,s,o,r){const n=t.context,l=n.gl,h=i.dem;if(h&&h.data){const c=h.dim,u=h.stride,d=h.getPixels();if(n.activeTexture.set(l.TEXTURE1),n.pixelStoreUnpackPremultiplyAlpha.set(!1),i.demTexture=i.demTexture||t.getTileTexture(u),i.demTexture){const t=i.demTexture;t.update(d,{premultiply:!1}),t.bind(l.NEAREST,l.CLAMP_TO_EDGE);}else i.demTexture=new b(n,d,l.RGBA,{premultiply:!1}),i.demTexture.bind(l.NEAREST,l.CLAMP_TO_EDGE);n.activeTexture.set(l.TEXTURE0);let _=i.fbo;if(!_){const t=new b(n,{width:c,height:c,data:null},l.RGBA);t.bind(l.LINEAR,l.CLAMP_TO_EDGE),_=i.fbo=n.createFramebuffer(c,c,!0,!1),_.colorAttachment.set(t.texture);}n.bindFramebuffer.set(_.framebuffer),n.viewport.set([0,0,c,c]),t.useProgram("hillshadePrepare").draw(n,l.TRIANGLES,s,o,r,ji.disabled,((t,i)=>{const a=i.stride,s=e.H();return e.aP(s,0,e.X,-e.X,0,0,1),e.J(s,s,[0,-e.X,0]),{u_matrix:s,u_image:1,u_dimension:[a,a],u_zoom:t.overscaledZ,u_unpack:i.getUnpackVector()}})(i.tileID,h),null,a.id,t.rasterBoundsBuffer,t.quadTriangleIndexBuffer,t.rasterBoundsSegments),i.needsHillshadePrepare=!1;}}function ra(t,i,a,s,r,n){const l=s.paint.get("raster-fade-duration");if(!n&&l>0){const s=o.now(),n=(s-t.timeAdded)/l,h=i?(s-i.timeAdded)/l:-1,c=a.getSource(),u=r.coveringZoomLevel({tileSize:c.tileSize,roundZoom:c.roundZoom}),d=!i||Math.abs(i.tileID.overscaledZ-u)>Math.abs(t.tileID.overscaledZ-u),_=d&&t.refreshedUponExpiration?1:e.ac(d?n:1-h,0,1);return t.refreshedUponExpiration&&n>=1&&(t.refreshedUponExpiration=!1),i?{opacity:1,mix:1-_}:{opacity:_,mix:0}}return {opacity:1,mix:0}}const na=new e.aM(1,0,0,1),la=new e.aM(0,1,0,1),ha=new e.aM(0,0,1,1),ca=new e.aM(1,0,1,1),ua=new e.aM(0,1,1,1);function da(t,e,i,a){pa(t,0,e+i/2,t.transform.width,i,a);}function _a(t,e,i,a){pa(t,e-i/2,0,i,t.transform.height,a);}function pa(t,e,i,a,s,o){const r=t.context,n=r.gl;n.enable(n.SCISSOR_TEST),n.scissor(e*t.pixelRatio,i*t.pixelRatio,a*t.pixelRatio,s*t.pixelRatio),r.clear({color:o}),n.disable(n.SCISSOR_TEST);}function ma(t,i,a){const s=t.context,o=s.gl,r=a.posMatrix,n=t.useProgram("debug"),l=Oi.disabled,h=Ui.disabled,c=t.colorModeForRenderPass(),u="$debug",d=t.style.map.terrain&&t.style.map.terrain.getTerrainData(a);s.activeTexture.set(o.TEXTURE0);const _=i.getTileByID(a.key).latestRawTileData,p=Math.floor((_&&_.byteLength||0)/1024),m=i.getTile(a).tileSize,f=512/Math.min(m,512)*(a.overscaledZ/t.transform.zoom)*.5;let g=a.canonical.toString();a.overscaledZ!==a.canonical.z&&(g+=` => ${a.overscaledZ}`),function(t,e){t.initDebugOverlayCanvas();const i=t.debugOverlayCanvas,a=t.context.gl,s=t.debugOverlayCanvas.getContext("2d");s.clearRect(0,0,i.width,i.height),s.shadowColor="white",s.shadowBlur=2,s.lineWidth=1.5,s.strokeStyle="white",s.textBaseline="top",s.font="bold 36px Open Sans, sans-serif",s.fillText(e,5,5),s.strokeText(e,5,5),t.debugOverlayTexture.update(i),t.debugOverlayTexture.bind(a.LINEAR,a.CLAMP_TO_EDGE);}(t,`${g} ${p}kB`),n.draw(s,o.TRIANGLES,l,h,Fi.alphaBlended,ji.disabled,ze(r,e.aM.transparent,f),null,u,t.debugBuffer,t.quadTriangleIndexBuffer,t.debugSegments),n.draw(s,o.LINE_STRIP,l,h,c,ji.disabled,ze(r,e.aM.red),d,u,t.debugBuffer,t.tileBorderIndexBuffer,t.debugSegments);}function fa(t,e,i){const a=t.context,s=a.gl,o=t.colorModeForRenderPass(),r=new Oi(s.LEQUAL,Oi.ReadWrite,t.depthRangeFor3D),n=t.useProgram("terrain"),l=e.getTerrainMesh();a.bindFramebuffer.set(null),a.viewport.set([0,0,t.width,t.height]);for(const h of i){const i=t.renderToTexture.getTexture(h),c=e.getTerrainData(h.tileID);a.activeTexture.set(s.TEXTURE0),s.bindTexture(s.TEXTURE_2D,i.texture);const u=t.transform.calculatePosMatrix(h.tileID.toUnwrapped()),d=e.getMeshFrameDelta(t.transform.zoom),_=t.transform.calculateFogMatrix(h.tileID.toUnwrapped()),p=ge(u,d,_,t.style.sky,t.transform.pitch);n.draw(a,s.TRIANGLES,r,Ui.disabled,o,ji.backCCW,p,c,"terrain",l.vertexBuffer,l.indexBuffer,l.segments);}}class ga{constructor(t,e,i){this.vertexBuffer=t,this.indexBuffer=e,this.segments=i;}destroy(){this.vertexBuffer.destroy(),this.indexBuffer.destroy(),this.segments.destroy(),this.vertexBuffer=null,this.indexBuffer=null,this.segments=null;}}class va{constructor(t,i){this.context=new Bi(t),this.transform=i,this._tileTextures={},this.terrainFacilitator={dirty:!0,matrix:e.an(new Float64Array(16)),renderTime:0},this.setup(),this.numSublayers=ut.maxUnderzooming+ut.maxOverzooming+1,this.depthEpsilon=1/Math.pow(2,16),this.crossTileSymbolIndex=new he;}resize(t,e,i){if(this.width=Math.floor(t*i),this.height=Math.floor(e*i),this.pixelRatio=i,this.context.viewport.set([0,0,this.width,this.height]),this.style)for(const t of this.style._order)this.style._layers[t].resize();}setup(){const t=this.context,i=new e.aX;i.emplaceBack(0,0),i.emplaceBack(e.X,0),i.emplaceBack(0,e.X),i.emplaceBack(e.X,e.X),this.tileExtentBuffer=t.createVertexBuffer(i,_e.members),this.tileExtentSegments=e.a0.simpleSegment(0,0,4,2);const a=new e.aX;a.emplaceBack(0,0),a.emplaceBack(e.X,0),a.emplaceBack(0,e.X),a.emplaceBack(e.X,e.X),this.debugBuffer=t.createVertexBuffer(a,_e.members),this.debugSegments=e.a0.simpleSegment(0,0,4,5);const s=new e.$;s.emplaceBack(0,0,0,0),s.emplaceBack(e.X,0,e.X,0),s.emplaceBack(0,e.X,0,e.X),s.emplaceBack(e.X,e.X,e.X,e.X),this.rasterBoundsBuffer=t.createVertexBuffer(s,Y.members),this.rasterBoundsSegments=e.a0.simpleSegment(0,0,4,2);const o=new e.aX;o.emplaceBack(0,0),o.emplaceBack(1,0),o.emplaceBack(0,1),o.emplaceBack(1,1),this.viewportBuffer=t.createVertexBuffer(o,_e.members),this.viewportSegments=e.a0.simpleSegment(0,0,4,2);const r=new e.aZ;r.emplaceBack(0),r.emplaceBack(1),r.emplaceBack(3),r.emplaceBack(2),r.emplaceBack(0),this.tileBorderIndexBuffer=t.createIndexBuffer(r);const n=new e.aY;n.emplaceBack(0,1,2),n.emplaceBack(2,1,3),this.quadTriangleIndexBuffer=t.createIndexBuffer(n);const l=this.context.gl;this.stencilClearMode=new Ui({func:l.ALWAYS,mask:0},0,255,l.ZERO,l.ZERO,l.ZERO);}clearStencil(){const t=this.context,i=t.gl;this.nextStencilID=1,this.currentStencilSource=void 0;const a=e.H();e.aP(a,0,this.width,this.height,0,0,1),e.K(a,a,[i.drawingBufferWidth,i.drawingBufferHeight,0]),this.useProgram("clippingMask").draw(t,i.TRIANGLES,Oi.disabled,this.stencilClearMode,Fi.disabled,ji.disabled,De(a),null,"$clipping",this.viewportBuffer,this.quadTriangleIndexBuffer,this.viewportSegments);}_renderTileClippingMasks(t,e){if(this.currentStencilSource===t.source||!t.isTileClipped()||!e||!e.length)return;this.currentStencilSource=t.source;const i=this.context,a=i.gl;this.nextStencilID+e.length>256&&this.clearStencil(),i.setColorMode(Fi.disabled),i.setDepthMode(Oi.disabled);const s=this.useProgram("clippingMask");this._tileClippingMaskIDs={};for(const t of e){const e=this._tileClippingMaskIDs[t.key]=this.nextStencilID++,o=this.style.map.terrain&&this.style.map.terrain.getTerrainData(t);s.draw(i,a.TRIANGLES,Oi.disabled,new Ui({func:a.ALWAYS,mask:0},e,255,a.KEEP,a.KEEP,a.REPLACE),Fi.disabled,ji.disabled,De(t.posMatrix),o,"$clipping",this.tileExtentBuffer,this.quadTriangleIndexBuffer,this.tileExtentSegments);}}stencilModeFor3D(){this.currentStencilSource=void 0,this.nextStencilID+1>256&&this.clearStencil();const t=this.nextStencilID++,e=this.context.gl;return new Ui({func:e.NOTEQUAL,mask:255},t,255,e.KEEP,e.KEEP,e.REPLACE)}stencilModeForClipping(t){const e=this.context.gl;return new Ui({func:e.EQUAL,mask:255},this._tileClippingMaskIDs[t.key],0,e.KEEP,e.KEEP,e.REPLACE)}stencilConfigForOverlap(t){const e=this.context.gl,i=t.sort(((t,e)=>e.overscaledZ-t.overscaledZ)),a=i[i.length-1].overscaledZ,s=i[0].overscaledZ-a+1;if(s>1){this.currentStencilSource=void 0,this.nextStencilID+s>256&&this.clearStencil();const t={};for(let i=0;i({u_sky_color:t.properties.get("sky-color"),u_horizon_color:t.properties.get("horizon-color"),u_horizon:(e.height/2+e.getHorizon())*i,u_sky_horizon_blend:t.properties.get("sky-horizon-blend")*e.height/2*i}))(i,t.style.map.transform,t.pixelRatio),r=new Oi(s.LEQUAL,Oi.ReadWrite,[0,1]),n=Ui.disabled,l=t.colorModeForRenderPass(),h=t.useProgram("sky");if(!i.mesh){const t=new e.aX;t.emplaceBack(-1,-1),t.emplaceBack(1,-1),t.emplaceBack(1,1),t.emplaceBack(-1,1);const s=new e.aY;s.emplaceBack(0,1,2),s.emplaceBack(0,2,3),i.mesh=new ga(a.createVertexBuffer(t,_e.members),a.createIndexBuffer(s),e.a0.simpleSegment(0,0,t.length,s.length));}h.draw(a,s.TRIANGLES,r,n,l,ji.disabled,o,void 0,"sky",i.mesh.vertexBuffer,i.mesh.indexBuffer,i.mesh.segments);}(this,this.style.sky),this._showOverdrawInspector=i.showOverdrawInspector,this.depthRangeFor3D=[0,1-(t._order.length+2)*this.numSublayers*this.depthEpsilon],!this.renderToTexture)for(this.renderPass="opaque",this.currentLayer=a.length-1;this.currentLayer>=0;this.currentLayer--){const t=this.style._layers[a[this.currentLayer]],e=s[t.source],i=r[t.source];this._renderTileClippingMasks(t,i),this.renderLayer(this,e,t,i);}for(this.renderPass="translucent",this.currentLayer=0;this.currentLayeri.source&&!i.isHidden(e)?[t.sourceCaches[i.source]]:[])),s=a.filter((t=>"vector"===t.getSource().type)),o=a.filter((t=>"vector"!==t.getSource().type)),r=t=>{(!i||i.getSource().maxzoomr(t))),i||o.forEach((t=>r(t))),i}(this.style,this.transform.zoom);t&&function(t,e,i){for(let a=0;a0),s&&(e.b0(i,a),this.terrainFacilitator.renderTime=Date.now(),this.terrainFacilitator.dirty=!1,function(t,i){const a=t.context,s=a.gl,o=Fi.unblended,r=new Oi(s.LEQUAL,Oi.ReadWrite,[0,1]),n=i.getTerrainMesh(),l=i.sourceCache.getRenderableTiles(),h=t.useProgram("terrainDepth");a.bindFramebuffer.set(i.getFramebuffer("depth").framebuffer),a.viewport.set([0,0,t.width/devicePixelRatio,t.height/devicePixelRatio]),a.clear({color:e.aM.transparent,depth:1});for(const e of l){const l=i.getTerrainData(e.tileID),c={u_matrix:t.transform.calculatePosMatrix(e.tileID.toUnwrapped()),u_ele_delta:i.getMeshFrameDelta(t.transform.zoom)};h.draw(a,s.TRIANGLES,r,Ui.disabled,o,ji.backCCW,c,l,"terrain",n.vertexBuffer,n.indexBuffer,n.segments);}a.bindFramebuffer.set(null),a.viewport.set([0,0,t.width,t.height]);}(this,this.style.map.terrain),function(t,i){const a=t.context,s=a.gl,o=Fi.unblended,r=new Oi(s.LEQUAL,Oi.ReadWrite,[0,1]),n=i.getTerrainMesh(),l=i.getCoordsTexture(),h=i.sourceCache.getRenderableTiles(),c=t.useProgram("terrainCoords");a.bindFramebuffer.set(i.getFramebuffer("coords").framebuffer),a.viewport.set([0,0,t.width/devicePixelRatio,t.height/devicePixelRatio]),a.clear({color:e.aM.transparent,depth:1}),i.coordsIndex=[];for(const e of h){const h=i.getTerrainData(e.tileID);a.activeTexture.set(s.TEXTURE0),s.bindTexture(s.TEXTURE_2D,l.texture);const u={u_matrix:t.transform.calculatePosMatrix(e.tileID.toUnwrapped()),u_terrain_coords_id:(255-i.coordsIndex.length)/255,u_texture:0,u_ele_delta:i.getMeshFrameDelta(t.transform.zoom)};c.draw(a,s.TRIANGLES,r,Ui.disabled,o,ji.backCCW,u,h,"terrain",n.vertexBuffer,n.indexBuffer,n.segments),i.coordsIndex.push(e.tileID.key);}a.bindFramebuffer.set(null),a.viewport.set([0,0,t.width,t.height]);}(this,this.style.map.terrain));}renderLayer(t,i,a,s){if(!a.isHidden(this.transform.zoom)&&("background"===a.type||"custom"===a.type||(s||[]).length))switch(this.id=a.id,a.type){case"symbol":!function(t,i,a,s,o){if("translucent"!==t.renderPass)return;const r=Ui.disabled,n=t.colorModeForRenderPass();(a._unevaluatedLayout.hasValue("text-variable-anchor")||a._unevaluatedLayout.hasValue("text-variable-anchor-offset"))&&function(t,i,a,s,o,r,n,l,h){const c=i.transform,u=ie(),d="map"===o,_="map"===r;for(const o of t){const t=s.getTile(o),r=t.getBucket(a);if(!r||!r.text||!r.text.segments.get().length)continue;const p=e.ag(r.textSizeData,c.zoom),m=Bt(t,1,i.transform.zoom),f=gt(o.posMatrix,_,d,i.transform,m),g="none"!==a.layout.get("icon-text-fit")&&r.hasIconData();if(p){const e=Math.pow(2,c.zoom-t.tileID.overscaledZ),a=i.style.map.terrain?(t,e)=>i.style.map.terrain.getElevation(o,t,e):null,s=u.translatePosition(c,t,n,l);Wi(r,d,_,h,c,f,o.posMatrix,e,p,g,u,s,o.toUnwrapped(),a);}}}(s,t,a,i,a.layout.get("text-rotation-alignment"),a.layout.get("text-pitch-alignment"),a.paint.get("text-translate"),a.paint.get("text-translate-anchor"),o),0!==a.paint.get("icon-opacity").constantOr(1)&&Xi(t,i,a,s,!1,a.paint.get("icon-translate"),a.paint.get("icon-translate-anchor"),a.layout.get("icon-rotation-alignment"),a.layout.get("icon-pitch-alignment"),a.layout.get("icon-keep-upright"),r,n),0!==a.paint.get("text-opacity").constantOr(1)&&Xi(t,i,a,s,!0,a.paint.get("text-translate"),a.paint.get("text-translate-anchor"),a.layout.get("text-rotation-alignment"),a.layout.get("text-pitch-alignment"),a.layout.get("text-keep-upright"),r,n),i.map.showCollisionBoxes&&(qi(t,i,a,s,!0),qi(t,i,a,s,!1));}(t,i,a,s,this.style.placement.variableOffsets);break;case"circle":!function(t,i,a,s){if("translucent"!==t.renderPass)return;const o=a.paint.get("circle-opacity"),r=a.paint.get("circle-stroke-width"),n=a.paint.get("circle-stroke-opacity"),l=!a.layout.get("circle-sort-key").isConstant();if(0===o.constantOr(1)&&(0===r.constantOr(1)||0===n.constantOr(1)))return;const h=t.context,c=h.gl,u=t.depthModeForSublayer(0,Oi.ReadOnly),d=Ui.disabled,_=t.colorModeForRenderPass(),p=[];for(let o=0;ot.sortKey-e.sortKey));for(const e of p){const{programConfiguration:i,program:s,layoutVertexBuffer:o,indexBuffer:r,uniformValues:n,terrainData:l}=e.state;s.draw(h,c.TRIANGLES,u,d,_,ji.disabled,n,l,a.id,o,r,e.segments,a.paint,t.transform.zoom,i);}}(t,i,a,s);break;case"heatmap":!function(t,i,a,s){if(0===a.paint.get("heatmap-opacity"))return;const o=t.context;if(t.style.map.terrain){for(const e of s){const s=i.getTile(e);i.hasRenderableParent(e)||("offscreen"===t.renderPass?Ji(t,s,a,e):"translucent"===t.renderPass&&Yi(t,a,e));}o.viewport.set([0,0,t.width,t.height]);}else "offscreen"===t.renderPass?function(t,i,a,s){const o=t.context,r=o.gl,n=Ui.disabled,l=new Fi([r.ONE,r.ONE],e.aM.transparent,[!0,!0,!0,!0]);((function(t,i,a){const s=t.gl;t.activeTexture.set(s.TEXTURE1),t.viewport.set([0,0,i.width/4,i.height/4]);let o=a.heatmapFbos.get(e.aU);o?(s.bindTexture(s.TEXTURE_2D,o.colorAttachment.get()),t.bindFramebuffer.set(o.framebuffer)):(o=Qi(t,i.width/4,i.height/4),a.heatmapFbos.set(e.aU,o));}))(o,t,a),o.clear({color:e.aM.transparent});for(let e=0;e20&&o.texParameterf(o.TEXTURE_2D,s.extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT,s.extTextureFilterAnisotropicMax);const y=t.style.map.terrain&&t.style.map.terrain.getTerrainData(a),b=y?a:null,w=b?b.posMatrix:t.transform.calculatePosMatrix(a.toUnwrapped(),d),T=Ue(w,v||[0,0],g||1,f,i);r instanceof Q?n.draw(s,o.TRIANGLES,c,Ui.disabled,l,ji.disabled,T,y,i.id,r.boundsBuffer,t.quadTriangleIndexBuffer,r.boundsSegments):n.draw(s,o.TRIANGLES,c,h[a.overscaledZ],l,ji.disabled,T,y,i.id,t.rasterBoundsBuffer,t.quadTriangleIndexBuffer,t.rasterBoundsSegments);}}(t,i,a,s);break;case"background":!function(t,e,i,a){const s=i.paint.get("background-color"),o=i.paint.get("background-opacity");if(0===o)return;const r=t.context,n=r.gl,l=t.transform,h=l.tileSize,c=i.paint.get("background-pattern");if(t.isPatternMissing(c))return;const u=!c&&1===s.a&&1===o&&t.opaquePassEnabledForLayer()?"opaque":"translucent";if(t.renderPass!==u)return;const d=Ui.disabled,_=t.depthModeForSublayer(0,"opaque"===u?Oi.ReadWrite:Oi.ReadOnly),p=t.colorModeForRenderPass(),m=t.useProgram(c?"backgroundPattern":"background"),f=a||l.coveringTiles({tileSize:h,terrain:t.style.map.terrain});c&&(r.activeTexture.set(n.TEXTURE0),t.imageManager.bind(t.context));const g=i.getCrossfadeParameters();for(const e of f){const l=a?e.posMatrix:t.transform.calculatePosMatrix(e.toUnwrapped()),u=c?He(l,o,t,c,{tileID:e,tileSize:h},g):Ge(l,o,s),f=t.style.map.terrain&&t.style.map.terrain.getTerrainData(e);m.draw(r,n.TRIANGLES,_,d,p,ji.disabled,u,f,i.id,t.tileExtentBuffer,t.quadTriangleIndexBuffer,t.tileExtentSegments);}}(t,0,a,s);break;case"custom":!function(t,e,i){const a=t.context,s=i.implementation;if("offscreen"===t.renderPass){const e=s.prerender;e&&(t.setCustomLayerDefaults(),a.setColorMode(t.colorModeForRenderPass()),e.call(s,a.gl,t.transform.customLayerMatrix()),a.setDirty(),t.setBaseState());}else if("translucent"===t.renderPass){t.setCustomLayerDefaults(),a.setColorMode(t.colorModeForRenderPass()),a.setStencilMode(Ui.disabled);const e="3d"===s.renderingMode?new Oi(t.context.gl.LEQUAL,Oi.ReadWrite,t.depthRangeFor3D):t.depthModeForSublayer(0,Oi.ReadOnly);a.setDepthMode(e),s.render(a.gl,t.transform.customLayerMatrix(),{farZ:t.transform.farZ,nearZ:t.transform.nearZ,fov:t.transform._fov,modelViewProjectionMatrix:t.transform.modelViewProjectionMatrix,projectionMatrix:t.transform.projectionMatrix}),a.setDirty(),t.setBaseState(),a.bindFramebuffer.set(null);}}(t,0,a);}}translatePosMatrix(t,i,a,s,o){if(!a[0]&&!a[1])return t;const r=o?"map"===s?this.transform.angle:0:"viewport"===s?-this.transform.angle:0;if(r){const t=Math.sin(r),e=Math.cos(r);a=[a[0]*e-a[1]*t,a[0]*t+a[1]*e];}const n=[o?a[0]:Bt(i,a[0],this.transform.zoom),o?a[1]:Bt(i,a[1],this.transform.zoom),0],l=new Float32Array(16);return e.J(l,t,n),l}saveTileTexture(t){const e=this._tileTextures[t.size[0]];e?e.push(t):this._tileTextures[t.size[0]]=[t];}getTileTexture(t){const e=this._tileTextures[t];return e&&e.length>0?e.pop():null}isPatternMissing(t){if(!t)return !1;if(!t.from||!t.to)return !0;const e=this.imageManager.getPattern(t.from.toString()),i=this.imageManager.getPattern(t.to.toString());return !e||!i}useProgram(t,e){this.cache=this.cache||{};const i=t+(e?e.cacheKey:"")+(this._showOverdrawInspector?"/overdraw":"")+(this.style.map.terrain?"/terrain":"");return this.cache[i]||(this.cache[i]=new xe(this.context,pe[t],e,We[t],this._showOverdrawInspector,this.style.map.terrain)),this.cache[i]}setCustomLayerDefaults(){this.context.unbindVAO(),this.context.cullFace.setDefault(),this.context.activeTexture.setDefault(),this.context.pixelStoreUnpack.setDefault(),this.context.pixelStoreUnpackPremultiplyAlpha.setDefault(),this.context.pixelStoreUnpackFlipY.setDefault();}setBaseState(){const t=this.context.gl;this.context.cullFace.set(!1),this.context.viewport.set([0,0,this.width,this.height]),this.context.blendEquation.set(t.FUNC_ADD);}initDebugOverlayCanvas(){null==this.debugOverlayCanvas&&(this.debugOverlayCanvas=document.createElement("canvas"),this.debugOverlayCanvas.width=512,this.debugOverlayCanvas.height=512,this.debugOverlayTexture=new b(this.context,this.debugOverlayCanvas,this.context.gl.RGBA));}destroy(){this.debugOverlayTexture&&this.debugOverlayTexture.destroy();}overLimit(){const{drawingBufferWidth:t,drawingBufferHeight:e}=this.context.gl;return this.width!==t||this.height!==e}}class xa{constructor(t,e){this.points=t,this.planes=e;}static fromInvProjectionMatrix(t,i,a){const s=Math.pow(2,a),o=[[-1,1,-1,1],[1,1,-1,1],[1,-1,-1,1],[-1,-1,-1,1],[-1,1,1,1],[1,1,1,1],[1,-1,1,1],[-1,-1,1,1]].map((a=>{const o=1/(a=e.af([],a,t))[3]/i*s;return e.b1(a,a,[o,o,1/a[3],o])})),r=[[0,1,2],[6,5,4],[0,3,7],[2,1,5],[3,2,6],[0,4,5]].map((t=>{const e=function(t,e){var i=e[0],a=e[1],s=e[2],o=i*i+a*a+s*s;return o>0&&(o=1/Math.sqrt(o)),t[0]=e[0]*o,t[1]=e[1]*o,t[2]=e[2]*o,t}([],function(t,e,i){var a=e[0],s=e[1],o=e[2],r=i[0],n=i[1],l=i[2];return t[0]=s*l-o*n,t[1]=o*r-a*l,t[2]=a*n-s*r,t}([],g([],o[t[0]],o[t[1]]),g([],o[t[2]],o[t[1]]))),i=-((a=e)[0]*(s=o[t[1]])[0]+a[1]*s[1]+a[2]*s[2]);var a,s;return e.concat(i)}));return new xa(o,r)}}class ya{constructor(t,e){this.min=t,this.max=e,this.center=function(t,e,i){return t[0]=.5*e[0],t[1]=.5*e[1],t[2]=.5*e[2],t}([],function(t,e,i){return t[0]=e[0]+i[0],t[1]=e[1]+i[1],t[2]=e[2]+i[2],t}([],this.min,this.max));}quadrant(t){const e=[t%2==0,t<2],i=m(this.min),a=m(this.max);for(let t=0;t=0&&r++;if(0===r)return 0;r!==i.length&&(a=!1);}if(a)return 2;for(let e=0;e<3;e++){let i=Number.MAX_VALUE,a=-Number.MAX_VALUE;for(let s=0;sthis.max[e]-this.min[e])return 0}return 1}}class ba{constructor(t=0,e=0,i=0,a=0){if(isNaN(t)||t<0||isNaN(e)||e<0||isNaN(i)||i<0||isNaN(a)||a<0)throw new Error("Invalid value for edge-insets, top, bottom, left and right must all be numbers");this.top=t,this.bottom=e,this.left=i,this.right=a;}interpolate(t,i,a){return null!=i.top&&null!=t.top&&(this.top=e.y.number(t.top,i.top,a)),null!=i.bottom&&null!=t.bottom&&(this.bottom=e.y.number(t.bottom,i.bottom,a)),null!=i.left&&null!=t.left&&(this.left=e.y.number(t.left,i.left,a)),null!=i.right&&null!=t.right&&(this.right=e.y.number(t.right,i.right,a)),this}getCenter(t,i){const a=e.ac((this.left+t-this.right)/2,0,t),s=e.ac((this.top+i-this.bottom)/2,0,i);return new e.P(a,s)}equals(t){return this.top===t.top&&this.bottom===t.bottom&&this.left===t.left&&this.right===t.right}clone(){return new ba(this.top,this.bottom,this.left,this.right)}toJSON(){return {top:this.top,bottom:this.bottom,left:this.left,right:this.right}}}const wa=85.051129;class Ta{constructor(t,i,a,s,o){this.tileSize=512,this._renderWorldCopies=void 0===o||!!o,this._minZoom=t||0,this._maxZoom=i||22,this._minPitch=null==a?0:a,this._maxPitch=null==s?60:s,this.setMaxBounds(),this.width=0,this.height=0,this._center=new e.N(0,0),this._elevation=0,this.zoom=0,this.angle=0,this._fov=.6435011087932844,this._pitch=0,this._unmodified=!0,this._edgeInsets=new ba,this._posMatrixCache={},this._alignedPosMatrixCache={},this._fogMatrixCache={},this.minElevationForCurrentTile=0;}clone(){const t=new Ta(this._minZoom,this._maxZoom,this._minPitch,this.maxPitch,this._renderWorldCopies);return t.apply(this),t}apply(t){this.tileSize=t.tileSize,this.latRange=t.latRange,this.lngRange=t.lngRange,this.width=t.width,this.height=t.height,this._center=t._center,this._elevation=t._elevation,this.minElevationForCurrentTile=t.minElevationForCurrentTile,this.zoom=t.zoom,this.angle=t.angle,this._fov=t._fov,this._pitch=t._pitch,this._unmodified=t._unmodified,this._edgeInsets=t._edgeInsets.clone(),this._calcMatrices();}get minZoom(){return this._minZoom}set minZoom(t){this._minZoom!==t&&(this._minZoom=t,this.zoom=Math.max(this.zoom,t));}get maxZoom(){return this._maxZoom}set maxZoom(t){this._maxZoom!==t&&(this._maxZoom=t,this.zoom=Math.min(this.zoom,t));}get minPitch(){return this._minPitch}set minPitch(t){this._minPitch!==t&&(this._minPitch=t,this.pitch=Math.max(this.pitch,t));}get maxPitch(){return this._maxPitch}set maxPitch(t){this._maxPitch!==t&&(this._maxPitch=t,this.pitch=Math.min(this.pitch,t));}get renderWorldCopies(){return this._renderWorldCopies}set renderWorldCopies(t){void 0===t?t=!0:null===t&&(t=!1),this._renderWorldCopies=t;}get worldSize(){return this.tileSize*this.scale}get centerOffset(){return this.centerPoint._sub(this.size._div(2))}get size(){return new e.P(this.width,this.height)}get bearing(){return -this.angle/Math.PI*180}set bearing(t){const i=-e.b3(t,-180,180)*Math.PI/180;this.angle!==i&&(this._unmodified=!1,this.angle=i,this._calcMatrices(),this.rotationMatrix=function(){var t=new e.A(4);return e.A!=Float32Array&&(t[1]=0,t[2]=0),t[0]=1,t[3]=1,t}(),function(t,e,i){var a=e[0],s=e[1],o=e[2],r=e[3],n=Math.sin(i),l=Math.cos(i);t[0]=a*l+o*n,t[1]=s*l+r*n,t[2]=a*-n+o*l,t[3]=s*-n+r*l;}(this.rotationMatrix,this.rotationMatrix,this.angle));}get pitch(){return this._pitch/Math.PI*180}set pitch(t){const i=e.ac(t,this.minPitch,this.maxPitch)/180*Math.PI;this._pitch!==i&&(this._unmodified=!1,this._pitch=i,this._calcMatrices());}get fov(){return this._fov/Math.PI*180}set fov(t){t=Math.max(.01,Math.min(60,t)),this._fov!==t&&(this._unmodified=!1,this._fov=t/180*Math.PI,this._calcMatrices());}get zoom(){return this._zoom}set zoom(t){const e=Math.min(Math.max(t,this.minZoom),this.maxZoom);this._zoom!==e&&(this._unmodified=!1,this._zoom=e,this.tileZoom=Math.max(0,Math.floor(e)),this.scale=this.zoomScale(e),this._constrain(),this._calcMatrices());}get center(){return this._center}set center(t){t.lat===this._center.lat&&t.lng===this._center.lng||(this._unmodified=!1,this._center=t,this._constrain(),this._calcMatrices());}get elevation(){return this._elevation}set elevation(t){t!==this._elevation&&(this._elevation=t,this._constrain(),this._calcMatrices());}get padding(){return this._edgeInsets.toJSON()}set padding(t){this._edgeInsets.equals(t)||(this._unmodified=!1,this._edgeInsets.interpolate(this._edgeInsets,t,1),this._calcMatrices());}get centerPoint(){return this._edgeInsets.getCenter(this.width,this.height)}isPaddingEqual(t){return this._edgeInsets.equals(t)}interpolatePadding(t,e,i){this._unmodified=!1,this._edgeInsets.interpolate(t,e,i),this._constrain(),this._calcMatrices();}coveringZoomLevel(t){const e=(t.roundZoom?Math.round:Math.floor)(this.zoom+this.scaleZoom(this.tileSize/t.tileSize));return Math.max(0,e)}getVisibleUnwrappedCoordinates(t){const i=[new e.b4(0,t)];if(this._renderWorldCopies){const a=this.pointCoordinate(new e.P(0,0)),s=this.pointCoordinate(new e.P(this.width,0)),o=this.pointCoordinate(new e.P(this.width,this.height)),r=this.pointCoordinate(new e.P(0,this.height)),n=Math.floor(Math.min(a.x,s.x,o.x,r.x)),l=Math.floor(Math.max(a.x,s.x,o.x,r.x)),h=1;for(let a=n-h;a<=l+h;a++)0!==a&&i.push(new e.b4(a,t));}return i}coveringTiles(t){var i,a;let s=this.coveringZoomLevel(t);const o=s;if(void 0!==t.minzoom&&st.maxzoom&&(s=t.maxzoom);const r=this.pointCoordinate(this.getCameraPoint()),n=e.Z.fromLngLat(this.center),l=Math.pow(2,s),h=[l*r.x,l*r.y,0],c=[l*n.x,l*n.y,0],u=xa.fromInvProjectionMatrix(this.invModelViewProjectionMatrix,this.worldSize,s);let d=t.minzoom||0;!t.terrain&&this.pitch<=60&&this._edgeInsets.top<.1&&(d=s);const _=t.terrain?2/Math.min(this.tileSize,t.tileSize)*this.tileSize:3,p=t=>({aabb:new ya([t*l,0,0],[(t+1)*l,l,0]),zoom:0,x:0,y:0,wrap:t,fullyVisible:!1}),m=[],f=[],g=s,x=t.reparseOverscaled?o:s;if(this._renderWorldCopies)for(let t=1;t<=3;t++)m.push(p(-t)),m.push(p(t));for(m.push(p(0));m.length>0;){const s=m.pop(),o=s.x,r=s.y;let n=s.fullyVisible;if(!n){const t=s.aabb.intersects(u);if(0===t)continue;n=2===t;}const l=t.terrain?h:c,p=s.aabb.distanceX(l),y=s.aabb.distanceY(l),b=Math.max(Math.abs(p),Math.abs(y));if(s.zoom===g||b>_+(1<=d){const t=g-s.zoom,i=h[0]-.5-(o<>1),u=s.zoom+1;let d=s.aabb.quadrant(l);if(t.terrain){const o=new e.S(u,s.wrap,u,h,c),r=t.terrain.getMinMaxElevation(o),n=null!==(i=r.minElevation)&&void 0!==i?i:this.elevation,l=null!==(a=r.maxElevation)&&void 0!==a?a:this.elevation;d=new ya([d.min[0],d.min[1],n],[d.max[0],d.max[1],l]);}m.push({aabb:d,zoom:u,x:h,y:c,wrap:s.wrap,fullyVisible:n});}}return f.sort(((t,e)=>t.distanceSq-e.distanceSq)).map((t=>t.tileID))}resize(t,e){this.width=t,this.height=e,this.pixelsToGLUnits=[2/t,-2/e],this._constrain(),this._calcMatrices();}get unmodified(){return this._unmodified}zoomScale(t){return Math.pow(2,t)}scaleZoom(t){return Math.log(t)/Math.LN2}project(t){const i=e.ac(t.lat,-85.051129,wa);return new e.P(e.O(t.lng)*this.worldSize,e.Q(i)*this.worldSize)}unproject(t){return new e.Z(t.x/this.worldSize,t.y/this.worldSize).toLngLat()}get point(){return this.project(this.center)}getCameraPosition(){return {lngLat:this.pointLocation(this.getCameraPoint()),altitude:Math.cos(this._pitch)*this.cameraToCenterDistance/this._pixelPerMeter+this.elevation}}recalculateZoom(t){const i=this.elevation,a=Math.cos(this._pitch)*this.cameraToCenterDistance/this._pixelPerMeter,s=this.pointLocation(this.centerPoint,t),o=t.getElevationForLngLatZoom(s,this.tileZoom);if(!(this.elevation-o))return;const r=a+i-o,n=Math.cos(this._pitch)*this.cameraToCenterDistance/r/e.b5(1,s.lat),l=this.scaleZoom(n/this.tileSize);this._elevation=o,this._center=s,this.zoom=l;}setLocationAtPoint(t,i){const a=this.pointCoordinate(i),s=this.pointCoordinate(this.centerPoint),o=this.locationCoordinate(t),r=new e.Z(o.x-(a.x-s.x),o.y-(a.y-s.y));this.center=this.coordinateLocation(r),this._renderWorldCopies&&(this.center=this.center.wrap());}locationPoint(t,e){return e?this.coordinatePoint(this.locationCoordinate(t),e.getElevationForLngLatZoom(t,this.tileZoom),this.pixelMatrix3D):this.coordinatePoint(this.locationCoordinate(t))}pointLocation(t,e){return this.coordinateLocation(this.pointCoordinate(t,e))}locationCoordinate(t){return e.Z.fromLngLat(t)}coordinateLocation(t){return t&&t.toLngLat()}pointCoordinate(t,i){if(i){const e=i.pointCoordinate(t);if(null!=e)return e}const a=[t.x,t.y,0,1],s=[t.x,t.y,1,1];e.af(a,a,this.pixelMatrixInverse),e.af(s,s,this.pixelMatrixInverse);const o=a[3],r=s[3],n=a[1]/o,l=s[1]/r,h=a[2]/o,c=s[2]/r,u=h===c?0:(0-h)/(c-h);return new e.Z(e.y.number(a[0]/o,s[0]/r,u)/this.worldSize,e.y.number(n,l,u)/this.worldSize)}coordinatePoint(t,i=0,a=this.pixelMatrix){const s=[t.x*this.worldSize,t.y*this.worldSize,i,1];return e.af(s,s,a),new e.P(s[0]/s[3],s[1]/s[3])}getBounds(){const t=Math.max(0,this.height/2-this.getHorizon());return (new H).extend(this.pointLocation(new e.P(0,t))).extend(this.pointLocation(new e.P(this.width,t))).extend(this.pointLocation(new e.P(this.width,this.height))).extend(this.pointLocation(new e.P(0,this.height)))}getMaxBounds(){return this.latRange&&2===this.latRange.length&&this.lngRange&&2===this.lngRange.length?new H([this.lngRange[0],this.latRange[0]],[this.lngRange[1],this.latRange[1]]):null}getHorizon(){return Math.tan(Math.PI/2-this._pitch)*this.cameraToCenterDistance*.85}setMaxBounds(t){t?(this.lngRange=[t.getWest(),t.getEast()],this.latRange=[t.getSouth(),t.getNorth()],this._constrain()):(this.lngRange=null,this.latRange=[-85.051129,wa]);}calculateTileMatrix(t){const i=t.canonical,a=this.worldSize/this.zoomScale(i.z),s=i.x+Math.pow(2,i.z)*t.wrap,o=e.an(new Float64Array(16));return e.J(o,o,[s*a,i.y*a,0]),e.K(o,o,[a/e.X,a/e.X,1]),o}calculatePosMatrix(t,i=!1){const a=t.key,s=i?this._alignedPosMatrixCache:this._posMatrixCache;if(s[a])return s[a];const o=this.calculateTileMatrix(t);return e.L(o,i?this.alignedModelViewProjectionMatrix:this.modelViewProjectionMatrix,o),s[a]=new Float32Array(o),s[a]}calculateFogMatrix(t){const i=t.key,a=this._fogMatrixCache;if(a[i])return a[i];const s=this.calculateTileMatrix(t);return e.L(s,this.fogMatrix,s),a[i]=new Float32Array(s),a[i]}customLayerMatrix(){return this.mercatorMatrix.slice()}getConstrained(t,i){i=e.ac(+i,this.minZoom,this.maxZoom);const a={center:new e.N(t.lng,t.lat),zoom:i};let s=this.lngRange;if(!this._renderWorldCopies&&null===s){const t=180-1e-10;s=[-t,t];}const o=this.tileSize*this.zoomScale(a.zoom);let r=0,n=o,l=0,h=o,c=0,u=0;const{x:d,y:_}=this.size;if(this.latRange){const t=this.latRange;r=e.Q(t[1])*o,n=e.Q(t[0])*o,n-r<_&&(c=_/(n-r));}s&&(l=e.b3(e.O(s[0])*o,0,o),h=e.b3(e.O(s[1])*o,0,o),hn&&(g=n-t);}if(s){const t=(l+h)/2;let i=p;this._renderWorldCopies&&(i=e.b3(p,t-o/2,t+o/2));const a=d/2;i-ah&&(f=h-a);}if(void 0!==f||void 0!==g){const t=new e.P(null!=f?f:p,null!=g?g:m);a.center=this.unproject.call({worldSize:o},t).wrap();}return a}_constrain(){if(!this.center||!this.width||!this.height||this._constraining)return;this._constraining=!0;const t=this._unmodified,{center:e,zoom:i}=this.getConstrained(this.center,this.zoom);this.center=e,this.zoom=i,this._unmodified=t,this._constraining=!1;}_calcMatrices(){if(!this.height)return;const t=this.centerOffset,i=this.point.x,a=this.point.y;this.cameraToCenterDistance=.5/Math.tan(this._fov/2)*this.height,this._pixelPerMeter=e.b5(1,this.center.lat)*this.worldSize;let s=e.an(new Float64Array(16));e.K(s,s,[this.width/2,-this.height/2,1]),e.J(s,s,[1,-1,0]),this.labelPlaneMatrix=s,s=e.an(new Float64Array(16)),e.K(s,s,[1,-1,1]),e.J(s,s,[-1,-1,0]),e.K(s,s,[2/this.width,2/this.height,1]),this.glCoordMatrix=s;const o=this.cameraToCenterDistance+this._elevation*this._pixelPerMeter/Math.cos(this._pitch),r=Math.min(this.elevation,this.minElevationForCurrentTile),n=o-r*this._pixelPerMeter/Math.cos(this._pitch),l=r<0?n:o,h=Math.PI/2+this._pitch,c=this._fov*(.5+t.y/this.height),u=Math.sin(c)*l/Math.sin(e.ac(Math.PI-h-c,.01,Math.PI-.01)),d=this.getHorizon(),_=2*Math.atan(d/this.cameraToCenterDistance)*(.5+t.y/(2*d)),p=Math.sin(_)*l/Math.sin(e.ac(Math.PI-h-_,.01,Math.PI-.01)),m=Math.min(u,p);this.farZ=1.01*(Math.cos(Math.PI/2-this._pitch)*m+l),this.nearZ=this.height/50,s=new Float64Array(16),e.b6(s,this._fov,this.width/this.height,this.nearZ,this.farZ),s[8]=2*-t.x/this.width,s[9]=2*t.y/this.height,this.projectionMatrix=e.ae(s),e.K(s,s,[1,-1,1]),e.J(s,s,[0,0,-this.cameraToCenterDistance]),e.b7(s,s,this._pitch),e.ad(s,s,this.angle),e.J(s,s,[-i,-a,0]),this.mercatorMatrix=e.K([],s,[this.worldSize,this.worldSize,this.worldSize]),e.K(s,s,[1,1,this._pixelPerMeter]),this.pixelMatrix=e.L(new Float64Array(16),this.labelPlaneMatrix,s),e.J(s,s,[0,0,-this.elevation]),this.modelViewProjectionMatrix=s,this.invModelViewProjectionMatrix=e.as([],s),this.fogMatrix=new Float64Array(16),e.b6(this.fogMatrix,this._fov,this.width/this.height,o,this.farZ),this.fogMatrix[8]=2*-t.x/this.width,this.fogMatrix[9]=2*t.y/this.height,e.K(this.fogMatrix,this.fogMatrix,[1,-1,1]),e.J(this.fogMatrix,this.fogMatrix,[0,0,-this.cameraToCenterDistance]),e.b7(this.fogMatrix,this.fogMatrix,this._pitch),e.ad(this.fogMatrix,this.fogMatrix,this.angle),e.J(this.fogMatrix,this.fogMatrix,[-i,-a,0]),e.K(this.fogMatrix,this.fogMatrix,[1,1,this._pixelPerMeter]),e.J(this.fogMatrix,this.fogMatrix,[0,0,-this.elevation]),this.pixelMatrix3D=e.L(new Float64Array(16),this.labelPlaneMatrix,s);const f=this.width%2/2,g=this.height%2/2,v=Math.cos(this.angle),x=Math.sin(this.angle),y=i-Math.round(i)+v*f+x*g,b=a-Math.round(a)+v*g+x*f,w=new Float64Array(s);if(e.J(w,w,[y>.5?y-1:y,b>.5?b-1:b,0]),this.alignedModelViewProjectionMatrix=w,s=e.as(new Float64Array(16),this.pixelMatrix),!s)throw new Error("failed to invert matrix");this.pixelMatrixInverse=s,this._posMatrixCache={},this._alignedPosMatrixCache={},this._fogMatrixCache={};}maxPitchScaleFactor(){if(!this.pixelMatrixInverse)return 1;const t=this.pointCoordinate(new e.P(0,0)),i=[t.x*this.worldSize,t.y*this.worldSize,0,1];return e.af(i,i,this.pixelMatrix)[3]/this.cameraToCenterDistance}getCameraPoint(){const t=Math.tan(this._pitch)*(this.cameraToCenterDistance||1);return this.centerPoint.add(new e.P(0,t))}getCameraQueryGeometry(t){const i=this.getCameraPoint();if(1===t.length)return [t[0],i];{let a=i.x,s=i.y,o=i.x,r=i.y;for(const e of t)a=Math.min(a,e.x),s=Math.min(s,e.y),o=Math.max(o,e.x),r=Math.max(r,e.y);return [new e.P(a,s),new e.P(o,s),new e.P(o,r),new e.P(a,r),new e.P(a,s)]}}lngLatToCameraDepth(t,i){const a=this.locationCoordinate(t),s=[a.x*this.worldSize,a.y*this.worldSize,i,1];return e.af(s,s,this.modelViewProjectionMatrix),s[2]/s[3]}}function Ia(t,e){let i,a=!1,s=null,o=null;const r=()=>{s=null,a&&(t.apply(o,i),s=setTimeout(r,e),a=!1);};return (...t)=>(a=!0,o=this,i=t,s||r(),s)}class Ea{constructor(t){this._getCurrentHash=()=>{const t=window.location.hash.replace("#","");if(this._hashName){let e;return t.split("&").map((t=>t.split("="))).forEach((t=>{t[0]===this._hashName&&(e=t);})),(e&&e[1]||"").split("/")}return t.split("/")},this._onHashChange=()=>{const t=this._getCurrentHash();if(t.length>=3&&!t.some((t=>isNaN(t)))){const e=this._map.dragRotate.isEnabled()&&this._map.touchZoomRotate.isEnabled()?+(t[3]||0):this._map.getBearing();return this._map.jumpTo({center:[+t[2],+t[1]],zoom:+t[0],bearing:e,pitch:+(t[4]||0)}),!0}return !1},this._updateHashUnthrottled=()=>{const t=window.location.href.replace(/(#.*)?$/,this.getHashString());window.history.replaceState(window.history.state,null,t);},this._removeHash=()=>{const t=this._getCurrentHash();if(0===t.length)return;const e=t.join("/");let i=e;i.split("&").length>0&&(i=i.split("&")[0]),this._hashName&&(i=`${this._hashName}=${e}`);let a=window.location.hash.replace(i,"");a.startsWith("#&")?a=a.slice(0,1)+a.slice(2):"#"===a&&(a="");let s=window.location.href.replace(/(#.+)?$/,a);s=s.replace("&&","&"),window.history.replaceState(window.history.state,null,s);},this._updateHash=Ia(this._updateHashUnthrottled,300),this._hashName=t&&encodeURIComponent(t);}addTo(t){return this._map=t,addEventListener("hashchange",this._onHashChange,!1),this._map.on("moveend",this._updateHash),this}remove(){return removeEventListener("hashchange",this._onHashChange,!1),this._map.off("moveend",this._updateHash),clearTimeout(this._updateHash()),this._removeHash(),delete this._map,this}getHashString(t){const e=this._map.getCenter(),i=Math.round(100*this._map.getZoom())/100,a=Math.ceil((i*Math.LN2+Math.log(512/360/.5))/Math.LN10),s=Math.pow(10,a),o=Math.round(e.lng*s)/s,r=Math.round(e.lat*s)/s,n=this._map.getBearing(),l=this._map.getPitch();let h="";if(h+=t?`/${o}/${r}/${i}`:`${i}/${r}/${o}`,(n||l)&&(h+="/"+Math.round(10*n)/10),l&&(h+=`/${Math.round(l)}`),this._hashName){const t=this._hashName;let e=!1;const i=window.location.hash.slice(1).split("&").map((i=>{const a=i.split("=")[0];return a===t?(e=!0,`${a}=${h}`):i})).filter((t=>t));return e||i.push(`${t}=${h}`),`#${i.join("&")}`}return `#${h}`}}const Pa={linearity:.3,easing:e.b8(0,0,.3,1)},Ca=e.e({deceleration:2500,maxSpeed:1400},Pa),Sa=e.e({deceleration:20,maxSpeed:1400},Pa),za=e.e({deceleration:1e3,maxSpeed:360},Pa),Da=e.e({deceleration:1e3,maxSpeed:90},Pa);class Ma{constructor(t){this._map=t,this.clear();}clear(){this._inertiaBuffer=[];}record(t){this._drainInertiaBuffer(),this._inertiaBuffer.push({time:o.now(),settings:t});}_drainInertiaBuffer(){const t=this._inertiaBuffer,e=o.now();for(;t.length>0&&e-t[0].time>160;)t.shift();}_onMoveEnd(t){if(this._drainInertiaBuffer(),this._inertiaBuffer.length<2)return;const i={zoom:0,bearing:0,pitch:0,pan:new e.P(0,0),pinchAround:void 0,around:void 0};for(const{settings:t}of this._inertiaBuffer)i.zoom+=t.zoomDelta||0,i.bearing+=t.bearingDelta||0,i.pitch+=t.pitchDelta||0,t.panDelta&&i.pan._add(t.panDelta),t.around&&(i.around=t.around),t.pinchAround&&(i.pinchAround=t.pinchAround);const a=this._inertiaBuffer[this._inertiaBuffer.length-1].time-this._inertiaBuffer[0].time,s={};if(i.pan.mag()){const o=Ra(i.pan.mag(),a,e.e({},Ca,t||{}));s.offset=i.pan.mult(o.amount/i.pan.mag()),s.center=this._map.transform.center,Aa(s,o);}if(i.zoom){const t=Ra(i.zoom,a,Sa);s.zoom=this._map.transform.zoom+t.amount,Aa(s,t);}if(i.bearing){const t=Ra(i.bearing,a,za);s.bearing=this._map.transform.bearing+e.ac(t.amount,-179,179),Aa(s,t);}if(i.pitch){const t=Ra(i.pitch,a,Da);s.pitch=this._map.transform.pitch+t.amount,Aa(s,t);}if(s.zoom||s.bearing){const t=void 0===i.pinchAround?i.around:i.pinchAround;s.around=t?this._map.unproject(t):this._map.getCenter();}return this.clear(),e.e(s,{noMoveStart:!0})}}function Aa(t,e){(!t.duration||t.durationi.unproject(t))),l=o.reduce(((t,e,i,a)=>t.add(e.div(a.length))),new e.P(0,0));super(t,{points:o,point:l,lngLats:n,lngLat:i.unproject(l),originalEvent:a}),this._defaultPrevented=!1;}}class Fa extends e.k{preventDefault(){this._defaultPrevented=!0;}get defaultPrevented(){return this._defaultPrevented}constructor(t,e,i){super(t,{originalEvent:i}),this._defaultPrevented=!1;}}class Ba{constructor(t,e){this._map=t,this._clickTolerance=e.clickTolerance;}reset(){delete this._mousedownPos;}wheel(t){return this._firePreventable(new Fa(t.type,this._map,t))}mousedown(t,e){return this._mousedownPos=e,this._firePreventable(new ka(t.type,this._map,t))}mouseup(t){this._map.fire(new ka(t.type,this._map,t));}click(t,e){this._mousedownPos&&this._mousedownPos.dist(e)>=this._clickTolerance||this._map.fire(new ka(t.type,this._map,t));}dblclick(t){return this._firePreventable(new ka(t.type,this._map,t))}mouseover(t){this._map.fire(new ka(t.type,this._map,t));}mouseout(t){this._map.fire(new ka(t.type,this._map,t));}touchstart(t){return this._firePreventable(new La(t.type,this._map,t))}touchmove(t){this._map.fire(new La(t.type,this._map,t));}touchend(t){this._map.fire(new La(t.type,this._map,t));}touchcancel(t){this._map.fire(new La(t.type,this._map,t));}_firePreventable(t){if(this._map.fire(t),t.defaultPrevented)return {}}isEnabled(){return !0}isActive(){return !1}enable(){}disable(){}}class Oa{constructor(t){this._map=t;}reset(){this._delayContextMenu=!1,this._ignoreContextMenu=!0,delete this._contextMenuEvent;}mousemove(t){this._map.fire(new ka(t.type,this._map,t));}mousedown(){this._delayContextMenu=!0,this._ignoreContextMenu=!1;}mouseup(){this._delayContextMenu=!1,this._contextMenuEvent&&(this._map.fire(new ka("contextmenu",this._map,this._contextMenuEvent)),delete this._contextMenuEvent);}contextmenu(t){this._delayContextMenu?this._contextMenuEvent=t:this._ignoreContextMenu||this._map.fire(new ka(t.type,this._map,t)),this._map.listens("contextmenu")&&t.preventDefault();}isEnabled(){return !0}isActive(){return !1}enable(){}disable(){}}class Na{constructor(t){this._map=t;}get transform(){return this._map._requestedCameraState||this._map.transform}get center(){return {lng:this.transform.center.lng,lat:this.transform.center.lat}}get zoom(){return this.transform.zoom}get pitch(){return this.transform.pitch}get bearing(){return this.transform.bearing}unproject(t){return this.transform.pointLocation(e.P.convert(t),this._map.terrain)}}class Ua{constructor(t,e){this._map=t,this._tr=new Na(t),this._el=t.getCanvasContainer(),this._container=t.getContainer(),this._clickTolerance=e.clickTolerance||1;}isEnabled(){return !!this._enabled}isActive(){return !!this._active}enable(){this.isEnabled()||(this._enabled=!0);}disable(){this.isEnabled()&&(this._enabled=!1);}mousedown(t,e){this.isEnabled()&&t.shiftKey&&0===t.button&&(r.disableDrag(),this._startPos=this._lastPos=e,this._active=!0);}mousemoveWindow(t,e){if(!this._active)return;const i=e;if(this._lastPos.equals(i)||!this._box&&i.dist(this._startPos)t.fitScreenCoordinates(a,s,this._tr.bearing,{linear:!0})};this._fireEvent("boxzoomcancel",t);}keydown(t){this._active&&27===t.keyCode&&(this.reset(),this._fireEvent("boxzoomcancel",t));}reset(){this._active=!1,this._container.classList.remove("maplibregl-crosshair"),this._box&&(r.remove(this._box),this._box=null),r.enableDrag(),delete this._startPos,delete this._lastPos;}_fireEvent(t,i){return this._map.fire(new e.k(t,{originalEvent:i}))}}function ja(t,e){if(t.length!==e.length)throw new Error(`The number of touches and points are not equal - touches ${t.length}, points ${e.length}`);const i={};for(let a=0;athis.numTouches)&&(this.aborted=!0),this.aborted||(void 0===this.startTime&&(this.startTime=t.timeStamp),a.length===this.numTouches&&(this.centroid=function(t){const i=new e.P(0,0);for(const e of t)i._add(e);return i.div(t.length)}(i),this.touches=ja(a,i)));}touchmove(t,e,i){if(this.aborted||!this.centroid)return;const a=ja(i,e);for(const t in this.touches){const e=a[t];(!e||e.dist(this.touches[t])>30)&&(this.aborted=!0);}}touchend(t,e,i){if((!this.centroid||t.timeStamp-this.startTime>500)&&(this.aborted=!0),0===i.length){const t=!this.aborted&&this.centroid;if(this.reset(),t)return t}}}class qa{constructor(t){this.singleTap=new Za(t),this.numTaps=t.numTaps,this.reset();}reset(){this.lastTime=1/0,delete this.lastTap,this.count=0,this.singleTap.reset();}touchstart(t,e,i){this.singleTap.touchstart(t,e,i);}touchmove(t,e,i){this.singleTap.touchmove(t,e,i);}touchend(t,e,i){const a=this.singleTap.touchend(t,e,i);if(a){const e=t.timeStamp-this.lastTime<500,i=!this.lastTap||this.lastTap.dist(a)<30;if(e&&i||this.reset(),this.count++,this.lastTime=t.timeStamp,this.lastTap=a,this.count===this.numTaps)return this.reset(),a}}}class Va{constructor(t){this._tr=new Na(t),this._zoomIn=new qa({numTouches:1,numTaps:2}),this._zoomOut=new qa({numTouches:2,numTaps:1}),this.reset();}reset(){this._active=!1,this._zoomIn.reset(),this._zoomOut.reset();}touchstart(t,e,i){this._zoomIn.touchstart(t,e,i),this._zoomOut.touchstart(t,e,i);}touchmove(t,e,i){this._zoomIn.touchmove(t,e,i),this._zoomOut.touchmove(t,e,i);}touchend(t,e,i){const a=this._zoomIn.touchend(t,e,i),s=this._zoomOut.touchend(t,e,i),o=this._tr;return a?(this._active=!0,t.preventDefault(),setTimeout((()=>this.reset()),0),{cameraAnimation:e=>e.easeTo({duration:300,zoom:o.zoom+1,around:o.unproject(a)},{originalEvent:t})}):s?(this._active=!0,t.preventDefault(),setTimeout((()=>this.reset()),0),{cameraAnimation:e=>e.easeTo({duration:300,zoom:o.zoom-1,around:o.unproject(s)},{originalEvent:t})}):void 0}touchcancel(){this.reset();}enable(){this._enabled=!0;}disable(){this._enabled=!1,this.reset();}isEnabled(){return this._enabled}isActive(){return this._active}}class Ga{constructor(t){this._enabled=!!t.enable,this._moveStateManager=t.moveStateManager,this._clickTolerance=t.clickTolerance||1,this._moveFunction=t.move,this._activateOnStart=!!t.activateOnStart,t.assignEvents(this),this.reset();}reset(t){this._active=!1,this._moved=!1,delete this._lastPoint,this._moveStateManager.endMove(t);}_move(...t){const e=this._moveFunction(...t);if(e.bearingDelta||e.pitchDelta||e.around||e.panDelta)return this._active=!0,e}dragStart(t,e){this.isEnabled()&&!this._lastPoint&&this._moveStateManager.isValidStartEvent(t)&&(this._moveStateManager.startMove(t),this._lastPoint=e.length?e[0]:e,this._activateOnStart&&this._lastPoint&&(this._active=!0));}dragMove(t,e){if(!this.isEnabled())return;const i=this._lastPoint;if(!i)return;if(t.preventDefault(),!this._moveStateManager.isValidMoveEvent(t))return void this.reset(t);const a=e.length?e[0]:e;return !this._moved&&a.dist(i){t.mousedown=t.dragStart,t.mousemoveWindow=t.dragMove,t.mouseup=t.dragEnd,t.contextmenu=t=>{t.preventDefault();};},Ka=({enable:t,clickTolerance:e,bearingDegreesPerPixelMoved:i=.8})=>{const a=new Wa({checkCorrectEvent:t=>0===r.mouseButton(t)&&t.ctrlKey||2===r.mouseButton(t)});return new Ga({clickTolerance:e,move:(t,e)=>({bearingDelta:(e.x-t.x)*i}),moveStateManager:a,enable:t,assignEvents:Xa})},Ja=({enable:t,clickTolerance:e,pitchDegreesPerPixelMoved:i=-.5})=>{const a=new Wa({checkCorrectEvent:t=>0===r.mouseButton(t)&&t.ctrlKey||2===r.mouseButton(t)});return new Ga({clickTolerance:e,move:(t,e)=>({pitchDelta:(e.y-t.y)*i}),moveStateManager:a,enable:t,assignEvents:Xa})};class Ya{constructor(t,e){this._clickTolerance=t.clickTolerance||1,this._map=e,this.reset();}reset(){this._active=!1,this._touches={},this._sum=new e.P(0,0);}_shouldBePrevented(t){return t<(this._map.cooperativeGestures.isEnabled()?2:1)}touchstart(t,e,i){return this._calculateTransform(t,e,i)}touchmove(t,e,i){if(this._active){if(!this._shouldBePrevented(i.length))return t.preventDefault(),this._calculateTransform(t,e,i);this._map.cooperativeGestures.notifyGestureBlocked("touch_pan",t);}}touchend(t,e,i){this._calculateTransform(t,e,i),this._active&&this._shouldBePrevented(i.length)&&this.reset();}touchcancel(){this.reset();}_calculateTransform(t,i,a){a.length>0&&(this._active=!0);const s=ja(a,i),o=new e.P(0,0),r=new e.P(0,0);let n=0;for(const t in s){const e=s[t],i=this._touches[t];i&&(o._add(e),r._add(e.sub(i)),n++,s[t]=e);}if(this._touches=s,this._shouldBePrevented(n)||!r.mag())return;const l=r.div(n);return this._sum._add(l),this._sum.mag()Math.abs(t.x)}class rs extends Qa{constructor(t){super(),this._currentTouchCount=0,this._map=t;}reset(){super.reset(),this._valid=void 0,delete this._firstMove,delete this._lastPoints;}touchstart(t,e,i){super.touchstart(t,e,i),this._currentTouchCount=i.length;}_start(t){this._lastPoints=t,os(t[0].sub(t[1]))&&(this._valid=!1);}_move(t,e,i){if(this._map.cooperativeGestures.isEnabled()&&this._currentTouchCount<3)return;const a=t[0].sub(this._lastPoints[0]),s=t[1].sub(this._lastPoints[1]);return this._valid=this.gestureBeginsVertically(a,s,i.timeStamp),this._valid?(this._lastPoints=t,this._active=!0,{pitchDelta:(a.y+s.y)/2*-.5}):void 0}gestureBeginsVertically(t,e,i){if(void 0!==this._valid)return this._valid;const a=t.mag()>=2,s=e.mag()>=2;if(!a&&!s)return;if(!a||!s)return void 0===this._firstMove&&(this._firstMove=i),i-this._firstMove<100&&void 0;const o=t.y>0==e.y>0;return os(t)&&os(e)&&o}}const ns={panStep:100,bearingStep:15,pitchStep:10};class ls{constructor(t){this._tr=new Na(t);const e=ns;this._panStep=e.panStep,this._bearingStep=e.bearingStep,this._pitchStep=e.pitchStep,this._rotationDisabled=!1;}reset(){this._active=!1;}keydown(t){if(t.altKey||t.ctrlKey||t.metaKey)return;let e=0,i=0,a=0,s=0,o=0;switch(t.keyCode){case 61:case 107:case 171:case 187:e=1;break;case 189:case 109:case 173:e=-1;break;case 37:t.shiftKey?i=-1:(t.preventDefault(),s=-1);break;case 39:t.shiftKey?i=1:(t.preventDefault(),s=1);break;case 38:t.shiftKey?a=1:(t.preventDefault(),o=-1);break;case 40:t.shiftKey?a=-1:(t.preventDefault(),o=1);break;default:return}return this._rotationDisabled&&(i=0,a=0),{cameraAnimation:r=>{const n=this._tr;r.easeTo({duration:300,easeId:"keyboardHandler",easing:hs,zoom:e?Math.round(n.zoom)+e*(t.shiftKey?2:1):n.zoom,bearing:n.bearing+i*this._bearingStep,pitch:n.pitch+a*this._pitchStep,offset:[-s*this._panStep,-o*this._panStep],center:n.center},{originalEvent:t});}}}enable(){this._enabled=!0;}disable(){this._enabled=!1,this.reset();}isEnabled(){return this._enabled}isActive(){return this._active}disableRotation(){this._rotationDisabled=!0;}enableRotation(){this._rotationDisabled=!1;}}function hs(t){return t*(2-t)}const cs=4.000244140625;class us{constructor(t,e){this._onTimeout=t=>{this._type="wheel",this._delta-=this._lastValue,this._active||this._start(t);},this._map=t,this._tr=new Na(t),this._triggerRenderFrame=e,this._delta=0,this._defaultZoomRate=.01,this._wheelZoomRate=.0022222222222222222;}setZoomRate(t){this._defaultZoomRate=t;}setWheelZoomRate(t){this._wheelZoomRate=t;}isEnabled(){return !!this._enabled}isActive(){return !!this._active||void 0!==this._finishTimeout}isZooming(){return !!this._zooming}enable(t){this.isEnabled()||(this._enabled=!0,this._aroundCenter=!!t&&"center"===t.around);}disable(){this.isEnabled()&&(this._enabled=!1);}_shouldBePrevented(t){return !!this._map.cooperativeGestures.isEnabled()&&!(t.ctrlKey||this._map.cooperativeGestures.isBypassed(t))}wheel(t){if(!this.isEnabled())return;if(this._shouldBePrevented(t))return void this._map.cooperativeGestures.notifyGestureBlocked("wheel_zoom",t);let e=t.deltaMode===WheelEvent.DOM_DELTA_LINE?40*t.deltaY:t.deltaY;const i=o.now(),a=i-(this._lastWheelEventTime||0);this._lastWheelEventTime=i,0!==e&&e%cs==0?this._type="wheel":0!==e&&Math.abs(e)<4?this._type="trackpad":a>400?(this._type=null,this._lastValue=e,this._timeout=setTimeout(this._onTimeout,40,t)):this._type||(this._type=Math.abs(a*e)<200?"trackpad":"wheel",this._timeout&&(clearTimeout(this._timeout),this._timeout=null,e+=this._lastValue)),t.shiftKey&&e&&(e/=4),this._type&&(this._lastWheelEvent=t,this._delta-=e,this._active||this._start(t)),t.preventDefault();}_start(t){if(!this._delta)return;this._frameId&&(this._frameId=null),this._active=!0,this.isZooming()||(this._zooming=!0),this._finishTimeout&&(clearTimeout(this._finishTimeout),delete this._finishTimeout);const i=r.mousePos(this._map.getCanvas(),t),a=this._tr;this._around=i.y>a.transform.height/2-a.transform.getHorizon()?e.N.convert(this._aroundCenter?a.center:a.unproject(i)):e.N.convert(a.center),this._aroundPoint=a.transform.locationPoint(this._around),this._frameId||(this._frameId=!0,this._triggerRenderFrame());}renderFrame(){if(!this._frameId)return;if(this._frameId=null,!this.isActive())return;const t=this._tr.transform;if(0!==this._delta){const e="wheel"===this._type&&Math.abs(this._delta)>cs?this._wheelZoomRate:this._defaultZoomRate;let i=2/(1+Math.exp(-Math.abs(this._delta*e)));this._delta<0&&0!==i&&(i=1/i);const a="number"==typeof this._targetZoom?t.zoomScale(this._targetZoom):t.scale;this._targetZoom=Math.min(t.maxZoom,Math.max(t.minZoom,t.scaleZoom(a*i))),"wheel"===this._type&&(this._startZoom=t.zoom,this._easing=this._smoothOutEasing(200)),this._delta=0;}const i="number"==typeof this._targetZoom?this._targetZoom:t.zoom,a=this._startZoom,s=this._easing;let r,n=!1;const l=o.now()-this._lastWheelEventTime;if("wheel"===this._type&&a&&s&&l){const t=Math.min(l/200,1),o=s(t);r=e.y.number(a,i,o),t<1?this._frameId||(this._frameId=!0):n=!0;}else r=i,n=!0;return this._active=!0,n&&(this._active=!1,this._finishTimeout=setTimeout((()=>{this._zooming=!1,this._triggerRenderFrame(),delete this._targetZoom,delete this._finishTimeout;}),200)),{noInertia:!0,needsRenderFrame:!n,zoomDelta:r-t.zoom,around:this._aroundPoint,originalEvent:this._lastWheelEvent}}_smoothOutEasing(t){let i=e.b9;if(this._prevEase){const t=this._prevEase,a=(o.now()-t.start)/t.duration,s=t.easing(a+.01)-t.easing(a),r=.27/Math.sqrt(s*s+1e-4)*.01,n=Math.sqrt(.0729-r*r);i=e.b8(r,n,.25,1);}return this._prevEase={start:o.now(),duration:t,easing:i},i}reset(){this._active=!1,this._zooming=!1,delete this._targetZoom,this._finishTimeout&&(clearTimeout(this._finishTimeout),delete this._finishTimeout);}}class ds{constructor(t,e){this._clickZoom=t,this._tapZoom=e;}enable(){this._clickZoom.enable(),this._tapZoom.enable();}disable(){this._clickZoom.disable(),this._tapZoom.disable();}isEnabled(){return this._clickZoom.isEnabled()&&this._tapZoom.isEnabled()}isActive(){return this._clickZoom.isActive()||this._tapZoom.isActive()}}class _s{constructor(t){this._tr=new Na(t),this.reset();}reset(){this._active=!1;}dblclick(t,e){return t.preventDefault(),{cameraAnimation:i=>{i.easeTo({duration:300,zoom:this._tr.zoom+(t.shiftKey?-1:1),around:this._tr.unproject(e)},{originalEvent:t});}}}enable(){this._enabled=!0;}disable(){this._enabled=!1,this.reset();}isEnabled(){return this._enabled}isActive(){return this._active}}class ps{constructor(){this._tap=new qa({numTouches:1,numTaps:1}),this.reset();}reset(){this._active=!1,delete this._swipePoint,delete this._swipeTouch,delete this._tapTime,delete this._tapPoint,this._tap.reset();}touchstart(t,e,i){if(!this._swipePoint)if(this._tapTime){const a=e[0],s=t.timeStamp-this._tapTime<500,o=this._tapPoint.dist(a)<30;s&&o?i.length>0&&(this._swipePoint=a,this._swipeTouch=i[0].identifier):this.reset();}else this._tap.touchstart(t,e,i);}touchmove(t,e,i){if(this._tapTime){if(this._swipePoint){if(i[0].identifier!==this._swipeTouch)return;const a=e[0],s=a.y-this._swipePoint.y;return this._swipePoint=a,t.preventDefault(),this._active=!0,{zoomDelta:s/128}}}else this._tap.touchmove(t,e,i);}touchend(t,e,i){if(this._tapTime)this._swipePoint&&0===i.length&&this.reset();else {const a=this._tap.touchend(t,e,i);a&&(this._tapTime=t.timeStamp,this._tapPoint=a);}}touchcancel(){this.reset();}enable(){this._enabled=!0;}disable(){this._enabled=!1,this.reset();}isEnabled(){return this._enabled}isActive(){return this._active}}class ms{constructor(t,e,i){this._el=t,this._mousePan=e,this._touchPan=i;}enable(t){this._inertiaOptions=t||{},this._mousePan.enable(),this._touchPan.enable(),this._el.classList.add("maplibregl-touch-drag-pan");}disable(){this._mousePan.disable(),this._touchPan.disable(),this._el.classList.remove("maplibregl-touch-drag-pan");}isEnabled(){return this._mousePan.isEnabled()&&this._touchPan.isEnabled()}isActive(){return this._mousePan.isActive()||this._touchPan.isActive()}}class fs{constructor(t,e,i){this._pitchWithRotate=t.pitchWithRotate,this._mouseRotate=e,this._mousePitch=i;}enable(){this._mouseRotate.enable(),this._pitchWithRotate&&this._mousePitch.enable();}disable(){this._mouseRotate.disable(),this._mousePitch.disable();}isEnabled(){return this._mouseRotate.isEnabled()&&(!this._pitchWithRotate||this._mousePitch.isEnabled())}isActive(){return this._mouseRotate.isActive()||this._mousePitch.isActive()}}class gs{constructor(t,e,i,a){this._el=t,this._touchZoom=e,this._touchRotate=i,this._tapDragZoom=a,this._rotationDisabled=!1,this._enabled=!0;}enable(t){this._touchZoom.enable(t),this._rotationDisabled||this._touchRotate.enable(t),this._tapDragZoom.enable(),this._el.classList.add("maplibregl-touch-zoom-rotate");}disable(){this._touchZoom.disable(),this._touchRotate.disable(),this._tapDragZoom.disable(),this._el.classList.remove("maplibregl-touch-zoom-rotate");}isEnabled(){return this._touchZoom.isEnabled()&&(this._rotationDisabled||this._touchRotate.isEnabled())&&this._tapDragZoom.isEnabled()}isActive(){return this._touchZoom.isActive()||this._touchRotate.isActive()||this._tapDragZoom.isActive()}disableRotation(){this._rotationDisabled=!0,this._touchRotate.disable();}enableRotation(){this._rotationDisabled=!1,this._touchZoom.isEnabled()&&this._touchRotate.enable();}}class vs{constructor(t,e){this._bypassKey=-1!==navigator.userAgent.indexOf("Mac")?"metaKey":"ctrlKey",this._map=t,this._options=e,this._enabled=!1;}isActive(){return !1}reset(){}_setupUI(){if(this._container)return;const t=this._map.getCanvasContainer();t.classList.add("maplibregl-cooperative-gestures"),this._container=r.create("div","maplibregl-cooperative-gesture-screen",t);let e=this._map._getUIString("CooperativeGesturesHandler.WindowsHelpText");"metaKey"===this._bypassKey&&(e=this._map._getUIString("CooperativeGesturesHandler.MacHelpText"));const i=this._map._getUIString("CooperativeGesturesHandler.MobileHelpText"),a=document.createElement("div");a.className="maplibregl-desktop-message",a.textContent=e,this._container.appendChild(a);const s=document.createElement("div");s.className="maplibregl-mobile-message",s.textContent=i,this._container.appendChild(s),this._container.setAttribute("aria-hidden","true");}_destroyUI(){this._container&&(r.remove(this._container),this._map.getCanvasContainer().classList.remove("maplibregl-cooperative-gestures")),delete this._container;}enable(){this._setupUI(),this._enabled=!0;}disable(){this._enabled=!1,this._destroyUI();}isEnabled(){return this._enabled}isBypassed(t){return t[this._bypassKey]}notifyGestureBlocked(t,i){this._enabled&&(this._map.fire(new e.k("cooperativegestureprevented",{gestureType:t,originalEvent:i})),this._container.classList.add("maplibregl-show"),setTimeout((()=>{this._container.classList.remove("maplibregl-show");}),100));}}const xs=t=>t.zoom||t.drag||t.pitch||t.rotate;class ys extends e.k{}function bs(t){return t.panDelta&&t.panDelta.mag()||t.zoomDelta||t.bearingDelta||t.pitchDelta}class ws{constructor(t,e){this.handleWindowEvent=t=>{this.handleEvent(t,`${t.type}Window`);},this.handleEvent=(t,e)=>{if("blur"===t.type)return void this.stop(!0);this._updatingCamera=!0;const i="renderFrame"===t.type?void 0:t,a={needsRenderFrame:!1},s={},o={},n=t.touches,l=n?this._getMapTouches(n):void 0,h=l?r.touchPos(this._map.getCanvas(),l):r.mousePos(this._map.getCanvas(),t);for(const{handlerName:r,handler:n,allowed:c}of this._handlers){if(!n.isEnabled())continue;let u;this._blockedByActive(o,c,r)?n.reset():n[e||t.type]&&(u=n[e||t.type](t,h,l),this.mergeHandlerResult(a,s,u,r,i),u&&u.needsRenderFrame&&this._triggerRenderFrame()),(u||n.isActive())&&(o[r]=n);}const c={};for(const t in this._previousActiveHandlers)o[t]||(c[t]=i);this._previousActiveHandlers=o,(Object.keys(c).length||bs(a))&&(this._changes.push([a,s,c]),this._triggerRenderFrame()),(Object.keys(o).length||bs(a))&&this._map._stop(!0),this._updatingCamera=!1;const{cameraAnimation:u}=a;u&&(this._inertia.clear(),this._fireEvents({},{},!0),this._changes=[],u(this._map));},this._map=t,this._el=this._map.getCanvasContainer(),this._handlers=[],this._handlersById={},this._changes=[],this._inertia=new Ma(t),this._bearingSnap=e.bearingSnap,this._previousActiveHandlers={},this._eventsInProgress={},this._addDefaultHandlers(e);const i=this._el;this._listeners=[[i,"touchstart",{passive:!0}],[i,"touchmove",{passive:!1}],[i,"touchend",void 0],[i,"touchcancel",void 0],[i,"mousedown",void 0],[i,"mousemove",void 0],[i,"mouseup",void 0],[document,"mousemove",{capture:!0}],[document,"mouseup",void 0],[i,"mouseover",void 0],[i,"mouseout",void 0],[i,"dblclick",void 0],[i,"click",void 0],[i,"keydown",{capture:!1}],[i,"keyup",void 0],[i,"wheel",{passive:!1}],[i,"contextmenu",void 0],[window,"blur",void 0]];for(const[t,e,i]of this._listeners)r.addEventListener(t,e,t===document?this.handleWindowEvent:this.handleEvent,i);}destroy(){for(const[t,e,i]of this._listeners)r.removeEventListener(t,e,t===document?this.handleWindowEvent:this.handleEvent,i);}_addDefaultHandlers(t){const e=this._map,i=e.getCanvasContainer();this._add("mapEvent",new Ba(e,t));const a=e.boxZoom=new Ua(e,t);this._add("boxZoom",a),t.interactive&&t.boxZoom&&a.enable();const s=e.cooperativeGestures=new vs(e,t.cooperativeGestures);this._add("cooperativeGestures",s),t.cooperativeGestures&&s.enable();const o=new Va(e),n=new _s(e);e.doubleClickZoom=new ds(n,o),this._add("tapZoom",o),this._add("clickZoom",n),t.interactive&&t.doubleClickZoom&&e.doubleClickZoom.enable();const l=new ps;this._add("tapDragZoom",l);const h=e.touchPitch=new rs(e);this._add("touchPitch",h),t.interactive&&t.touchPitch&&e.touchPitch.enable(t.touchPitch);const c=Ka(t),u=Ja(t);e.dragRotate=new fs(t,c,u),this._add("mouseRotate",c,["mousePitch"]),this._add("mousePitch",u,["mouseRotate"]),t.interactive&&t.dragRotate&&e.dragRotate.enable();const d=(({enable:t,clickTolerance:e})=>{const i=new Wa({checkCorrectEvent:t=>0===r.mouseButton(t)&&!t.ctrlKey});return new Ga({clickTolerance:e,move:(t,e)=>({around:e,panDelta:e.sub(t)}),activateOnStart:!0,moveStateManager:i,enable:t,assignEvents:Xa})})(t),_=new Ya(t,e);e.dragPan=new ms(i,d,_),this._add("mousePan",d),this._add("touchPan",_,["touchZoom","touchRotate"]),t.interactive&&t.dragPan&&e.dragPan.enable(t.dragPan);const p=new ss,m=new is;e.touchZoomRotate=new gs(i,m,p,l),this._add("touchRotate",p,["touchPan","touchZoom"]),this._add("touchZoom",m,["touchPan","touchRotate"]),t.interactive&&t.touchZoomRotate&&e.touchZoomRotate.enable(t.touchZoomRotate);const f=e.scrollZoom=new us(e,(()=>this._triggerRenderFrame()));this._add("scrollZoom",f,["mousePan"]),t.interactive&&t.scrollZoom&&e.scrollZoom.enable(t.scrollZoom);const g=e.keyboard=new ls(e);this._add("keyboard",g),t.interactive&&t.keyboard&&e.keyboard.enable(),this._add("blockableMapEvent",new Oa(e));}_add(t,e,i){this._handlers.push({handlerName:t,handler:e,allowed:i}),this._handlersById[t]=e;}stop(t){if(!this._updatingCamera){for(const{handler:t}of this._handlers)t.reset();this._inertia.clear(),this._fireEvents({},{},t),this._changes=[];}}isActive(){for(const{handler:t}of this._handlers)if(t.isActive())return !0;return !1}isZooming(){return !!this._eventsInProgress.zoom||this._map.scrollZoom.isZooming()}isRotating(){return !!this._eventsInProgress.rotate}isMoving(){return Boolean(xs(this._eventsInProgress))||this.isZooming()}_blockedByActive(t,e,i){for(const a in t)if(a!==i&&(!e||e.indexOf(a)<0))return !0;return !1}_getMapTouches(t){const e=[];for(const i of t)this._el.contains(i.target)&&e.push(i);return e}mergeHandlerResult(t,i,a,s,o){if(!a)return;e.e(t,a);const r={handlerName:s,originalEvent:a.originalEvent||o};void 0!==a.zoomDelta&&(i.zoom=r),void 0!==a.panDelta&&(i.drag=r),void 0!==a.pitchDelta&&(i.pitch=r),void 0!==a.bearingDelta&&(i.rotate=r);}_applyChanges(){const t={},i={},a={};for(const[s,o,r]of this._changes)s.panDelta&&(t.panDelta=(t.panDelta||new e.P(0,0))._add(s.panDelta)),s.zoomDelta&&(t.zoomDelta=(t.zoomDelta||0)+s.zoomDelta),s.bearingDelta&&(t.bearingDelta=(t.bearingDelta||0)+s.bearingDelta),s.pitchDelta&&(t.pitchDelta=(t.pitchDelta||0)+s.pitchDelta),void 0!==s.around&&(t.around=s.around),void 0!==s.pinchAround&&(t.pinchAround=s.pinchAround),s.noInertia&&(t.noInertia=s.noInertia),e.e(i,o),e.e(a,r);this._updateMapTransform(t,i,a),this._changes=[];}_updateMapTransform(t,e,i){const a=this._map,s=a._getTransformForUpdate(),o=a.terrain;if(!(bs(t)||o&&this._terrainMovement))return this._fireEvents(e,i,!0);let{panDelta:r,zoomDelta:n,bearingDelta:l,pitchDelta:h,around:c,pinchAround:u}=t;void 0!==u&&(c=u),a._stop(!0),c=c||a.transform.centerPoint;const d=s.pointLocation(r?c.sub(r):c);l&&(s.bearing+=l),h&&(s.pitch+=h),n&&(s.zoom+=n),o?this._terrainMovement||!e.drag&&!e.zoom?e.drag&&this._terrainMovement?s.center=s.pointLocation(s.centerPoint.sub(r)):s.setLocationAtPoint(d,c):(this._terrainMovement=!0,this._map._elevationFreeze=!0,s.setLocationAtPoint(d,c)):s.setLocationAtPoint(d,c),a._applyUpdatedTransform(s),this._map._update(),t.noInertia||this._inertia.record(t),this._fireEvents(e,i,!0);}_fireEvents(t,i,a){const s=xs(this._eventsInProgress),r=xs(t),n={};for(const e in t){const{originalEvent:i}=t[e];this._eventsInProgress[e]||(n[`${e}start`]=i),this._eventsInProgress[e]=t[e];}!s&&r&&this._fireEvent("movestart",r.originalEvent);for(const t in n)this._fireEvent(t,n[t]);r&&this._fireEvent("move",r.originalEvent);for(const e in t){const{originalEvent:i}=t[e];this._fireEvent(e,i);}const l={};let h;for(const t in this._eventsInProgress){const{handlerName:e,originalEvent:a}=this._eventsInProgress[t];this._handlersById[e].isActive()||(delete this._eventsInProgress[t],h=i[e]||a,l[`${t}end`]=h);}for(const t in l)this._fireEvent(t,l[t]);const c=xs(this._eventsInProgress),u=(s||r)&&!c;if(u&&this._terrainMovement){this._map._elevationFreeze=!1,this._terrainMovement=!1;const t=this._map._getTransformForUpdate();t.recalculateZoom(this._map.terrain),this._map._applyUpdatedTransform(t);}if(a&&u){this._updatingCamera=!0;const t=this._inertia._onMoveEnd(this._map.dragPan._inertiaOptions),i=t=>0!==t&&-this._bearingSnap{delete this._frameId,this.handleEvent(new ys("renderFrame",{timeStamp:t})),this._applyChanges();}))}_triggerRenderFrame(){void 0===this._frameId&&(this._frameId=this._requestFrame());}}class Ts extends e.E{constructor(t,e){super(),this._renderFrameCallback=()=>{const t=Math.min((o.now()-this._easeStart)/this._easeOptions.duration,1);this._onEaseFrame(this._easeOptions.easing(t)),t<1&&this._easeFrameId?this._easeFrameId=this._requestRenderFrame(this._renderFrameCallback):this.stop();},this._moving=!1,this._zooming=!1,this.transform=t,this._bearingSnap=e.bearingSnap,this.on("moveend",(()=>{delete this._requestedCameraState;}));}getCenter(){return new e.N(this.transform.center.lng,this.transform.center.lat)}setCenter(t,e){return this.jumpTo({center:t},e)}panBy(t,i,a){return t=e.P.convert(t).mult(-1),this.panTo(this.transform.center,e.e({offset:t},i),a)}panTo(t,i,a){return this.easeTo(e.e({center:t},i),a)}getZoom(){return this.transform.zoom}setZoom(t,e){return this.jumpTo({zoom:t},e),this}zoomTo(t,i,a){return this.easeTo(e.e({zoom:t},i),a)}zoomIn(t,e){return this.zoomTo(this.getZoom()+1,t,e),this}zoomOut(t,e){return this.zoomTo(this.getZoom()-1,t,e),this}getBearing(){return this.transform.bearing}setBearing(t,e){return this.jumpTo({bearing:t},e),this}getPadding(){return this.transform.padding}setPadding(t,e){return this.jumpTo({padding:t},e),this}rotateTo(t,i,a){return this.easeTo(e.e({bearing:t},i),a)}resetNorth(t,i){return this.rotateTo(0,e.e({duration:1e3},t),i),this}resetNorthPitch(t,i){return this.easeTo(e.e({bearing:0,pitch:0,duration:1e3},t),i),this}snapToNorth(t,e){return Math.abs(this.getBearing()){if(this._zooming&&(s.zoom=e.y.number(r,g,a)),this._rotating&&(s.bearing=e.y.number(n,c,a)),this._pitching&&(s.pitch=e.y.number(l,u,a)),this._padding&&(s.interpolatePadding(h,d,a),p=s.centerPoint.add(_)),this.terrain&&!t.freezeElevation&&this._updateElevation(a),b)s.setLocationAtPoint(b,w);else {const t=s.zoomScale(s.zoom-r),e=g>r?Math.min(2,y):Math.max(.5,y),i=Math.pow(e,1-a),o=s.unproject(v.add(x.mult(a*i)).mult(t));s.setLocationAtPoint(s.renderWorldCopies?o.wrap():o,p);}this._applyUpdatedTransform(s),this._fireMoveEvents(i);}),(e=>{this.terrain&&t.freezeElevation&&this._finalizeElevation(),this._afterEase(i,e);}),t),this}_prepareEase(t,i,a={}){this._moving=!0,i||a.moving||this.fire(new e.k("movestart",t)),this._zooming&&!a.zooming&&this.fire(new e.k("zoomstart",t)),this._rotating&&!a.rotating&&this.fire(new e.k("rotatestart",t)),this._pitching&&!a.pitching&&this.fire(new e.k("pitchstart",t));}_prepareElevation(t){this._elevationCenter=t,this._elevationStart=this.transform.elevation,this._elevationTarget=this.terrain.getElevationForLngLatZoom(t,this.transform.tileZoom),this._elevationFreeze=!0;}_updateElevation(t){this.transform.minElevationForCurrentTile=this.terrain.getMinTileElevationForLngLatZoom(this._elevationCenter,this.transform.tileZoom);const i=this.terrain.getElevationForLngLatZoom(this._elevationCenter,this.transform.tileZoom);if(t<1&&i!==this._elevationTarget){const e=this._elevationTarget-this._elevationStart;this._elevationStart+=t*(e-(i-(e*t+this._elevationStart))/(1-t)),this._elevationTarget=i;}this.transform.elevation=e.y.number(this._elevationStart,this._elevationTarget,t);}_finalizeElevation(){this._elevationFreeze=!1,this.transform.recalculateZoom(this.terrain);}_getTransformForUpdate(){return this.transformCameraUpdate||this.terrain?(this._requestedCameraState||(this._requestedCameraState=this.transform.clone()),this._requestedCameraState):this.transform}_elevateCameraIfInsideTerrain(t){const e=t.getCameraPosition(),i=this.terrain.getElevationForLngLatZoom(e.lngLat,t.zoom);if(e.altitudethis._elevateCameraIfInsideTerrain(t))),this.transformCameraUpdate&&e.push((t=>this.transformCameraUpdate(t))),!e.length)return;const i=t.clone();for(const t of e){const e=i.clone(),{center:a,zoom:s,pitch:o,bearing:r,elevation:n}=t(e);a&&(e.center=a),void 0!==s&&(e.zoom=s),void 0!==o&&(e.pitch=o),void 0!==r&&(e.bearing=r),void 0!==n&&(e.elevation=n),i.apply(e);}this.transform.apply(i);}_fireMoveEvents(t){this.fire(new e.k("move",t)),this._zooming&&this.fire(new e.k("zoom",t)),this._rotating&&this.fire(new e.k("rotate",t)),this._pitching&&this.fire(new e.k("pitch",t));}_afterEase(t,i){if(this._easeId&&i&&this._easeId===i)return;delete this._easeId;const a=this._zooming,s=this._rotating,o=this._pitching;this._moving=!1,this._zooming=!1,this._rotating=!1,this._pitching=!1,this._padding=!1,a&&this.fire(new e.k("zoomend",t)),s&&this.fire(new e.k("rotateend",t)),o&&this.fire(new e.k("pitchend",t)),this.fire(new e.k("moveend",t));}flyTo(t,i){var a;if(!t.essential&&o.prefersReducedMotion){const a=e.M(t,["center","zoom","bearing","pitch","around"]);return this.jumpTo(a,i)}this.stop(),t=e.e({offset:[0,0],speed:1.2,curve:1.42,easing:e.b9},t);const s=this._getTransformForUpdate(),r=s.zoom,n=s.bearing,l=s.pitch,h=s.padding,c="bearing"in t?this._normalizeBearing(t.bearing,n):n,u="pitch"in t?+t.pitch:l,d="padding"in t?t.padding:s.padding,_=e.P.convert(t.offset);let p=s.centerPoint.add(_);const m=s.pointLocation(p),{center:f,zoom:g}=s.getConstrained(e.N.convert(t.center||m),null!==(a=t.zoom)&&void 0!==a?a:r);this._normalizeCenter(f,s);const v=s.zoomScale(g-r),x=s.project(m),y=s.project(f).sub(x);let b=t.curve;const w=Math.max(s.width,s.height),T=w/v,I=y.mag();if("minZoom"in t){const i=e.ac(Math.min(t.minZoom,r,g),s.minZoom,s.maxZoom),a=w/s.zoomScale(i-r);b=Math.sqrt(a/I*2);}const E=b*b;function P(t){const e=(T*T-w*w+(t?-1:1)*E*E*I*I)/(2*(t?T:w)*E*I);return Math.log(Math.sqrt(e*e+1)-e)}function C(t){return (Math.exp(t)-Math.exp(-t))/2}function S(t){return (Math.exp(t)+Math.exp(-t))/2}const z=P(!1);let D=function(t){return S(z)/S(z+b*t)},M=function(t){return w*((S(z)*(C(e=z+b*t)/S(e))-C(z))/E)/I;var e;},A=(P(!0)-z)/b;if(Math.abs(I)<1e-6||!isFinite(A)){if(Math.abs(w-T)<1e-6)return this.easeTo(t,i);const e=T0,D=t=>Math.exp(e*b*t);}return t.duration="duration"in t?+t.duration:1e3*A/("screenSpeed"in t?+t.screenSpeed/b:+t.speed),t.maxDuration&&t.duration>t.maxDuration&&(t.duration=0),this._zooming=!0,this._rotating=n!==c,this._pitching=u!==l,this._padding=!s.isPaddingEqual(d),this._prepareEase(i,!1),this.terrain&&this._prepareElevation(f),this._ease((a=>{const o=a*A,m=1/D(o);s.zoom=1===a?g:r+s.scaleZoom(m),this._rotating&&(s.bearing=e.y.number(n,c,a)),this._pitching&&(s.pitch=e.y.number(l,u,a)),this._padding&&(s.interpolatePadding(h,d,a),p=s.centerPoint.add(_)),this.terrain&&!t.freezeElevation&&this._updateElevation(a);const v=1===a?f:s.unproject(x.add(y.mult(M(o))).mult(m));s.setLocationAtPoint(s.renderWorldCopies?v.wrap():v,p),this._applyUpdatedTransform(s),this._fireMoveEvents(i);}),(()=>{this.terrain&&t.freezeElevation&&this._finalizeElevation(),this._afterEase(i);}),t),this}isEasing(){return !!this._easeFrameId}stop(){return this._stop()}_stop(t,e){var i;if(this._easeFrameId&&(this._cancelRenderFrame(this._easeFrameId),delete this._easeFrameId,delete this._onEaseFrame),this._onEaseEnd){const t=this._onEaseEnd;delete this._onEaseEnd,t.call(this,e);}return t||null===(i=this.handlers)||void 0===i||i.stop(!1),this}_ease(t,e,i){!1===i.animate||0===i.duration?(t(1),e()):(this._easeStart=o.now(),this._easeOptions=i,this._onEaseFrame=t,this._onEaseEnd=e,this._easeFrameId=this._requestRenderFrame(this._renderFrameCallback));}_normalizeBearing(t,i){t=e.b3(t,-180,180);const a=Math.abs(t-i);return Math.abs(t-360-i)180?-360:i<-180?360:0;}queryTerrainElevation(t){return this.terrain?this.terrain.getElevationForLngLatZoom(e.N.convert(t),this.transform.tileZoom)-this.transform.elevation:null}}const Is={compact:!0,customAttribution:'MapLibre'};class Es{constructor(t=Is){this._toggleAttribution=()=>{this._container.classList.contains("maplibregl-compact")&&(this._container.classList.contains("maplibregl-compact-show")?(this._container.setAttribute("open",""),this._container.classList.remove("maplibregl-compact-show")):(this._container.classList.add("maplibregl-compact-show"),this._container.removeAttribute("open")));},this._updateData=t=>{!t||"metadata"!==t.sourceDataType&&"visibility"!==t.sourceDataType&&"style"!==t.dataType&&"terrain"!==t.type||this._updateAttributions();},this._updateCompact=()=>{this._map.getCanvasContainer().offsetWidth<=640||this._compact?!1===this._compact?this._container.setAttribute("open",""):this._container.classList.contains("maplibregl-compact")||this._container.classList.contains("maplibregl-attrib-empty")||(this._container.setAttribute("open",""),this._container.classList.add("maplibregl-compact","maplibregl-compact-show")):(this._container.setAttribute("open",""),this._container.classList.contains("maplibregl-compact")&&this._container.classList.remove("maplibregl-compact","maplibregl-compact-show"));},this._updateCompactMinimize=()=>{this._container.classList.contains("maplibregl-compact")&&this._container.classList.contains("maplibregl-compact-show")&&this._container.classList.remove("maplibregl-compact-show");},this.options=t;}getDefaultPosition(){return "bottom-right"}onAdd(t){return this._map=t,this._compact=this.options.compact,this._container=r.create("details","maplibregl-ctrl maplibregl-ctrl-attrib"),this._compactButton=r.create("summary","maplibregl-ctrl-attrib-button",this._container),this._compactButton.addEventListener("click",this._toggleAttribution),this._setElementTitle(this._compactButton,"ToggleAttribution"),this._innerContainer=r.create("div","maplibregl-ctrl-attrib-inner",this._container),this._updateAttributions(),this._updateCompact(),this._map.on("styledata",this._updateData),this._map.on("sourcedata",this._updateData),this._map.on("terrain",this._updateData),this._map.on("resize",this._updateCompact),this._map.on("drag",this._updateCompactMinimize),this._container}onRemove(){r.remove(this._container),this._map.off("styledata",this._updateData),this._map.off("sourcedata",this._updateData),this._map.off("terrain",this._updateData),this._map.off("resize",this._updateCompact),this._map.off("drag",this._updateCompactMinimize),this._map=void 0,this._compact=void 0,this._attribHTML=void 0;}_setElementTitle(t,e){const i=this._map._getUIString(`AttributionControl.${e}`);t.title=i,t.setAttribute("aria-label",i);}_updateAttributions(){if(!this._map.style)return;let t=[];if(this.options.customAttribution&&(Array.isArray(this.options.customAttribution)?t=t.concat(this.options.customAttribution.map((t=>"string"!=typeof t?"":t))):"string"==typeof this.options.customAttribution&&t.push(this.options.customAttribution)),this._map.style.stylesheet){const t=this._map.style.stylesheet;this.styleOwner=t.owner,this.styleId=t.id;}const e=this._map.style.sourceCaches;for(const i in e){const a=e[i];if(a.used||a.usedForTerrain){const e=a.getSource();e.attribution&&t.indexOf(e.attribution)<0&&t.push(e.attribution);}}t=t.filter((t=>String(t).trim())),t.sort(((t,e)=>t.length-e.length)),t=t.filter(((e,i)=>{for(let a=i+1;a=0)return !1;return !0}));const i=t.join(" | ");i!==this._attribHTML&&(this._attribHTML=i,t.length?(this._innerContainer.innerHTML=i,this._container.classList.remove("maplibregl-attrib-empty")):this._container.classList.add("maplibregl-attrib-empty"),this._updateCompact(),this._editLink=null);}}class Ps{constructor(t={}){this._updateCompact=()=>{const t=this._container.children;if(t.length){const e=t[0];this._map.getCanvasContainer().offsetWidth<=640||this._compact?!1!==this._compact&&e.classList.add("maplibregl-compact"):e.classList.remove("maplibregl-compact");}},this.options=t;}getDefaultPosition(){return "bottom-left"}onAdd(t){this._map=t,this._compact=this.options&&this.options.compact,this._container=r.create("div","maplibregl-ctrl");const e=r.create("a","maplibregl-ctrl-logo");return e.target="_blank",e.rel="noopener nofollow",e.href="https://maplibre.org/",e.setAttribute("aria-label",this._map._getUIString("LogoControl.Title")),e.setAttribute("rel","noopener nofollow"),this._container.appendChild(e),this._container.style.display="block",this._map.on("resize",this._updateCompact),this._updateCompact(),this._container}onRemove(){r.remove(this._container),this._map.off("resize",this._updateCompact),this._map=void 0,this._compact=void 0;}}class Cs{constructor(){this._queue=[],this._id=0,this._cleared=!1,this._currentlyRunning=!1;}add(t){const e=++this._id;return this._queue.push({callback:t,id:e,cancelled:!1}),e}remove(t){const e=this._currentlyRunning,i=e?this._queue.concat(e):this._queue;for(const e of i)if(e.id===t)return void(e.cancelled=!0)}run(t=0){if(this._currentlyRunning)throw new Error("Attempting to run(), but is already running.");const e=this._currentlyRunning=this._queue;this._queue=[];for(const i of e)if(!i.cancelled&&(i.callback(t),this._cleared))break;this._cleared=!1,this._currentlyRunning=!1;}clear(){this._currentlyRunning&&(this._cleared=!0),this._queue=[];}}var Ss=e.Y([{name:"a_pos3d",type:"Int16",components:3}]);class zs extends e.E{constructor(t){super(),this.sourceCache=t,this._tiles={},this._renderableTilesKeys=[],this._sourceTileCache={},this.minzoom=0,this.maxzoom=22,this.tileSize=512,this.deltaZoom=1,t.usedForTerrain=!0,t.tileSize=this.tileSize*2**this.deltaZoom;}destruct(){this.sourceCache.usedForTerrain=!1,this.sourceCache.tileSize=null;}update(t,i){this.sourceCache.update(t,i),this._renderableTilesKeys=[];const a={};for(const s of t.coveringTiles({tileSize:this.tileSize,minzoom:this.minzoom,maxzoom:this.maxzoom,reparseOverscaled:!1,terrain:i}))a[s.key]=!0,this._renderableTilesKeys.push(s.key),this._tiles[s.key]||(s.posMatrix=new Float64Array(16),e.aP(s.posMatrix,0,e.X,0,e.X,0,1),this._tiles[s.key]=new lt(s,this.tileSize));for(const t in this._tiles)a[t]||delete this._tiles[t];}freeRtt(t){for(const e in this._tiles){const i=this._tiles[e];(!t||i.tileID.equals(t)||i.tileID.isChildOf(t)||t.isChildOf(i.tileID))&&(i.rtt=[]);}}getRenderableTiles(){return this._renderableTilesKeys.map((t=>this.getTileByID(t)))}getTileByID(t){return this._tiles[t]}getTerrainCoords(t){const i={};for(const a of this._renderableTilesKeys){const s=this._tiles[a].tileID;if(s.canonical.equals(t.canonical)){const s=t.clone();s.posMatrix=new Float64Array(16),e.aP(s.posMatrix,0,e.X,0,e.X,0,1),i[a]=s;}else if(s.canonical.isChildOf(t.canonical)){const o=t.clone();o.posMatrix=new Float64Array(16);const r=s.canonical.z-t.canonical.z,n=s.canonical.x-(s.canonical.x>>r<>r<>r;e.aP(o.posMatrix,0,h,0,h,0,1),e.J(o.posMatrix,o.posMatrix,[-n*h,-l*h,0]),i[a]=o;}else if(t.canonical.isChildOf(s.canonical)){const o=t.clone();o.posMatrix=new Float64Array(16);const r=t.canonical.z-s.canonical.z,n=t.canonical.x-(t.canonical.x>>r<>r<>r;e.aP(o.posMatrix,0,e.X,0,e.X,0,1),e.J(o.posMatrix,o.posMatrix,[n*h,l*h,0]),e.K(o.posMatrix,o.posMatrix,[1/2**r,1/2**r,0]),i[a]=o;}}return i}getSourceTile(t,e){const i=this.sourceCache._source;let a=t.overscaledZ-this.deltaZoom;if(a>i.maxzoom&&(a=i.maxzoom),a=i.minzoom&&(!s||!s.dem);)s=this.sourceCache.getTileByID(t.scaledTo(a--).key);return s}tilesAfterTime(t=Date.now()){return Object.values(this._tiles).filter((e=>e.timeAdded>=t))}}class Ds{constructor(t,e,i){this.painter=t,this.sourceCache=new zs(e),this.options=i,this.exaggeration="number"==typeof i.exaggeration?i.exaggeration:1,this.qualityFactor=2,this.meshSize=128,this._demMatrixCache={},this.coordsIndex=[],this._coordsTextureSize=1024;}getDEMElevation(t,i,a,s=e.X){var o;if(!(i>=0&&i=0&&at.canonical.z&&(t.canonical.z>=a?s=t.canonical.z-a:e.w("cannot calculate elevation if elevation maxzoom > source.maxzoom"));const o=t.canonical.x-(t.canonical.x>>s<>s<>8<<4|t>>8,i[e+3]=0;const a=new e.R({width:this._coordsTextureSize,height:this._coordsTextureSize},new Uint8Array(i.buffer)),s=new b(t,a,t.gl.RGBA,{premultiply:!1});return s.bind(t.gl.NEAREST,t.gl.CLAMP_TO_EDGE),this._coordsTexture=s,s}pointCoordinate(t){this.painter.maybeDrawDepthAndCoords(!0);const i=new Uint8Array(4),a=this.painter.context,s=a.gl,o=Math.round(t.x*this.painter.pixelRatio/devicePixelRatio),r=Math.round(t.y*this.painter.pixelRatio/devicePixelRatio),n=Math.round(this.painter.height/devicePixelRatio);a.bindFramebuffer.set(this.getFramebuffer("coords").framebuffer),s.readPixels(o,n-r-1,1,1,s.RGBA,s.UNSIGNED_BYTE,i),a.bindFramebuffer.set(null);const l=i[0]+(i[2]>>4<<8),h=i[1]+((15&i[2])<<8),c=this.coordsIndex[255-i[3]],u=c&&this.sourceCache.getTileByID(c);if(!u)return null;const d=this._coordsTextureSize,_=(1<t.id!==e)),this._recentlyUsed.push(t.id);}stampObject(t){t.stamp=++this._stamp;}getOrCreateFreeObject(){for(const t of this._recentlyUsed)if(!this._objects[t].inUse)return this._objects[t];if(this._objects.length>=this._size)throw new Error("No free RenderPool available, call freeAllObjects() required!");const t=this._createObject(this._objects.length);return this._objects.push(t),t}freeObject(t){t.inUse=!1;}freeAllObjects(){for(const t of this._objects)this.freeObject(t);}isFull(){return !(this._objects.length!t.inUse))}}const As={background:!0,fill:!0,line:!0,raster:!0,hillshade:!0};class Rs{constructor(t,e){this.painter=t,this.terrain=e,this.pool=new Ms(t.context,30,e.sourceCache.tileSize*e.qualityFactor);}destruct(){this.pool.destruct();}getTexture(t){return this.pool.getObjectForId(t.rtt[this._stacks.length-1].id).texture}prepareForRender(t,e){this._stacks=[],this._prevType=null,this._rttTiles=[],this._renderableTiles=this.terrain.sourceCache.getRenderableTiles(),this._renderableLayerIds=t._order.filter((i=>!t._layers[i].isHidden(e))),this._coordsDescendingInv={};for(const e in t.sourceCaches){this._coordsDescendingInv[e]={};const i=t.sourceCaches[e].getVisibleCoordinates();for(const t of i){const i=this.terrain.sourceCache.getTerrainCoords(t);for(const t in i)this._coordsDescendingInv[e][t]||(this._coordsDescendingInv[e][t]=[]),this._coordsDescendingInv[e][t].push(i[t]);}}this._coordsDescendingInvStr={};for(const e of t._order){const i=t._layers[e],a=i.source;if(As[i.type]&&!this._coordsDescendingInvStr[a]){this._coordsDescendingInvStr[a]={};for(const t in this._coordsDescendingInv[a])this._coordsDescendingInvStr[a][t]=this._coordsDescendingInv[a][t].map((t=>t.key)).sort().join();}}for(const t of this._renderableTiles)for(const e in this._coordsDescendingInvStr){const i=this._coordsDescendingInvStr[e][t.tileID.key];i&&i!==t.rttCoords[e]&&(t.rtt=[]);}}renderLayer(t){if(t.isHidden(this.painter.transform.zoom))return !1;const i=t.type,a=this.painter,s=this._renderableLayerIds[this._renderableLayerIds.length-1]===t.id;if(As[i]&&(this._prevType&&As[this._prevType]||this._stacks.push([]),this._prevType=i,this._stacks[this._stacks.length-1].push(t.id),!s))return !0;if(As[this._prevType]||As[i]&&s){this._prevType=i;const t=this._stacks.length-1,s=this._stacks[t]||[];for(const i of this._renderableTiles){if(this.pool.isFull()&&(fa(this.painter,this.terrain,this._rttTiles),this._rttTiles=[],this.pool.freeAllObjects()),this._rttTiles.push(i),i.rtt[t]){const e=this.pool.getObjectForId(i.rtt[t].id);if(e.stamp===i.rtt[t].stamp){this.pool.useObject(e);continue}}const o=this.pool.getOrCreateFreeObject();this.pool.useObject(o),this.pool.stampObject(o),i.rtt[t]={id:o.id,stamp:o.stamp},a.context.bindFramebuffer.set(o.fbo.framebuffer),a.context.clear({color:e.aM.transparent,stencil:0}),a.currentStencilSource=void 0;for(let t=0;t{t.touchstart=t.dragStart,t.touchmoveWindow=t.dragMove,t.touchend=t.dragEnd;},Os={showCompass:!0,showZoom:!0,visualizePitch:!1};class Ns{constructor(t,i,a=!1){this.mousedown=t=>{this.startMouse(e.e({},t,{ctrlKey:!0,preventDefault:()=>t.preventDefault()}),r.mousePos(this.element,t)),r.addEventListener(window,"mousemove",this.mousemove),r.addEventListener(window,"mouseup",this.mouseup);},this.mousemove=t=>{this.moveMouse(t,r.mousePos(this.element,t));},this.mouseup=t=>{this.mouseRotate.dragEnd(t),this.mousePitch&&this.mousePitch.dragEnd(t),this.offTemp();},this.touchstart=t=>{1!==t.targetTouches.length?this.reset():(this._startPos=this._lastPos=r.touchPos(this.element,t.targetTouches)[0],this.startTouch(t,this._startPos),r.addEventListener(window,"touchmove",this.touchmove,{passive:!1}),r.addEventListener(window,"touchend",this.touchend));},this.touchmove=t=>{1!==t.targetTouches.length?this.reset():(this._lastPos=r.touchPos(this.element,t.targetTouches)[0],this.moveTouch(t,this._lastPos));},this.touchend=t=>{0===t.targetTouches.length&&this._startPos&&this._lastPos&&this._startPos.dist(this._lastPos){this.mouseRotate.reset(),this.mousePitch&&this.mousePitch.reset(),this.touchRotate.reset(),this.touchPitch&&this.touchPitch.reset(),delete this._startPos,delete this._lastPos,this.offTemp();},this._clickTolerance=10;const s=t.dragRotate._mouseRotate.getClickTolerance(),o=t.dragRotate._mousePitch.getClickTolerance();this.element=i,this.mouseRotate=Ka({clickTolerance:s,enable:!0}),this.touchRotate=(({enable:t,clickTolerance:e,bearingDegreesPerPixelMoved:i=.8})=>{const a=new $a;return new Ga({clickTolerance:e,move:(t,e)=>({bearingDelta:(e.x-t.x)*i}),moveStateManager:a,enable:t,assignEvents:Bs})})({clickTolerance:s,enable:!0}),this.map=t,a&&(this.mousePitch=Ja({clickTolerance:o,enable:!0}),this.touchPitch=(({enable:t,clickTolerance:e,pitchDegreesPerPixelMoved:i=-.5})=>{const a=new $a;return new Ga({clickTolerance:e,move:(t,e)=>({pitchDelta:(e.y-t.y)*i}),moveStateManager:a,enable:t,assignEvents:Bs})})({clickTolerance:o,enable:!0})),r.addEventListener(i,"mousedown",this.mousedown),r.addEventListener(i,"touchstart",this.touchstart,{passive:!1}),r.addEventListener(i,"touchcancel",this.reset);}startMouse(t,e){this.mouseRotate.dragStart(t,e),this.mousePitch&&this.mousePitch.dragStart(t,e),r.disableDrag();}startTouch(t,e){this.touchRotate.dragStart(t,e),this.touchPitch&&this.touchPitch.dragStart(t,e),r.disableDrag();}moveMouse(t,e){const i=this.map,{bearingDelta:a}=this.mouseRotate.dragMove(t,e)||{};if(a&&i.setBearing(i.getBearing()+a),this.mousePitch){const{pitchDelta:a}=this.mousePitch.dragMove(t,e)||{};a&&i.setPitch(i.getPitch()+a);}}moveTouch(t,e){const i=this.map,{bearingDelta:a}=this.touchRotate.dragMove(t,e)||{};if(a&&i.setBearing(i.getBearing()+a),this.touchPitch){const{pitchDelta:a}=this.touchPitch.dragMove(t,e)||{};a&&i.setPitch(i.getPitch()+a);}}off(){const t=this.element;r.removeEventListener(t,"mousedown",this.mousedown),r.removeEventListener(t,"touchstart",this.touchstart,{passive:!1}),r.removeEventListener(window,"touchmove",this.touchmove,{passive:!1}),r.removeEventListener(window,"touchend",this.touchend),r.removeEventListener(t,"touchcancel",this.reset),this.offTemp();}offTemp(){r.enableDrag(),r.removeEventListener(window,"mousemove",this.mousemove),r.removeEventListener(window,"mouseup",this.mouseup),r.removeEventListener(window,"touchmove",this.touchmove,{passive:!1}),r.removeEventListener(window,"touchend",this.touchend);}}let Us;function js(t,i,a){const s=new e.N(t.lng,t.lat);if(t=new e.N(t.lng,t.lat),i){const s=new e.N(t.lng-360,t.lat),o=new e.N(t.lng+360,t.lat),r=a.locationPoint(t).distSqr(i);a.locationPoint(s).distSqr(i)180;){const e=a.locationPoint(t);if(e.x>=0&&e.y>=0&&e.x<=a.width&&e.y<=a.height)break;t.lng>a.center.lng?t.lng-=360:t.lng+=360;}return t.lng!==s.lng&&a.locationPoint(t).y>a.height/2-a.getHorizon()?t:s}const Zs={center:"translate(-50%,-50%)",top:"translate(-50%,0)","top-left":"translate(0,0)","top-right":"translate(-100%,0)",bottom:"translate(-50%,-100%)","bottom-left":"translate(0,-100%)","bottom-right":"translate(-100%,-100%)",left:"translate(0,-50%)",right:"translate(-100%,-50%)"};function qs(t,e,i){const a=t.classList;for(const t in Zs)a.remove(`maplibregl-${i}-anchor-${t}`);a.add(`maplibregl-${i}-anchor-${e}`);}class Vs extends e.E{constructor(t){if(super(),this._onKeyPress=t=>{const e=t.code,i=t.charCode||t.keyCode;"Space"!==e&&"Enter"!==e&&32!==i&&13!==i||this.togglePopup();},this._onMapClick=t=>{const e=t.originalEvent.target,i=this._element;this._popup&&(e===i||i.contains(e))&&this.togglePopup();},this._update=t=>{var e;if(!this._map)return;const i=this._map.loaded()&&!this._map.isMoving();("terrain"===(null==t?void 0:t.type)||"render"===(null==t?void 0:t.type)&&!i)&&this._map.once("render",this._update),this._lngLat=this._map.transform.renderWorldCopies?js(this._lngLat,this._flatPos,this._map.transform):null===(e=this._lngLat)||void 0===e?void 0:e.wrap(),this._flatPos=this._pos=this._map.project(this._lngLat)._add(this._offset),this._map.terrain&&(this._flatPos=this._map.transform.locationPoint(this._lngLat)._add(this._offset));let a="";"viewport"===this._rotationAlignment||"auto"===this._rotationAlignment?a=`rotateZ(${this._rotation}deg)`:"map"===this._rotationAlignment&&(a=`rotateZ(${this._rotation-this._map.getBearing()}deg)`);let s="";"viewport"===this._pitchAlignment||"auto"===this._pitchAlignment?s="rotateX(0deg)":"map"===this._pitchAlignment&&(s=`rotateX(${this._map.getPitch()}deg)`),this._subpixelPositioning||t&&"moveend"!==t.type||(this._pos=this._pos.round()),r.setTransform(this._element,`${Zs[this._anchor]} translate(${this._pos.x}px, ${this._pos.y}px) ${s} ${a}`),o.frameAsync(new AbortController).then((()=>{this._updateOpacity(t&&"moveend"===t.type);})).catch((()=>{}));},this._onMove=t=>{if(!this._isDragging){const e=this._clickTolerance||this._map._clickTolerance;this._isDragging=t.point.dist(this._pointerdownPos)>=e;}this._isDragging&&(this._pos=t.point.sub(this._positionDelta),this._lngLat=this._map.unproject(this._pos),this.setLngLat(this._lngLat),this._element.style.pointerEvents="none","pending"===this._state&&(this._state="active",this.fire(new e.k("dragstart"))),this.fire(new e.k("drag")));},this._onUp=()=>{this._element.style.pointerEvents="auto",this._positionDelta=null,this._pointerdownPos=null,this._isDragging=!1,this._map.off("mousemove",this._onMove),this._map.off("touchmove",this._onMove),"active"===this._state&&this.fire(new e.k("dragend")),this._state="inactive";},this._addDragHandler=t=>{this._element.contains(t.originalEvent.target)&&(t.preventDefault(),this._positionDelta=t.point.sub(this._pos).add(this._offset),this._pointerdownPos=t.point,this._state="pending",this._map.on("mousemove",this._onMove),this._map.on("touchmove",this._onMove),this._map.once("mouseup",this._onUp),this._map.once("touchend",this._onUp));},this._anchor=t&&t.anchor||"center",this._color=t&&t.color||"#3FB1CE",this._scale=t&&t.scale||1,this._draggable=t&&t.draggable||!1,this._clickTolerance=t&&t.clickTolerance||0,this._subpixelPositioning=t&&t.subpixelPositioning||!1,this._isDragging=!1,this._state="inactive",this._rotation=t&&t.rotation||0,this._rotationAlignment=t&&t.rotationAlignment||"auto",this._pitchAlignment=t&&t.pitchAlignment&&"auto"!==t.pitchAlignment?t.pitchAlignment:this._rotationAlignment,this.setOpacity(),this.setOpacity(null==t?void 0:t.opacity,null==t?void 0:t.opacityWhenCovered),t&&t.element)this._element=t.element,this._offset=e.P.convert(t&&t.offset||[0,0]);else {this._defaultMarker=!0,this._element=r.create("div");const i=r.createNS("http://www.w3.org/2000/svg","svg"),a=41,s=27;i.setAttributeNS(null,"display","block"),i.setAttributeNS(null,"height",`${a}px`),i.setAttributeNS(null,"width",`${s}px`),i.setAttributeNS(null,"viewBox",`0 0 ${s} ${a}`);const o=r.createNS("http://www.w3.org/2000/svg","g");o.setAttributeNS(null,"stroke","none"),o.setAttributeNS(null,"stroke-width","1"),o.setAttributeNS(null,"fill","none"),o.setAttributeNS(null,"fill-rule","evenodd");const n=r.createNS("http://www.w3.org/2000/svg","g");n.setAttributeNS(null,"fill-rule","nonzero");const l=r.createNS("http://www.w3.org/2000/svg","g");l.setAttributeNS(null,"transform","translate(3.0, 29.0)"),l.setAttributeNS(null,"fill","#000000");const h=[{rx:"10.5",ry:"5.25002273"},{rx:"10.5",ry:"5.25002273"},{rx:"9.5",ry:"4.77275007"},{rx:"8.5",ry:"4.29549936"},{rx:"7.5",ry:"3.81822308"},{rx:"6.5",ry:"3.34094679"},{rx:"5.5",ry:"2.86367051"},{rx:"4.5",ry:"2.38636864"}];for(const t of h){const e=r.createNS("http://www.w3.org/2000/svg","ellipse");e.setAttributeNS(null,"opacity","0.04"),e.setAttributeNS(null,"cx","10.5"),e.setAttributeNS(null,"cy","5.80029008"),e.setAttributeNS(null,"rx",t.rx),e.setAttributeNS(null,"ry",t.ry),l.appendChild(e);}const c=r.createNS("http://www.w3.org/2000/svg","g");c.setAttributeNS(null,"fill",this._color);const u=r.createNS("http://www.w3.org/2000/svg","path");u.setAttributeNS(null,"d","M27,13.5 C27,19.074644 20.250001,27.000002 14.75,34.500002 C14.016665,35.500004 12.983335,35.500004 12.25,34.500002 C6.7499993,27.000002 0,19.222562 0,13.5 C0,6.0441559 6.0441559,0 13.5,0 C20.955844,0 27,6.0441559 27,13.5 Z"),c.appendChild(u);const d=r.createNS("http://www.w3.org/2000/svg","g");d.setAttributeNS(null,"opacity","0.25"),d.setAttributeNS(null,"fill","#000000");const _=r.createNS("http://www.w3.org/2000/svg","path");_.setAttributeNS(null,"d","M13.5,0 C6.0441559,0 0,6.0441559 0,13.5 C0,19.222562 6.7499993,27 12.25,34.5 C13,35.522727 14.016664,35.500004 14.75,34.5 C20.250001,27 27,19.074644 27,13.5 C27,6.0441559 20.955844,0 13.5,0 Z M13.5,1 C20.415404,1 26,6.584596 26,13.5 C26,15.898657 24.495584,19.181431 22.220703,22.738281 C19.945823,26.295132 16.705119,30.142167 13.943359,33.908203 C13.743445,34.180814 13.612715,34.322738 13.5,34.441406 C13.387285,34.322738 13.256555,34.180814 13.056641,33.908203 C10.284481,30.127985 7.4148684,26.314159 5.015625,22.773438 C2.6163816,19.232715 1,15.953538 1,13.5 C1,6.584596 6.584596,1 13.5,1 Z"),d.appendChild(_);const p=r.createNS("http://www.w3.org/2000/svg","g");p.setAttributeNS(null,"transform","translate(6.0, 7.0)"),p.setAttributeNS(null,"fill","#FFFFFF");const m=r.createNS("http://www.w3.org/2000/svg","g");m.setAttributeNS(null,"transform","translate(8.0, 8.0)");const f=r.createNS("http://www.w3.org/2000/svg","circle");f.setAttributeNS(null,"fill","#000000"),f.setAttributeNS(null,"opacity","0.25"),f.setAttributeNS(null,"cx","5.5"),f.setAttributeNS(null,"cy","5.5"),f.setAttributeNS(null,"r","5.4999962");const g=r.createNS("http://www.w3.org/2000/svg","circle");g.setAttributeNS(null,"fill","#FFFFFF"),g.setAttributeNS(null,"cx","5.5"),g.setAttributeNS(null,"cy","5.5"),g.setAttributeNS(null,"r","5.4999962"),m.appendChild(f),m.appendChild(g),n.appendChild(l),n.appendChild(c),n.appendChild(d),n.appendChild(p),n.appendChild(m),i.appendChild(n),i.setAttributeNS(null,"height",a*this._scale+"px"),i.setAttributeNS(null,"width",s*this._scale+"px"),this._element.appendChild(i),this._offset=e.P.convert(t&&t.offset||[0,-14]);}if(this._element.classList.add("maplibregl-marker"),this._element.addEventListener("dragstart",(t=>{t.preventDefault();})),this._element.addEventListener("mousedown",(t=>{t.preventDefault();})),qs(this._element,this._anchor,"marker"),t&&t.className)for(const e of t.className.split(" "))this._element.classList.add(e);this._popup=null;}addTo(t){return this.remove(),this._map=t,this._element.setAttribute("aria-label",t._getUIString("Marker.Title")),t.getCanvasContainer().appendChild(this._element),t.on("move",this._update),t.on("moveend",this._update),t.on("terrain",this._update),this.setDraggable(this._draggable),this._update(),this._map.on("click",this._onMapClick),this}remove(){return this._opacityTimeout&&(clearTimeout(this._opacityTimeout),delete this._opacityTimeout),this._map&&(this._map.off("click",this._onMapClick),this._map.off("move",this._update),this._map.off("moveend",this._update),this._map.off("terrain",this._update),this._map.off("mousedown",this._addDragHandler),this._map.off("touchstart",this._addDragHandler),this._map.off("mouseup",this._onUp),this._map.off("touchend",this._onUp),this._map.off("mousemove",this._onMove),this._map.off("touchmove",this._onMove),delete this._map),r.remove(this._element),this._popup&&this._popup.remove(),this}getLngLat(){return this._lngLat}setLngLat(t){return this._lngLat=e.N.convert(t),this._pos=null,this._popup&&this._popup.setLngLat(this._lngLat),this._update(),this}getElement(){return this._element}setPopup(t){if(this._popup&&(this._popup.remove(),this._popup=null,this._element.removeEventListener("keypress",this._onKeyPress),this._originalTabIndex||this._element.removeAttribute("tabindex")),t){if(!("offset"in t.options)){const e=38.1,i=13.5,a=Math.abs(i)/Math.SQRT2;t.options.offset=this._defaultMarker?{top:[0,0],"top-left":[0,0],"top-right":[0,0],bottom:[0,-e],"bottom-left":[a,-1*(e-i+a)],"bottom-right":[-a,-1*(e-i+a)],left:[i,-1*(e-i)],right:[-i,-1*(e-i)]}:this._offset;}this._popup=t,this._originalTabIndex=this._element.getAttribute("tabindex"),this._originalTabIndex||this._element.setAttribute("tabindex","0"),this._element.addEventListener("keypress",this._onKeyPress);}return this}setSubpixelPositioning(t){return this._subpixelPositioning=t,this}getPopup(){return this._popup}togglePopup(){const t=this._popup;return this._element.style.opacity===this._opacityWhenCovered?this:t?(t.isOpen()?t.remove():(t.setLngLat(this._lngLat),t.addTo(this._map)),this):this}_updateOpacity(t=!1){var i,a;if(!(null===(i=this._map)||void 0===i?void 0:i.terrain))return void(this._element.style.opacity!==this._opacity&&(this._element.style.opacity=this._opacity));if(t)this._opacityTimeout=null;else {if(this._opacityTimeout)return;this._opacityTimeout=setTimeout((()=>{this._opacityTimeout=null;}),100);}const s=this._map,o=s.terrain.depthAtPoint(this._pos),r=s.terrain.getElevationForLngLatZoom(this._lngLat,s.transform.tileZoom);if(s.transform.lngLatToCameraDepth(this._lngLat,r)-o<.006)return void(this._element.style.opacity=this._opacity);const n=-this._offset.y/s.transform._pixelPerMeter,l=Math.sin(s.getPitch()*Math.PI/180)*n,h=s.terrain.depthAtPoint(new e.P(this._pos.x,this._pos.y-this._offset.y)),c=s.transform.lngLatToCameraDepth(this._lngLat,r+l)-h>.006;(null===(a=this._popup)||void 0===a?void 0:a.isOpen())&&c&&this._popup.remove(),this._element.style.opacity=c?this._opacityWhenCovered:this._opacity;}getOffset(){return this._offset}setOffset(t){return this._offset=e.P.convert(t),this._update(),this}addClassName(t){this._element.classList.add(t);}removeClassName(t){this._element.classList.remove(t);}toggleClassName(t){return this._element.classList.toggle(t)}setDraggable(t){return this._draggable=!!t,this._map&&(t?(this._map.on("mousedown",this._addDragHandler),this._map.on("touchstart",this._addDragHandler)):(this._map.off("mousedown",this._addDragHandler),this._map.off("touchstart",this._addDragHandler))),this}isDraggable(){return this._draggable}setRotation(t){return this._rotation=t||0,this._update(),this}getRotation(){return this._rotation}setRotationAlignment(t){return this._rotationAlignment=t||"auto",this._update(),this}getRotationAlignment(){return this._rotationAlignment}setPitchAlignment(t){return this._pitchAlignment=t&&"auto"!==t?t:this._rotationAlignment,this._update(),this}getPitchAlignment(){return this._pitchAlignment}setOpacity(t,e){return void 0===t&&void 0===e&&(this._opacity="1",this._opacityWhenCovered="0.2"),void 0!==t&&(this._opacity=t),void 0!==e&&(this._opacityWhenCovered=e),this._map&&this._updateOpacity(!0),this}}const Gs={positionOptions:{enableHighAccuracy:!1,maximumAge:0,timeout:6e3},fitBoundsOptions:{maxZoom:15},trackUserLocation:!1,showAccuracyCircle:!0,showUserLocation:!0};let Hs=0,Ws=!1;const $s={maxWidth:100,unit:"metric"};function Xs(t,e,i){const a=i&&i.maxWidth||100,s=t._container.clientHeight/2,o=t.unproject([0,s]),r=t.unproject([a,s]),n=o.distanceTo(r);if(i&&"imperial"===i.unit){const i=3.2808*n;i>5280?Ks(e,a,i/5280,t._getUIString("ScaleControl.Miles")):Ks(e,a,i,t._getUIString("ScaleControl.Feet"));}else i&&"nautical"===i.unit?Ks(e,a,n/1852,t._getUIString("ScaleControl.NauticalMiles")):n>=1e3?Ks(e,a,n/1e3,t._getUIString("ScaleControl.Kilometers")):Ks(e,a,n,t._getUIString("ScaleControl.Meters"));}function Ks(t,e,i,a){const s=function(t){const e=Math.pow(10,`${Math.floor(t)}`.length-1);let i=t/e;return i=i>=10?10:i>=5?5:i>=3?3:i>=2?2:i>=1?1:function(t){const e=Math.pow(10,Math.ceil(-Math.log(t)/Math.LN10));return Math.round(t*e)/e}(i),e*i}(i);t.style.width=e*(s/i)+"px",t.innerHTML=`${s} ${a}`;}const Js={closeButton:!0,closeOnClick:!0,focusAfterOpen:!0,className:"",maxWidth:"240px",subpixelPositioning:!1},Ys=["a[href]","[tabindex]:not([tabindex='-1'])","[contenteditable]:not([contenteditable='false'])","button:not([disabled])","input:not([disabled])","select:not([disabled])","textarea:not([disabled])"].join(", ");function Qs(t){if(t){if("number"==typeof t){const i=Math.round(Math.abs(t)/Math.SQRT2);return {center:new e.P(0,0),top:new e.P(0,t),"top-left":new e.P(i,i),"top-right":new e.P(-i,i),bottom:new e.P(0,-t),"bottom-left":new e.P(i,-i),"bottom-right":new e.P(-i,-i),left:new e.P(t,0),right:new e.P(-t,0)}}if(t instanceof e.P||Array.isArray(t)){const i=e.P.convert(t);return {center:i,top:i,"top-left":i,"top-right":i,bottom:i,"bottom-left":i,"bottom-right":i,left:i,right:i}}return {center:e.P.convert(t.center||[0,0]),top:e.P.convert(t.top||[0,0]),"top-left":e.P.convert(t["top-left"]||[0,0]),"top-right":e.P.convert(t["top-right"]||[0,0]),bottom:e.P.convert(t.bottom||[0,0]),"bottom-left":e.P.convert(t["bottom-left"]||[0,0]),"bottom-right":e.P.convert(t["bottom-right"]||[0,0]),left:e.P.convert(t.left||[0,0]),right:e.P.convert(t.right||[0,0])}}return Qs(new e.P(0,0))}const to=i;t.AJAXError=e.bh,t.Evented=e.E,t.LngLat=e.N,t.MercatorCoordinate=e.Z,t.Point=e.P,t.addProtocol=e.bi,t.config=e.a,t.removeProtocol=e.bj,t.AttributionControl=Es,t.BoxZoomHandler=Ua,t.CanvasSource=et,t.CooperativeGesturesHandler=vs,t.DoubleClickZoomHandler=ds,t.DragPanHandler=ms,t.DragRotateHandler=fs,t.EdgeInsets=ba,t.FullscreenControl=class extends e.E{constructor(t={}){super(),this._onFullscreenChange=()=>{var t;let e=window.document.fullscreenElement||window.document.mozFullScreenElement||window.document.webkitFullscreenElement||window.document.msFullscreenElement;for(;null===(t=null==e?void 0:e.shadowRoot)||void 0===t?void 0:t.fullscreenElement;)e=e.shadowRoot.fullscreenElement;e===this._container!==this._fullscreen&&this._handleFullscreenChange();},this._onClickFullscreen=()=>{this._isFullscreen()?this._exitFullscreen():this._requestFullscreen();},this._fullscreen=!1,t&&t.container&&(t.container instanceof HTMLElement?this._container=t.container:e.w("Full screen control 'container' must be a DOM element.")),"onfullscreenchange"in document?this._fullscreenchange="fullscreenchange":"onmozfullscreenchange"in document?this._fullscreenchange="mozfullscreenchange":"onwebkitfullscreenchange"in document?this._fullscreenchange="webkitfullscreenchange":"onmsfullscreenchange"in document&&(this._fullscreenchange="MSFullscreenChange");}onAdd(t){return this._map=t,this._container||(this._container=this._map.getContainer()),this._controlContainer=r.create("div","maplibregl-ctrl maplibregl-ctrl-group"),this._setupUI(),this._controlContainer}onRemove(){r.remove(this._controlContainer),this._map=null,window.document.removeEventListener(this._fullscreenchange,this._onFullscreenChange);}_setupUI(){const t=this._fullscreenButton=r.create("button","maplibregl-ctrl-fullscreen",this._controlContainer);r.create("span","maplibregl-ctrl-icon",t).setAttribute("aria-hidden","true"),t.type="button",this._updateTitle(),this._fullscreenButton.addEventListener("click",this._onClickFullscreen),window.document.addEventListener(this._fullscreenchange,this._onFullscreenChange);}_updateTitle(){const t=this._getTitle();this._fullscreenButton.setAttribute("aria-label",t),this._fullscreenButton.title=t;}_getTitle(){return this._map._getUIString(this._isFullscreen()?"FullscreenControl.Exit":"FullscreenControl.Enter")}_isFullscreen(){return this._fullscreen}_handleFullscreenChange(){this._fullscreen=!this._fullscreen,this._fullscreenButton.classList.toggle("maplibregl-ctrl-shrink"),this._fullscreenButton.classList.toggle("maplibregl-ctrl-fullscreen"),this._updateTitle(),this._fullscreen?(this.fire(new e.k("fullscreenstart")),this._prevCooperativeGesturesEnabled=this._map.cooperativeGestures.isEnabled(),this._map.cooperativeGestures.disable()):(this.fire(new e.k("fullscreenend")),this._prevCooperativeGesturesEnabled&&this._map.cooperativeGestures.enable());}_exitFullscreen(){window.document.exitFullscreen?window.document.exitFullscreen():window.document.mozCancelFullScreen?window.document.mozCancelFullScreen():window.document.msExitFullscreen?window.document.msExitFullscreen():window.document.webkitCancelFullScreen?window.document.webkitCancelFullScreen():this._togglePseudoFullScreen();}_requestFullscreen(){this._container.requestFullscreen?this._container.requestFullscreen():this._container.mozRequestFullScreen?this._container.mozRequestFullScreen():this._container.msRequestFullscreen?this._container.msRequestFullscreen():this._container.webkitRequestFullscreen?this._container.webkitRequestFullscreen():this._togglePseudoFullScreen();}_togglePseudoFullScreen(){this._container.classList.toggle("maplibregl-pseudo-fullscreen"),this._handleFullscreenChange(),this._map.resize();}},t.GeoJSONSource=J,t.GeolocateControl=class extends e.E{constructor(t){super(),this._onSuccess=t=>{if(this._map){if(this._isOutOfMapMaxBounds(t))return this._setErrorState(),this.fire(new e.k("outofmaxbounds",t)),this._updateMarker(),void this._finish();if(this.options.trackUserLocation)switch(this._lastKnownPosition=t,this._watchState){case"WAITING_ACTIVE":case"ACTIVE_LOCK":case"ACTIVE_ERROR":this._watchState="ACTIVE_LOCK",this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-waiting"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active-error"),this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-active");break;case"BACKGROUND":case"BACKGROUND_ERROR":this._watchState="BACKGROUND",this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-waiting"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background-error"),this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-background");break;default:throw new Error(`Unexpected watchState ${this._watchState}`)}this.options.showUserLocation&&"OFF"!==this._watchState&&this._updateMarker(t),this.options.trackUserLocation&&"ACTIVE_LOCK"!==this._watchState||this._updateCamera(t),this.options.showUserLocation&&this._dotElement.classList.remove("maplibregl-user-location-dot-stale"),this.fire(new e.k("geolocate",t)),this._finish();}},this._updateCamera=t=>{const i=new e.N(t.coords.longitude,t.coords.latitude),a=t.coords.accuracy,s=this._map.getBearing(),o=e.e({bearing:s},this.options.fitBoundsOptions),r=H.fromLngLat(i,a);this._map.fitBounds(r,o,{geolocateSource:!0});},this._updateMarker=t=>{if(t){const i=new e.N(t.coords.longitude,t.coords.latitude);this._accuracyCircleMarker.setLngLat(i).addTo(this._map),this._userLocationDotMarker.setLngLat(i).addTo(this._map),this._accuracy=t.coords.accuracy,this.options.showUserLocation&&this.options.showAccuracyCircle&&this._updateCircleRadius();}else this._userLocationDotMarker.remove(),this._accuracyCircleMarker.remove();},this._onZoom=()=>{this.options.showUserLocation&&this.options.showAccuracyCircle&&this._updateCircleRadius();},this._onError=t=>{if(this._map){if(this.options.trackUserLocation)if(1===t.code){this._watchState="OFF",this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-waiting"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active-error"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background-error"),this._geolocateButton.disabled=!0;const t=this._map._getUIString("GeolocateControl.LocationNotAvailable");this._geolocateButton.title=t,this._geolocateButton.setAttribute("aria-label",t),void 0!==this._geolocationWatchID&&this._clearWatch();}else {if(3===t.code&&Ws)return;this._setErrorState();}"OFF"!==this._watchState&&this.options.showUserLocation&&this._dotElement.classList.add("maplibregl-user-location-dot-stale"),this.fire(new e.k("error",t)),this._finish();}},this._finish=()=>{this._timeoutId&&clearTimeout(this._timeoutId),this._timeoutId=void 0;},this._setupUI=()=>{this._map&&(this._container.addEventListener("contextmenu",(t=>t.preventDefault())),this._geolocateButton=r.create("button","maplibregl-ctrl-geolocate",this._container),r.create("span","maplibregl-ctrl-icon",this._geolocateButton).setAttribute("aria-hidden","true"),this._geolocateButton.type="button",this._geolocateButton.disabled=!0);},this._finishSetupUI=t=>{if(this._map){if(!1===t){e.w("Geolocation support is not available so the GeolocateControl will be disabled.");const t=this._map._getUIString("GeolocateControl.LocationNotAvailable");this._geolocateButton.disabled=!0,this._geolocateButton.title=t,this._geolocateButton.setAttribute("aria-label",t);}else {const t=this._map._getUIString("GeolocateControl.FindMyLocation");this._geolocateButton.disabled=!1,this._geolocateButton.title=t,this._geolocateButton.setAttribute("aria-label",t);}this.options.trackUserLocation&&(this._geolocateButton.setAttribute("aria-pressed","false"),this._watchState="OFF"),this.options.showUserLocation&&(this._dotElement=r.create("div","maplibregl-user-location-dot"),this._userLocationDotMarker=new Vs({element:this._dotElement}),this._circleElement=r.create("div","maplibregl-user-location-accuracy-circle"),this._accuracyCircleMarker=new Vs({element:this._circleElement,pitchAlignment:"map"}),this.options.trackUserLocation&&(this._watchState="OFF"),this._map.on("zoom",this._onZoom)),this._geolocateButton.addEventListener("click",(()=>this.trigger())),this._setup=!0,this.options.trackUserLocation&&this._map.on("movestart",(t=>{t.geolocateSource||"ACTIVE_LOCK"!==this._watchState||t.originalEvent&&"resize"===t.originalEvent.type||(this._watchState="BACKGROUND",this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-background"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active"),this.fire(new e.k("trackuserlocationend")),this.fire(new e.k("userlocationlostfocus")));}));}},this.options=e.e({},Gs,t);}onAdd(t){return this._map=t,this._container=r.create("div","maplibregl-ctrl maplibregl-ctrl-group"),this._setupUI(),function(){return e._(this,arguments,void 0,(function*(t=!1){if(void 0!==Us&&!t)return Us;if(void 0===window.navigator.permissions)return Us=!!window.navigator.geolocation,Us;try{const t=yield window.navigator.permissions.query({name:"geolocation"});Us="denied"!==t.state;}catch(t){Us=!!window.navigator.geolocation;}return Us}))}().then((t=>this._finishSetupUI(t))),this._container}onRemove(){void 0!==this._geolocationWatchID&&(window.navigator.geolocation.clearWatch(this._geolocationWatchID),this._geolocationWatchID=void 0),this.options.showUserLocation&&this._userLocationDotMarker&&this._userLocationDotMarker.remove(),this.options.showAccuracyCircle&&this._accuracyCircleMarker&&this._accuracyCircleMarker.remove(),r.remove(this._container),this._map.off("zoom",this._onZoom),this._map=void 0,Hs=0,Ws=!1;}_isOutOfMapMaxBounds(t){const e=this._map.getMaxBounds(),i=t.coords;return e&&(i.longitudee.getEast()||i.latitudee.getNorth())}_setErrorState(){switch(this._watchState){case"WAITING_ACTIVE":this._watchState="ACTIVE_ERROR",this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active"),this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-active-error");break;case"ACTIVE_LOCK":this._watchState="ACTIVE_ERROR",this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active"),this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-active-error"),this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-waiting");break;case"BACKGROUND":this._watchState="BACKGROUND_ERROR",this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background"),this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-background-error"),this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-waiting");break;case"ACTIVE_ERROR":break;default:throw new Error(`Unexpected watchState ${this._watchState}`)}}_updateCircleRadius(){const t=this._map.getBounds(),e=t.getSouthEast(),i=t.getNorthEast(),a=e.distanceTo(i),s=Math.ceil(this._accuracy/(a/this._map._container.clientHeight)*2);this._circleElement.style.width=`${s}px`,this._circleElement.style.height=`${s}px`;}trigger(){if(!this._setup)return e.w("Geolocate control triggered before added to a map"),!1;if(this.options.trackUserLocation){switch(this._watchState){case"OFF":this._watchState="WAITING_ACTIVE",this.fire(new e.k("trackuserlocationstart"));break;case"WAITING_ACTIVE":case"ACTIVE_LOCK":case"ACTIVE_ERROR":case"BACKGROUND_ERROR":Hs--,Ws=!1,this._watchState="OFF",this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-waiting"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active-error"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background-error"),this.fire(new e.k("trackuserlocationend"));break;case"BACKGROUND":this._watchState="ACTIVE_LOCK",this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background"),this._lastKnownPosition&&this._updateCamera(this._lastKnownPosition),this.fire(new e.k("trackuserlocationstart")),this.fire(new e.k("userlocationfocus"));break;default:throw new Error(`Unexpected watchState ${this._watchState}`)}switch(this._watchState){case"WAITING_ACTIVE":this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-waiting"),this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-active");break;case"ACTIVE_LOCK":this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-active");break;case"OFF":break;default:throw new Error(`Unexpected watchState ${this._watchState}`)}if("OFF"===this._watchState&&void 0!==this._geolocationWatchID)this._clearWatch();else if(void 0===this._geolocationWatchID){let t;this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-waiting"),this._geolocateButton.setAttribute("aria-pressed","true"),Hs++,Hs>1?(t={maximumAge:6e5,timeout:0},Ws=!0):(t=this.options.positionOptions,Ws=!1),this._geolocationWatchID=window.navigator.geolocation.watchPosition(this._onSuccess,this._onError,t);}}else window.navigator.geolocation.getCurrentPosition(this._onSuccess,this._onError,this.options.positionOptions),this._timeoutId=setTimeout(this._finish,1e4);return !0}_clearWatch(){window.navigator.geolocation.clearWatch(this._geolocationWatchID),this._geolocationWatchID=void 0,this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-waiting"),this._geolocateButton.setAttribute("aria-pressed","false"),this.options.showUserLocation&&this._updateMarker(null);}},t.Hash=Ea,t.ImageSource=Q,t.KeyboardHandler=ls,t.LngLatBounds=H,t.LogoControl=Ps,t.Map=class extends Ts{constructor(t){e.bf.mark(e.bg.create);const i=Object.assign(Object.assign({},Fs),t);if(null!=i.minZoom&&null!=i.maxZoom&&i.minZoom>i.maxZoom)throw new Error("maxZoom must be greater than or equal to minZoom");if(null!=i.minPitch&&null!=i.maxPitch&&i.minPitch>i.maxPitch)throw new Error("maxPitch must be greater than or equal to minPitch");if(null!=i.minPitch&&i.minPitch<0)throw new Error("minPitch must be greater than or equal to 0");if(null!=i.maxPitch&&i.maxPitch>85)throw new Error("maxPitch must be less than or equal to 85");if(super(new Ta(i.minZoom,i.maxZoom,i.minPitch,i.maxPitch,i.renderWorldCopies),{bearingSnap:i.bearingSnap}),this._idleTriggered=!1,this._crossFadingFactor=1,this._renderTaskQueue=new Cs,this._controls=[],this._mapId=e.a4(),this._contextLost=t=>{t.preventDefault(),this._frameRequest&&(this._frameRequest.abort(),this._frameRequest=null),this.fire(new e.k("webglcontextlost",{originalEvent:t}));},this._contextRestored=t=>{this._setupPainter(),this.resize(),this._update(),this.fire(new e.k("webglcontextrestored",{originalEvent:t}));},this._onMapScroll=t=>{if(t.target===this._container)return this._container.scrollTop=0,this._container.scrollLeft=0,!1},this._onWindowOnline=()=>{this._update();},this._interactive=i.interactive,this._maxTileCacheSize=i.maxTileCacheSize,this._maxTileCacheZoomLevels=i.maxTileCacheZoomLevels,this._failIfMajorPerformanceCaveat=!0===i.failIfMajorPerformanceCaveat,this._preserveDrawingBuffer=!0===i.preserveDrawingBuffer,this._antialias=!0===i.antialias,this._trackResize=!0===i.trackResize,this._bearingSnap=i.bearingSnap,this._refreshExpiredTiles=!0===i.refreshExpiredTiles,this._fadeDuration=i.fadeDuration,this._crossSourceCollisions=!0===i.crossSourceCollisions,this._collectResourceTiming=!0===i.collectResourceTiming,this._locale=Object.assign(Object.assign({},ks),i.locale),this._clickTolerance=i.clickTolerance,this._overridePixelRatio=i.pixelRatio,this._maxCanvasSize=i.maxCanvasSize,this.transformCameraUpdate=i.transformCameraUpdate,this.cancelPendingTileRequestsWhileZooming=!0===i.cancelPendingTileRequestsWhileZooming,this._imageQueueHandle=_.addThrottleControl((()=>this.isMoving())),this._requestManager=new p(i.transformRequest),"string"==typeof i.container){if(this._container=document.getElementById(i.container),!this._container)throw new Error(`Container '${i.container}' not found.`)}else {if(!(i.container instanceof HTMLElement))throw new Error("Invalid type: 'container' must be a String or HTMLElement.");this._container=i.container;}if(i.maxBounds&&this.setMaxBounds(i.maxBounds),this._setupContainer(),this._setupPainter(),this.on("move",(()=>this._update(!1))).on("moveend",(()=>this._update(!1))).on("zoom",(()=>this._update(!0))).on("terrain",(()=>{this.painter.terrainFacilitator.dirty=!0,this._update(!0);})).once("idle",(()=>{this._idleTriggered=!0;})),"undefined"!=typeof window){addEventListener("online",this._onWindowOnline,!1);let t=!1;const e=Ia((t=>{this._trackResize&&!this._removed&&(this.resize(t),this.redraw());}),50);this._resizeObserver=new ResizeObserver((i=>{t?e(i):t=!0;})),this._resizeObserver.observe(this._container);}this.handlers=new ws(this,i),this._hash=i.hash&&new Ea("string"==typeof i.hash&&i.hash||void 0).addTo(this),this._hash&&this._hash._onHashChange()||(this.jumpTo({center:i.center,zoom:i.zoom,bearing:i.bearing,pitch:i.pitch}),i.bounds&&(this.resize(),this.fitBounds(i.bounds,e.e({},i.fitBoundsOptions,{duration:0})))),this.resize(),this._localIdeographFontFamily=i.localIdeographFontFamily,this._validateStyle=i.validateStyle,i.style&&this.setStyle(i.style,{localIdeographFontFamily:i.localIdeographFontFamily}),i.attributionControl&&this.addControl(new Es("boolean"==typeof i.attributionControl?void 0:i.attributionControl)),i.maplibreLogo&&this.addControl(new Ps,i.logoPosition),this.on("style.load",(()=>{this.transform.unmodified&&this.jumpTo(this.style.stylesheet);})),this.on("data",(t=>{this._update("style"===t.dataType),this.fire(new e.k(`${t.dataType}data`,t));})),this.on("dataloading",(t=>{this.fire(new e.k(`${t.dataType}dataloading`,t));})),this.on("dataabort",(t=>{this.fire(new e.k("sourcedataabort",t));}));}_getMapId(){return this._mapId}addControl(t,i){if(void 0===i&&(i=t.getDefaultPosition?t.getDefaultPosition():"top-right"),!t||!t.onAdd)return this.fire(new e.j(new Error("Invalid argument to map.addControl(). Argument must be a control with onAdd and onRemove methods.")));const a=t.onAdd(this);this._controls.push(t);const s=this._controlPositions[i];return -1!==i.indexOf("bottom")?s.insertBefore(a,s.firstChild):s.appendChild(a),this}removeControl(t){if(!t||!t.onRemove)return this.fire(new e.j(new Error("Invalid argument to map.removeControl(). Argument must be a control with onAdd and onRemove methods.")));const i=this._controls.indexOf(t);return i>-1&&this._controls.splice(i,1),t.onRemove(this),this}hasControl(t){return this._controls.indexOf(t)>-1}calculateCameraOptionsFromTo(t,e,i,a){return null==a&&this.terrain&&(a=this.terrain.getElevationForLngLatZoom(i,this.transform.tileZoom)),super.calculateCameraOptionsFromTo(t,e,i,a)}resize(t){var i;const a=this._containerDimensions(),s=a[0],o=a[1],r=this._getClampedPixelRatio(s,o);if(this._resizeCanvas(s,o,r),this.painter.resize(s,o,r),this.painter.overLimit()){const t=this.painter.context.gl;this._maxCanvasSize=[t.drawingBufferWidth,t.drawingBufferHeight];const e=this._getClampedPixelRatio(s,o);this._resizeCanvas(s,o,e),this.painter.resize(s,o,e);}this.transform.resize(s,o),null===(i=this._requestedCameraState)||void 0===i||i.resize(s,o);const n=!this._moving;return n&&(this.stop(),this.fire(new e.k("movestart",t)).fire(new e.k("move",t))),this.fire(new e.k("resize",t)),n&&this.fire(new e.k("moveend",t)),this}_getClampedPixelRatio(t,e){const{0:i,1:a}=this._maxCanvasSize,s=this.getPixelRatio(),o=t*s,r=e*s;return Math.min(o>i?i/o:1,r>a?a/r:1)*s}getPixelRatio(){var t;return null!==(t=this._overridePixelRatio)&&void 0!==t?t:devicePixelRatio}setPixelRatio(t){this._overridePixelRatio=t,this.resize();}getBounds(){return this.transform.getBounds()}getMaxBounds(){return this.transform.getMaxBounds()}setMaxBounds(t){return this.transform.setMaxBounds(H.convert(t)),this._update()}setMinZoom(t){if((t=null==t?-2:t)>=-2&&t<=this.transform.maxZoom)return this.transform.minZoom=t,this._update(),this.getZoom()=this.transform.minZoom)return this.transform.maxZoom=t,this._update(),this.getZoom()>t&&this.setZoom(t),this;throw new Error("maxZoom must be greater than the current minZoom")}getMaxZoom(){return this.transform.maxZoom}setMinPitch(t){if((t=null==t?0:t)<0)throw new Error("minPitch must be greater than or equal to 0");if(t>=0&&t<=this.transform.maxPitch)return this.transform.minPitch=t,this._update(),this.getPitch()85)throw new Error("maxPitch must be less than or equal to 85");if(t>=this.transform.minPitch)return this.transform.maxPitch=t,this._update(),this.getPitch()>t&&this.setPitch(t),this;throw new Error("maxPitch must be greater than the current minPitch")}getMaxPitch(){return this.transform.maxPitch}getRenderWorldCopies(){return this.transform.renderWorldCopies}setRenderWorldCopies(t){return this.transform.renderWorldCopies=t,this._update()}project(t){return this.transform.locationPoint(e.N.convert(t),this.style&&this.terrain)}unproject(t){return this.transform.pointLocation(e.P.convert(t),this.terrain)}isMoving(){var t;return this._moving||(null===(t=this.handlers)||void 0===t?void 0:t.isMoving())}isZooming(){var t;return this._zooming||(null===(t=this.handlers)||void 0===t?void 0:t.isZooming())}isRotating(){var t;return this._rotating||(null===(t=this.handlers)||void 0===t?void 0:t.isRotating())}_createDelegatedListener(t,e,i){if("mouseenter"===t||"mouseover"===t){let a=!1;const s=s=>{const o=e.filter((t=>this.getLayer(t))),r=0!==o.length?this.queryRenderedFeatures(s.point,{layers:o}):[];r.length?a||(a=!0,i.call(this,new ka(t,this,s.originalEvent,{features:r}))):a=!1;};return {layers:e,listener:i,delegates:{mousemove:s,mouseout:()=>{a=!1;}}}}if("mouseleave"===t||"mouseout"===t){let a=!1;const s=s=>{const o=e.filter((t=>this.getLayer(t)));(0!==o.length?this.queryRenderedFeatures(s.point,{layers:o}):[]).length?a=!0:a&&(a=!1,i.call(this,new ka(t,this,s.originalEvent)));},o=e=>{a&&(a=!1,i.call(this,new ka(t,this,e.originalEvent)));};return {layers:e,listener:i,delegates:{mousemove:s,mouseout:o}}}{const a=t=>{const a=e.filter((t=>this.getLayer(t))),s=0!==a.length?this.queryRenderedFeatures(t.point,{layers:a}):[];s.length&&(t.features=s,i.call(this,t),delete t.features);};return {layers:e,listener:i,delegates:{[t]:a}}}}_saveDelegatedListener(t,e){this._delegatedListeners=this._delegatedListeners||{},this._delegatedListeners[t]=this._delegatedListeners[t]||[],this._delegatedListeners[t].push(e);}_removeDelegatedListener(t,e,i){if(!this._delegatedListeners||!this._delegatedListeners[t])return;const a=this._delegatedListeners[t];for(let t=0;te.includes(t)))){for(const t in s.delegates)this.off(t,s.delegates[t]);return void a.splice(t,1)}}}on(t,e,i){if(void 0===i)return super.on(t,e);const a=this._createDelegatedListener(t,"string"==typeof e?[e]:e,i);this._saveDelegatedListener(t,a);for(const t in a.delegates)this.on(t,a.delegates[t]);return this}once(t,e,i){if(void 0===i)return super.once(t,e);const a="string"==typeof e?[e]:e,s=this._createDelegatedListener(t,a,i);for(const e in s.delegates){const o=s.delegates[e];s.delegates[e]=(...e)=>{this._removeDelegatedListener(t,a,i),o(...e);};}this._saveDelegatedListener(t,s);for(const t in s.delegates)this.once(t,s.delegates[t]);return this}off(t,e,i){return void 0===i?super.off(t,e):(this._removeDelegatedListener(t,"string"==typeof e?[e]:e,i),this)}queryRenderedFeatures(t,i){if(!this.style)return [];let a;const s=t instanceof e.P||Array.isArray(t),o=s?t:[[0,0],[this.transform.width,this.transform.height]];if(i=i||(s?{}:t)||{},o instanceof e.P||"number"==typeof o[0])a=[e.P.convert(o)];else {const t=e.P.convert(o[0]),i=e.P.convert(o[1]);a=[t,new e.P(i.x,t.y),i,new e.P(t.x,i.y),t];}return this.style.queryRenderedFeatures(a,i,this.transform)}querySourceFeatures(t,e){return this.style.querySourceFeatures(t,e)}setStyle(t,i){return !1!==(i=e.e({},{localIdeographFontFamily:this._localIdeographFontFamily,validate:this._validateStyle},i)).diff&&i.localIdeographFontFamily===this._localIdeographFontFamily&&this.style&&t?(this._diffStyle(t,i),this):(this._localIdeographFontFamily=i.localIdeographFontFamily,this._updateStyle(t,i))}setTransformRequest(t){return this._requestManager.setTransformRequest(t),this}_getUIString(t){const e=this._locale[t];if(null==e)throw new Error(`Missing UI string '${t}'`);return e}_updateStyle(t,e){if(e.transformStyle&&this.style&&!this.style._loaded)return void this.style.once("style.load",(()=>this._updateStyle(t,e)));const i=this.style&&e.transformStyle?this.style.serialize():void 0;return this.style&&(this.style.setEventedParent(null),this.style._remove(!t)),t?(this.style=new de(this,e||{}),this.style.setEventedParent(this,{style:this.style}),"string"==typeof t?this.style.loadURL(t,e,i):this.style.loadJSON(t,e,i),this):(delete this.style,this)}_lazyInitEmptyStyle(){this.style||(this.style=new de(this,{}),this.style.setEventedParent(this,{style:this.style}),this.style.loadEmpty());}_diffStyle(t,i){if("string"==typeof t){const a=this._requestManager.transformRequest(t,"Style");e.h(a,new AbortController).then((t=>{this._updateDiff(t.data,i);})).catch((t=>{t&&this.fire(new e.j(t));}));}else "object"==typeof t&&this._updateDiff(t,i);}_updateDiff(t,i){try{this.style.setState(t,i)&&this._update(!0);}catch(a){e.w(`Unable to perform style diff: ${a.message||a.error||a}. Rebuilding the style from scratch.`),this._updateStyle(t,i);}}getStyle(){if(this.style)return this.style.serialize()}isStyleLoaded(){return this.style?this.style.loaded():e.w("There is no style added to the map.")}addSource(t,e){return this._lazyInitEmptyStyle(),this.style.addSource(t,e),this._update(!0)}isSourceLoaded(t){const i=this.style&&this.style.sourceCaches[t];if(void 0!==i)return i.loaded();this.fire(new e.j(new Error(`There is no source with ID '${t}'`)));}setTerrain(t){if(this.style._checkLoaded(),this._terrainDataCallback&&this.style.off("data",this._terrainDataCallback),t){const i=this.style.sourceCaches[t.source];if(!i)throw new Error(`cannot load terrain, because there exists no source with ID: ${t.source}`);null===this.terrain&&i.reload();for(const i in this.style._layers){const a=this.style._layers[i];"hillshade"===a.type&&a.source===t.source&&e.w("You are using the same source for a hillshade layer and for 3D terrain. Please consider using two separate sources to improve rendering quality.");}this.terrain=new Ds(this.painter,i,t),this.painter.renderToTexture=new Rs(this.painter,this.terrain),this.transform.minElevationForCurrentTile=this.terrain.getMinTileElevationForLngLatZoom(this.transform.center,this.transform.tileZoom),this.transform.elevation=this.terrain.getElevationForLngLatZoom(this.transform.center,this.transform.tileZoom),this._terrainDataCallback=e=>{"style"===e.dataType?this.terrain.sourceCache.freeRtt():"source"===e.dataType&&e.tile&&(e.sourceId!==t.source||this._elevationFreeze||(this.transform.minElevationForCurrentTile=this.terrain.getMinTileElevationForLngLatZoom(this.transform.center,this.transform.tileZoom),this.transform.elevation=this.terrain.getElevationForLngLatZoom(this.transform.center,this.transform.tileZoom)),this.terrain.sourceCache.freeRtt(e.tile.tileID));},this.style.on("data",this._terrainDataCallback);}else this.terrain&&this.terrain.sourceCache.destruct(),this.terrain=null,this.painter.renderToTexture&&this.painter.renderToTexture.destruct(),this.painter.renderToTexture=null,this.transform.minElevationForCurrentTile=0,this.transform.elevation=0;return this.fire(new e.k("terrain",{terrain:t})),this}getTerrain(){var t,e;return null!==(e=null===(t=this.terrain)||void 0===t?void 0:t.options)&&void 0!==e?e:null}areTilesLoaded(){const t=this.style&&this.style.sourceCaches;for(const e in t){const i=t[e]._tiles;for(const t in i){const e=i[t];if("loaded"!==e.state&&"errored"!==e.state)return !1}}return !0}removeSource(t){return this.style.removeSource(t),this._update(!0)}getSource(t){return this.style.getSource(t)}addImage(t,i,a={}){const{pixelRatio:s=1,sdf:r=!1,stretchX:n,stretchY:l,content:h,textFitWidth:c,textFitHeight:u}=a;if(this._lazyInitEmptyStyle(),!(i instanceof HTMLImageElement||e.b(i))){if(void 0===i.width||void 0===i.height)return this.fire(new e.j(new Error("Invalid arguments to map.addImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, or object with `width`, `height`, and `data` properties with the same format as `ImageData`")));{const{width:a,height:o,data:d}=i,_=i;return this.style.addImage(t,{data:new e.R({width:a,height:o},new Uint8Array(d)),pixelRatio:s,stretchX:n,stretchY:l,content:h,textFitWidth:c,textFitHeight:u,sdf:r,version:0,userImage:_}),_.onAdd&&_.onAdd(this,t),this}}{const{width:a,height:d,data:_}=o.getImageData(i);this.style.addImage(t,{data:new e.R({width:a,height:d},_),pixelRatio:s,stretchX:n,stretchY:l,content:h,textFitWidth:c,textFitHeight:u,sdf:r,version:0});}}updateImage(t,i){const a=this.style.getImage(t);if(!a)return this.fire(new e.j(new Error("The map has no image with that id. If you are adding a new image use `map.addImage(...)` instead.")));const s=i instanceof HTMLImageElement||e.b(i)?o.getImageData(i):i,{width:r,height:n,data:l}=s;if(void 0===r||void 0===n)return this.fire(new e.j(new Error("Invalid arguments to map.updateImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, or object with `width`, `height`, and `data` properties with the same format as `ImageData`")));if(r!==a.data.width||n!==a.data.height)return this.fire(new e.j(new Error("The width and height of the updated image must be that same as the previous version of the image")));const h=!(i instanceof HTMLImageElement||e.b(i));return a.data.replace(l,h),this.style.updateImage(t,a),this}getImage(t){return this.style.getImage(t)}hasImage(t){return t?!!this.style.getImage(t):(this.fire(new e.j(new Error("Missing required image id"))),!1)}removeImage(t){this.style.removeImage(t);}loadImage(t){return _.getImage(this._requestManager.transformRequest(t,"Image"),new AbortController)}listImages(){return this.style.listImages()}addLayer(t,e){return this._lazyInitEmptyStyle(),this.style.addLayer(t,e),this._update(!0)}moveLayer(t,e){return this.style.moveLayer(t,e),this._update(!0)}removeLayer(t){return this.style.removeLayer(t),this._update(!0)}getLayer(t){return this.style.getLayer(t)}getLayersOrder(){return this.style.getLayersOrder()}setLayerZoomRange(t,e,i){return this.style.setLayerZoomRange(t,e,i),this._update(!0)}setFilter(t,e,i={}){return this.style.setFilter(t,e,i),this._update(!0)}getFilter(t){return this.style.getFilter(t)}setPaintProperty(t,e,i,a={}){return this.style.setPaintProperty(t,e,i,a),this._update(!0)}getPaintProperty(t,e){return this.style.getPaintProperty(t,e)}setLayoutProperty(t,e,i,a={}){return this.style.setLayoutProperty(t,e,i,a),this._update(!0)}getLayoutProperty(t,e){return this.style.getLayoutProperty(t,e)}setGlyphs(t,e={}){return this._lazyInitEmptyStyle(),this.style.setGlyphs(t,e),this._update(!0)}getGlyphs(){return this.style.getGlyphsUrl()}addSprite(t,e,i={}){return this._lazyInitEmptyStyle(),this.style.addSprite(t,e,i,(t=>{t||this._update(!0);})),this}removeSprite(t){return this._lazyInitEmptyStyle(),this.style.removeSprite(t),this._update(!0)}getSprite(){return this.style.getSprite()}setSprite(t,e={}){return this._lazyInitEmptyStyle(),this.style.setSprite(t,e,(t=>{t||this._update(!0);})),this}setLight(t,e={}){return this._lazyInitEmptyStyle(),this.style.setLight(t,e),this._update(!0)}getLight(){return this.style.getLight()}setSky(t){return this._lazyInitEmptyStyle(),this.style.setSky(t),this._update(!0)}getSky(){return this.style.getSky()}setFeatureState(t,e){return this.style.setFeatureState(t,e),this._update()}removeFeatureState(t,e){return this.style.removeFeatureState(t,e),this._update()}getFeatureState(t){return this.style.getFeatureState(t)}getContainer(){return this._container}getCanvasContainer(){return this._canvasContainer}getCanvas(){return this._canvas}_containerDimensions(){let t=0,e=0;return this._container&&(t=this._container.clientWidth||400,e=this._container.clientHeight||300),[t,e]}_setupContainer(){const t=this._container;t.classList.add("maplibregl-map");const e=this._canvasContainer=r.create("div","maplibregl-canvas-container",t);this._interactive&&e.classList.add("maplibregl-interactive"),this._canvas=r.create("canvas","maplibregl-canvas",e),this._canvas.addEventListener("webglcontextlost",this._contextLost,!1),this._canvas.addEventListener("webglcontextrestored",this._contextRestored,!1),this._canvas.setAttribute("tabindex",this._interactive?"0":"-1"),this._canvas.setAttribute("aria-label",this._getUIString("Map.Title")),this._canvas.setAttribute("role","region");const i=this._containerDimensions(),a=this._getClampedPixelRatio(i[0],i[1]);this._resizeCanvas(i[0],i[1],a);const s=this._controlContainer=r.create("div","maplibregl-control-container",t),o=this._controlPositions={};["top-left","top-right","bottom-left","bottom-right"].forEach((t=>{o[t]=r.create("div",`maplibregl-ctrl-${t} `,s);})),this._container.addEventListener("scroll",this._onMapScroll,!1);}_resizeCanvas(t,e,i){this._canvas.width=Math.floor(i*t),this._canvas.height=Math.floor(i*e),this._canvas.style.width=`${t}px`,this._canvas.style.height=`${e}px`;}_setupPainter(){const t={alpha:!0,stencil:!0,depth:!0,failIfMajorPerformanceCaveat:this._failIfMajorPerformanceCaveat,preserveDrawingBuffer:this._preserveDrawingBuffer,antialias:this._antialias||!1};let e=null;this._canvas.addEventListener("webglcontextcreationerror",(i=>{e={requestedAttributes:t},i&&(e.statusMessage=i.statusMessage,e.type=i.type);}),{once:!0});const i=this._canvas.getContext("webgl2",t)||this._canvas.getContext("webgl",t);if(!i){const t="Failed to initialize WebGL";throw e?(e.message=t,new Error(JSON.stringify(e))):new Error(t)}this.painter=new va(i,this.transform),n.testSupport(i);}loaded(){return !this._styleDirty&&!this._sourcesDirty&&!!this.style&&this.style.loaded()}_update(t){return this.style&&this.style._loaded?(this._styleDirty=this._styleDirty||t,this._sourcesDirty=!0,this.triggerRepaint(),this):this}_requestRenderFrame(t){return this._update(),this._renderTaskQueue.add(t)}_cancelRenderFrame(t){this._renderTaskQueue.remove(t);}_render(t){const i=this._idleTriggered?this._fadeDuration:0;if(this.painter.context.setDirty(),this.painter.setBaseState(),this._renderTaskQueue.run(t),this._removed)return;let a=!1;if(this.style&&this._styleDirty){this._styleDirty=!1;const t=this.transform.zoom,s=o.now();this.style.zoomHistory.update(t,s);const r=new e.z(t,{now:s,fadeDuration:i,zoomHistory:this.style.zoomHistory,transition:this.style.getTransition()}),n=r.crossFadingFactor();1===n&&n===this._crossFadingFactor||(a=!0,this._crossFadingFactor=n),this.style.update(r);}this.style&&this._sourcesDirty&&(this._sourcesDirty=!1,this.style._updateSources(this.transform)),this.terrain?(this.terrain.sourceCache.update(this.transform,this.terrain),this.transform.minElevationForCurrentTile=this.terrain.getMinTileElevationForLngLatZoom(this.transform.center,this.transform.tileZoom),this._elevationFreeze||(this.transform.elevation=this.terrain.getElevationForLngLatZoom(this.transform.center,this.transform.tileZoom))):(this.transform.minElevationForCurrentTile=0,this.transform.elevation=0),this._placementDirty=this.style&&this.style._updatePlacement(this.painter.transform,this.showCollisionBoxes,i,this._crossSourceCollisions),this.painter.render(this.style,{showTileBoundaries:this.showTileBoundaries,showOverdrawInspector:this._showOverdrawInspector,rotating:this.isRotating(),zooming:this.isZooming(),moving:this.isMoving(),fadeDuration:i,showPadding:this.showPadding}),this.fire(new e.k("render")),this.loaded()&&!this._loaded&&(this._loaded=!0,e.bf.mark(e.bg.load),this.fire(new e.k("load"))),this.style&&(this.style.hasTransitions()||a)&&(this._styleDirty=!0),this.style&&!this._placementDirty&&this.style._releaseSymbolFadeTiles();const s=this._sourcesDirty||this._styleDirty||this._placementDirty;return s||this._repaint?this.triggerRepaint():!this.isMoving()&&this.loaded()&&this.fire(new e.k("idle")),!this._loaded||this._fullyLoaded||s||(this._fullyLoaded=!0,e.bf.mark(e.bg.fullLoad)),this}redraw(){return this.style&&(this._frameRequest&&(this._frameRequest.abort(),this._frameRequest=null),this._render(0)),this}remove(){var t;this._hash&&this._hash.remove();for(const t of this._controls)t.onRemove(this);this._controls=[],this._frameRequest&&(this._frameRequest.abort(),this._frameRequest=null),this._renderTaskQueue.clear(),this.painter.destroy(),this.handlers.destroy(),delete this.handlers,this.setStyle(null),"undefined"!=typeof window&&removeEventListener("online",this._onWindowOnline,!1),_.removeThrottleControl(this._imageQueueHandle),null===(t=this._resizeObserver)||void 0===t||t.disconnect();const i=this.painter.context.gl.getExtension("WEBGL_lose_context");(null==i?void 0:i.loseContext)&&i.loseContext(),this._canvas.removeEventListener("webglcontextrestored",this._contextRestored,!1),this._canvas.removeEventListener("webglcontextlost",this._contextLost,!1),r.remove(this._canvasContainer),r.remove(this._controlContainer),this._container.classList.remove("maplibregl-map"),e.bf.clearMetrics(),this._removed=!0,this.fire(new e.k("remove"));}triggerRepaint(){this.style&&!this._frameRequest&&(this._frameRequest=new AbortController,o.frameAsync(this._frameRequest).then((t=>{e.bf.frame(t),this._frameRequest=null,this._render(t);})).catch((()=>{})));}get showTileBoundaries(){return !!this._showTileBoundaries}set showTileBoundaries(t){this._showTileBoundaries!==t&&(this._showTileBoundaries=t,this._update());}get showPadding(){return !!this._showPadding}set showPadding(t){this._showPadding!==t&&(this._showPadding=t,this._update());}get showCollisionBoxes(){return !!this._showCollisionBoxes}set showCollisionBoxes(t){this._showCollisionBoxes!==t&&(this._showCollisionBoxes=t,t?this.style._generateCollisionBoxes():this._update());}get showOverdrawInspector(){return !!this._showOverdrawInspector}set showOverdrawInspector(t){this._showOverdrawInspector!==t&&(this._showOverdrawInspector=t,this._update());}get repaint(){return !!this._repaint}set repaint(t){this._repaint!==t&&(this._repaint=t,this.triggerRepaint());}get vertices(){return !!this._vertices}set vertices(t){this._vertices=t,this._update();}get version(){return Ls}getCameraTargetElevation(){return this.transform.elevation}},t.MapMouseEvent=ka,t.MapTouchEvent=La,t.MapWheelEvent=Fa,t.Marker=Vs,t.NavigationControl=class{constructor(t){this._updateZoomButtons=()=>{const t=this._map.getZoom(),e=t===this._map.getMaxZoom(),i=t===this._map.getMinZoom();this._zoomInButton.disabled=e,this._zoomOutButton.disabled=i,this._zoomInButton.setAttribute("aria-disabled",e.toString()),this._zoomOutButton.setAttribute("aria-disabled",i.toString());},this._rotateCompassArrow=()=>{const t=this.options.visualizePitch?`scale(${1/Math.pow(Math.cos(this._map.transform.pitch*(Math.PI/180)),.5)}) rotateX(${this._map.transform.pitch}deg) rotateZ(${this._map.transform.angle*(180/Math.PI)}deg)`:`rotate(${this._map.transform.angle*(180/Math.PI)}deg)`;this._compassIcon.style.transform=t;},this._setButtonTitle=(t,e)=>{const i=this._map._getUIString(`NavigationControl.${e}`);t.title=i,t.setAttribute("aria-label",i);},this.options=e.e({},Os,t),this._container=r.create("div","maplibregl-ctrl maplibregl-ctrl-group"),this._container.addEventListener("contextmenu",(t=>t.preventDefault())),this.options.showZoom&&(this._zoomInButton=this._createButton("maplibregl-ctrl-zoom-in",(t=>this._map.zoomIn({},{originalEvent:t}))),r.create("span","maplibregl-ctrl-icon",this._zoomInButton).setAttribute("aria-hidden","true"),this._zoomOutButton=this._createButton("maplibregl-ctrl-zoom-out",(t=>this._map.zoomOut({},{originalEvent:t}))),r.create("span","maplibregl-ctrl-icon",this._zoomOutButton).setAttribute("aria-hidden","true")),this.options.showCompass&&(this._compass=this._createButton("maplibregl-ctrl-compass",(t=>{this.options.visualizePitch?this._map.resetNorthPitch({},{originalEvent:t}):this._map.resetNorth({},{originalEvent:t});})),this._compassIcon=r.create("span","maplibregl-ctrl-icon",this._compass),this._compassIcon.setAttribute("aria-hidden","true"));}onAdd(t){return this._map=t,this.options.showZoom&&(this._setButtonTitle(this._zoomInButton,"ZoomIn"),this._setButtonTitle(this._zoomOutButton,"ZoomOut"),this._map.on("zoom",this._updateZoomButtons),this._updateZoomButtons()),this.options.showCompass&&(this._setButtonTitle(this._compass,"ResetBearing"),this.options.visualizePitch&&this._map.on("pitch",this._rotateCompassArrow),this._map.on("rotate",this._rotateCompassArrow),this._rotateCompassArrow(),this._handler=new Ns(this._map,this._compass,this.options.visualizePitch)),this._container}onRemove(){r.remove(this._container),this.options.showZoom&&this._map.off("zoom",this._updateZoomButtons),this.options.showCompass&&(this.options.visualizePitch&&this._map.off("pitch",this._rotateCompassArrow),this._map.off("rotate",this._rotateCompassArrow),this._handler.off(),delete this._handler),delete this._map;}_createButton(t,e){const i=r.create("button",t,this._container);return i.type="button",i.addEventListener("click",e),i}},t.Popup=class extends e.E{constructor(t){super(),this.remove=()=>(this._content&&r.remove(this._content),this._container&&(r.remove(this._container),delete this._container),this._map&&(this._map.off("move",this._update),this._map.off("move",this._onClose),this._map.off("click",this._onClose),this._map.off("remove",this.remove),this._map.off("mousemove",this._onMouseMove),this._map.off("mouseup",this._onMouseUp),this._map.off("drag",this._onDrag),this._map._canvasContainer.classList.remove("maplibregl-track-pointer"),delete this._map,this.fire(new e.k("close"))),this),this._onMouseUp=t=>{this._update(t.point);},this._onMouseMove=t=>{this._update(t.point);},this._onDrag=t=>{this._update(t.point);},this._update=t=>{var e;if(!this._map||!this._lngLat&&!this._trackPointer||!this._content)return;if(!this._container){if(this._container=r.create("div","maplibregl-popup",this._map.getContainer()),this._tip=r.create("div","maplibregl-popup-tip",this._container),this._container.appendChild(this._content),this.options.className)for(const t of this.options.className.split(" "))this._container.classList.add(t);this._closeButton&&this._closeButton.setAttribute("aria-label",this._map._getUIString("Popup.Close")),this._trackPointer&&this._container.classList.add("maplibregl-popup-track-pointer");}if(this.options.maxWidth&&this._container.style.maxWidth!==this.options.maxWidth&&(this._container.style.maxWidth=this.options.maxWidth),this._lngLat=this._map.transform.renderWorldCopies&&!this._trackPointer?js(this._lngLat,this._flatPos,this._map.transform):null===(e=this._lngLat)||void 0===e?void 0:e.wrap(),this._trackPointer&&!t)return;const i=this._flatPos=this._pos=this._trackPointer&&t?t:this._map.project(this._lngLat);this._map.terrain&&(this._flatPos=this._trackPointer&&t?t:this._map.transform.locationPoint(this._lngLat));let a=this.options.anchor;const s=Qs(this.options.offset);if(!a){const t=this._container.offsetWidth,e=this._container.offsetHeight;let o;o=i.y+s.bottom.ythis._map.transform.height-e?["bottom"]:[],i.xthis._map.transform.width-t/2&&o.push("right"),a=0===o.length?"bottom":o.join("-");}let o=i.add(s[a]);this.options.subpixelPositioning||(o=o.round()),r.setTransform(this._container,`${Zs[a]} translate(${o.x}px,${o.y}px)`),qs(this._container,a,"popup");},this._onClose=()=>{this.remove();},this.options=e.e(Object.create(Js),t);}addTo(t){return this._map&&this.remove(),this._map=t,this.options.closeOnClick&&this._map.on("click",this._onClose),this.options.closeOnMove&&this._map.on("move",this._onClose),this._map.on("remove",this.remove),this._update(),this._focusFirstElement(),this._trackPointer?(this._map.on("mousemove",this._onMouseMove),this._map.on("mouseup",this._onMouseUp),this._container&&this._container.classList.add("maplibregl-popup-track-pointer"),this._map._canvasContainer.classList.add("maplibregl-track-pointer")):this._map.on("move",this._update),this.fire(new e.k("open")),this}isOpen(){return !!this._map}getLngLat(){return this._lngLat}setLngLat(t){return this._lngLat=e.N.convert(t),this._pos=null,this._flatPos=null,this._trackPointer=!1,this._update(),this._map&&(this._map.on("move",this._update),this._map.off("mousemove",this._onMouseMove),this._container&&this._container.classList.remove("maplibregl-popup-track-pointer"),this._map._canvasContainer.classList.remove("maplibregl-track-pointer")),this}trackPointer(){return this._trackPointer=!0,this._pos=null,this._flatPos=null,this._update(),this._map&&(this._map.off("move",this._update),this._map.on("mousemove",this._onMouseMove),this._map.on("drag",this._onDrag),this._container&&this._container.classList.add("maplibregl-popup-track-pointer"),this._map._canvasContainer.classList.add("maplibregl-track-pointer")),this}getElement(){return this._container}setText(t){return this.setDOMContent(document.createTextNode(t))}setHTML(t){const e=document.createDocumentFragment(),i=document.createElement("body");let a;for(i.innerHTML=t;a=i.firstChild,a;)e.appendChild(a);return this.setDOMContent(e)}getMaxWidth(){var t;return null===(t=this._container)||void 0===t?void 0:t.style.maxWidth}setMaxWidth(t){return this.options.maxWidth=t,this._update(),this}setDOMContent(t){if(this._content)for(;this._content.hasChildNodes();)this._content.firstChild&&this._content.removeChild(this._content.firstChild);else this._content=r.create("div","maplibregl-popup-content",this._container);return this._content.appendChild(t),this._createCloseButton(),this._update(),this._focusFirstElement(),this}addClassName(t){return this._container&&this._container.classList.add(t),this}removeClassName(t){return this._container&&this._container.classList.remove(t),this}setOffset(t){return this.options.offset=t,this._update(),this}toggleClassName(t){if(this._container)return this._container.classList.toggle(t)}setSubpixelPositioning(t){this.options.subpixelPositioning=t;}_createCloseButton(){this.options.closeButton&&(this._closeButton=r.create("button","maplibregl-popup-close-button",this._content),this._closeButton.type="button",this._closeButton.innerHTML="×",this._closeButton.addEventListener("click",this._onClose));}_focusFirstElement(){if(!this.options.focusAfterOpen||!this._container)return;const t=this._container.querySelector(Ys);t&&t.focus();}},t.RasterDEMTileSource=K,t.RasterTileSource=X,t.ScaleControl=class{constructor(t){this._onMove=()=>{Xs(this._map,this._container,this.options);},this.setUnit=t=>{this.options.unit=t,Xs(this._map,this._container,this.options);},this.options=Object.assign(Object.assign({},$s),t);}getDefaultPosition(){return "bottom-left"}onAdd(t){return this._map=t,this._container=r.create("div","maplibregl-ctrl maplibregl-ctrl-scale",t.getContainer()),this._map.on("move",this._onMove),this._onMove(),this._container}onRemove(){r.remove(this._container),this._map.off("move",this._onMove),this._map=void 0;}},t.ScrollZoomHandler=us,t.Style=de,t.TerrainControl=class{constructor(t){this._toggleTerrain=()=>{this._map.getTerrain()?this._map.setTerrain(null):this._map.setTerrain(this.options),this._updateTerrainIcon();},this._updateTerrainIcon=()=>{this._terrainButton.classList.remove("maplibregl-ctrl-terrain"),this._terrainButton.classList.remove("maplibregl-ctrl-terrain-enabled"),this._map.terrain?(this._terrainButton.classList.add("maplibregl-ctrl-terrain-enabled"),this._terrainButton.title=this._map._getUIString("TerrainControl.Disable")):(this._terrainButton.classList.add("maplibregl-ctrl-terrain"),this._terrainButton.title=this._map._getUIString("TerrainControl.Enable"));},this.options=t;}onAdd(t){return this._map=t,this._container=r.create("div","maplibregl-ctrl maplibregl-ctrl-group"),this._terrainButton=r.create("button","maplibregl-ctrl-terrain",this._container),r.create("span","maplibregl-ctrl-icon",this._terrainButton).setAttribute("aria-hidden","true"),this._terrainButton.type="button",this._terrainButton.addEventListener("click",this._toggleTerrain),this._updateTerrainIcon(),this._map.on("terrain",this._updateTerrainIcon),this._container}onRemove(){r.remove(this._container),this._map.off("terrain",this._updateTerrainIcon),this._map=void 0;}},t.TwoFingersTouchPitchHandler=rs,t.TwoFingersTouchRotateHandler=ss,t.TwoFingersTouchZoomHandler=is,t.TwoFingersTouchZoomRotateHandler=gs,t.VectorTileSource=$,t.VideoSource=tt,t.addSourceType=(t,i)=>e._(void 0,void 0,void 0,(function*(){if(at(t))throw new Error(`A source type called "${t}" already exists.`);((t,e)=>{it[t]=e;})(t,i);})),t.clearPrewarmedResources=function(){const t=B;t&&(t.isPreloaded()&&1===t.numActive()?(t.release(k),B=null):console.warn("Could not clear WebWorkers since there are active Map instances that still reference it. The pre-warmed WebWorker pool can only be cleared when all map instances have been removed with map.remove()"));},t.getMaxParallelImageRequests=function(){return e.a.MAX_PARALLEL_IMAGE_REQUESTS},t.getRTLTextPluginStatus=function(){return nt().getRTLTextPluginStatus()},t.getVersion=function(){return to},t.getWorkerCount=function(){return L.workerCount},t.getWorkerUrl=function(){return e.a.WORKER_URL},t.importScriptInWorkers=function(t){return j().broadcast("IS",t)},t.prewarm=function(){N().acquire(k);},t.setMaxParallelImageRequests=function(t){e.a.MAX_PARALLEL_IMAGE_REQUESTS=t;},t.setRTLTextPlugin=function(t,e){return nt().setRTLTextPlugin(t,e)},t.setWorkerCount=function(t){L.workerCount=t;},t.setWorkerUrl=function(t){e.a.WORKER_URL=t;};})); + +// +// Our custom intro provides a specialized "define()" function, called by the +// AMD modules below, that sets up the worker blob URL and then executes the +// main module, storing its exported value as 'maplibregl' + + +var maplibregl$1 = maplibregl; + +return maplibregl$1; + +})); +//# sourceMappingURL=maplibre-gl.js.map diff --git a/src/app/resources/map/trajectory_map.html b/src/app/resources/map/trajectory_map.html new file mode 100644 index 0000000..043cf27 --- /dev/null +++ b/src/app/resources/map/trajectory_map.html @@ -0,0 +1,363 @@ + + + + + + 电极轨迹地图 + + + + +
+ + + + diff --git a/src/controller/CMakeLists.txt b/src/controller/CMakeLists.txt index 45f34d7..0bbe490 100644 --- a/src/controller/CMakeLists.txt +++ b/src/controller/CMakeLists.txt @@ -1,6 +1,7 @@ find_package(Qt6 COMPONENTS Core REQUIRED) add_library(geopro_controller STATIC - WorkbenchNavController.cpp) + WorkbenchNavController.cpp + DatasetDetailController.cpp) target_include_directories(geopro_controller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(geopro_controller PUBLIC geopro_data Qt6::Core) target_compile_features(geopro_controller PUBLIC cxx_std_17) diff --git a/src/controller/DatasetDetailController.cpp b/src/controller/DatasetDetailController.cpp new file mode 100644 index 0000000..5706aa3 --- /dev/null +++ b/src/controller/DatasetDetailController.cpp @@ -0,0 +1,87 @@ +#include "DatasetDetailController.hpp" +#include +#include +#include "repo/IAsyncDatasetRepository.hpp" +#include "api/DatasetLoadHandles.hpp" +namespace geopro::controller { + +DatasetDetailController::DatasetDetailController(data::IAsyncDatasetRepository& repo, + ChartStrategyRegistry& registry, QObject* parent) + : QObject(parent), repo_(repo), registry_(registry) { + // QSignalSpy / 队列连接对自定义类型需注册(页签集随 datasetOpened 传递)。 + qRegisterMetaType>("std::vector"); +} + +DatasetDetailController::~DatasetDetailController() { + // 退出契约:abort 全部在飞句柄,不依赖外部析构顺序兜底。 + for (auto& load : inflight_) + if (load) load->abort(); +} + +void DatasetDetailController::openDataset(const QString& dsId, const QString& ddCode, + const QString& dsName) { + qInfo("[detail] openDataset id=%s ddCode=%s name=%s", qUtf8Printable(dsId), + qUtf8Printable(ddCode), qUtf8Printable(dsName)); + auto* s = registry_.find(ddCode.toStdString()); + if (!s) { // 未注册策略 → 优雅降级 + qWarning("[detail] 未注册策略 ddCode=%s → 降级提示", qUtf8Printable(ddCode)); + emit loadFailed(dsId, QStringLiteral("暂不支持该数据类型的预览")); + return; + } + const std::vector tabs = s->tabs(); + emit datasetOpened(dsId, ddCode, dsName, tabs); + for (int i = 0; i < static_cast(tabs.size()); ++i) + if (!tabs[static_cast(i)].lazy) loadTab(dsId, ddCode, i); +} + +void DatasetDetailController::loadTab(const QString& dsId, const QString& ddCode, int tabIndex) { + loadTabImpl(dsId, ddCode, tabIndex, /*pageNo*/ 1, /*pageSize*/ 0); +} + +void DatasetDetailController::loadTabPaged(const QString& dsId, const QString& ddCode, int tabIndex, + int pageNo, int pageSize) { + loadTabImpl(dsId, ddCode, tabIndex, pageNo, pageSize); +} + +void DatasetDetailController::loadTabImpl(const QString& dsId, const QString& ddCode, int tabIndex, + int pageNo, int pageSize) { + auto* s = registry_.find(ddCode.toStdString()); + if (!s) return; // 策略消失(不应发生):静默不加载 + const std::vector tabs = s->tabs(); + if (tabIndex < 0 || tabIndex >= static_cast(tabs.size())) return; + const controller::TabSpec& spec = tabs[static_cast(tabIndex)]; + + // loadAsync 对未知 loaderKey 抛 std::runtime_error;若逃逸槽函数会被 GuardedApplication + // 吞掉、遮罩永久悬挂(文档化的崩溃/挂起类)。就地兜底为 loadFailed,且不留半注册的在飞句柄。 + data::DetailLoad* load = nullptr; + try { + load = repo_.loadAsync(spec.loaderKey.toStdString(), dsId.toStdString(), pageNo, pageSize); + } catch (const std::exception& e) { + qWarning("[detail] loadAsync 失败 id=%s tab=%d key=%s: %s", qUtf8Printable(dsId), tabIndex, + qUtf8Printable(spec.loaderKey), e.what()); + emit loadFailed(dsId, QStringLiteral("暂不支持该数据类型的预览")); + return; + } + + if (auto& prev = inflight_[tabIndex]) prev->abort(); // abort-and-replace 该槽位 + inflight_[tabIndex] = load; + emit tabLoadStarted(dsId, tabIndex); + + QObject::connect(load, &data::DetailLoad::done, this, + [this, load, dsId, tabIndex](const QVariant& payload) { + if (load != inflight_.value(tabIndex)) return; // §5.0 句柄身份比对:丢弃迟到信号 + inflight_.remove(tabIndex); + emit tabReady(dsId, tabIndex, payload); + }); + QObject::connect(load, &data::DetailLoad::failed, this, + [this, load, dsId, tabIndex](const QString& msg) { + if (load != inflight_.value(tabIndex)) return; + inflight_.remove(tabIndex); + qWarning("[detail] 页签加载失败 id=%s tab=%d: %s", qUtf8Printable(dsId), tabIndex, + qUtf8Printable(msg)); + emit loadFailed(dsId, msg); + }); +} + +void DatasetDetailController::focusDataset(const QString& dsId) { emit focusRequested(dsId); } +} // namespace geopro::controller diff --git a/src/controller/DatasetDetailController.hpp b/src/controller/DatasetDetailController.hpp new file mode 100644 index 0000000..c3f6aaf --- /dev/null +++ b/src/controller/DatasetDetailController.hpp @@ -0,0 +1,48 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include "DatasetDetailTab.hpp" +#include "IDatasetChartStrategy.hpp" +namespace geopro::data { class IAsyncDatasetRepository; class DetailLoad; } +namespace geopro::controller { + +// 数据详情通用 tab 引擎编排:双击/页签激活 → 按 loaderKey 异步拉载荷(QVariant) → 发信号给详情面板。 +// 无 per-ddCode 分支:页签集由策略 tabs() 描述,载荷经 QVariant 类型擦除。被动视图。 +class DatasetDetailController : public QObject { + Q_OBJECT +public: + DatasetDetailController(data::IAsyncDatasetRepository& repo, + ChartStrategyRegistry& registry, QObject* parent = nullptr); + ~DatasetDetailController() override; // 退出契约(spec §7):abort 全部在飞句柄,避免迟到信号打到已析构 this +public slots: + // 打开数据集:查策略 → datasetOpened(页签集) → 对每个非 lazy 页签发起 loadTab。 + void openDataset(const QString& dsId, const QString& ddCode, const QString& dsName = QString()); + // 加载某页签(lazy 页签首次激活时由壳触发;非 lazy 由 openDataset 自动触发)。 + // 分页型页签(如 dd_grid 列表)首载用默认页(pageNo=1/pageSize=0 → 仓储解析默认每页条数)。 + void loadTab(const QString& dsId, const QString& ddCode, int tabIndex); + // 分页加载某页签(分页器翻页/改每页条数时由壳触发)。pageSize=0 → 仓储用该类型默认值。 + void loadTabPaged(const QString& dsId, const QString& ddCode, int tabIndex, int pageNo, + int pageSize); + void focusDataset(const QString& dsId); +signals: + void datasetOpened(const QString& dsId, const QString& ddCode, const QString& dsName, + const std::vector& tabs); + void tabLoadStarted(const QString& dsId, int tabIndex); + void tabReady(const QString& dsId, int tabIndex, const QVariant& payload); + void loadFailed(const QString& dsId, const QString& message); + void focusRequested(const QString& dsId); +private: + // loadTab/loadTabPaged 共用实现:按 (dsId,ddCode,tabIndex) 查 loaderKey,带分页参数异步加载。 + void loadTabImpl(const QString& dsId, const QString& ddCode, int tabIndex, int pageNo, + int pageSize); + + data::IAsyncDatasetRepository& repo_; + ChartStrategyRegistry& registry_; + QMap> inflight_; // 按页签槽位的在飞句柄(§5.0 身份比对) +}; +} // namespace geopro::controller diff --git a/src/controller/DatasetDetailTab.hpp b/src/controller/DatasetDetailTab.hpp new file mode 100644 index 0000000..2360d2e --- /dev/null +++ b/src/controller/DatasetDetailTab.hpp @@ -0,0 +1,25 @@ +#pragma once +#include +#include +#include + +namespace geopro::controller { + +// 详情页签的渲染 kind 全集(Image/GISMap 待 GPR/radar 有活样本再加,YAGNI)。 +// WebMap = trajectory 地图页签占位(真实 GIS 地图待 Qt WebEngine 模块安装后接 QWebEngineView 替换)。 +enum class ViewKind { Scatter, FilledContour, Bar, LineProfile, PolylineMap, Table, WebMap }; + +// 页签描述符:策略声明每个 dd 类型的页签集(标题/kind/加载键/惰性/分页)。 +struct TabSpec { + QString title; + ViewKind kind; + QString loaderKey; + bool lazy = false; + bool paginated = false; +}; + +} // namespace geopro::controller + +// QSignalSpy 对 datasetOpened(..., std::vector) 需注册的元类型。 +Q_DECLARE_METATYPE(geopro::controller::TabSpec) +Q_DECLARE_METATYPE(std::vector) diff --git a/src/controller/IDatasetChartStrategy.hpp b/src/controller/IDatasetChartStrategy.hpp new file mode 100644 index 0000000..4c7aa3c --- /dev/null +++ b/src/controller/IDatasetChartStrategy.hpp @@ -0,0 +1,41 @@ +#pragma once +#include +#include +#include +#include +#include "DatasetDetailTab.hpp" +namespace geopro::controller { + +// dd 类型驱动的图表策略:决定某 ddCode 的详情页如何加载/渲染。 +struct IDatasetChartStrategy { + virtual ~IDatasetChartStrategy() = default; + virtual std::string ddCode() const = 0; + // 该类型的页签集(标题/render kind/加载键/惰性/分页)。控制器据此建页签 + 驱动加载, + // 取代硬编码的 hasGridPhase()/ddCode 判断。 + virtual std::vector tabs() const = 0; +}; + +class ChartStrategyRegistry { +public: + ChartStrategyRegistry() = default; + // 禁拷贝(含 unique_ptr,本就不可拷贝;显式 delete 让意图清晰)。 + // 保留移动:map 移动只搬节点,find() 返回的裸指针指向的策略对象地址不变、仍有效; + // 且测试 makeInversionRegistry() 按值返回需要移动。 + ChartStrategyRegistry(const ChartStrategyRegistry&) = delete; + ChartStrategyRegistry& operator=(const ChartStrategyRegistry&) = delete; + ChartStrategyRegistry(ChartStrategyRegistry&&) = default; + ChartStrategyRegistry& operator=(ChartStrategyRegistry&&) = default; + + void add(std::unique_ptr s) { + const std::string code = s->ddCode(); + map_[code] = std::move(s); + } + IDatasetChartStrategy* find(const std::string& ddCode) const { + auto it = map_.find(ddCode); + return it == map_.end() ? nullptr : it->second.get(); + } + bool supports(const std::string& ddCode) const { return map_.count(ddCode) > 0; } +private: + std::map> map_; +}; +} // namespace geopro::controller diff --git a/src/controller/WorkbenchNavController.cpp b/src/controller/WorkbenchNavController.cpp index 2fabdcb..9993240 100644 --- a/src/controller/WorkbenchNavController.cpp +++ b/src/controller/WorkbenchNavController.cpp @@ -1,192 +1,445 @@ #include "WorkbenchNavController.hpp" -#include +#include +#include +#include +#include +#include "api/NavLoads.hpp" +#include "api/NavRequest.hpp" #include "dto/NavDto.hpp" +#include "repo/IAsyncProjectRepository.hpp" namespace geopro::controller { +namespace { +// 数据页树形:一次取全的大 pageSize(远超单 TM 实际 DS 数;超出会日志告警)+ 每页根节点数。 +constexpr int kFetchAllPageSize = 1000; +constexpr int kDataRootPageSize = 5; +} // namespace + +using data::DsPage; +using data::DsRow; +using data::DynamicForm; +using data::ExceptionRow; +using data::NavRequest; +using data::ProjectListPage; using data::ProjectSummary; +using data::StructNode; using data::Workspace; -WorkbenchNavController::WorkbenchNavController(data::IProjectRepository& repo, QObject* parent) +WorkbenchNavController::WorkbenchNavController(data::IAsyncProjectRepository& repo, QObject* parent) : QObject(parent), repo_(repo) {} -// RAII:进入公共导航操作时置忙(驱动等待光标),任何返回路径都复位——保证 busyChanged 配平。 -// 命名(非匿名)以匹配 controller 的 friend 声明,从而在析构时排空挂起的勾选请求。 -struct BusyGuard { - WorkbenchNavController* self; - bool* busy; - BusyGuard(WorkbenchNavController* s, bool* b) : self(s), busy(b) { - *busy = true; - emit self->busyChanged(true); - } - ~BusyGuard() { - WorkbenchNavController* ctrl = self; // 取本地副本:lambda 不能捕获成员 - *busy = false; - emit ctrl->busyChanged(false); - // 触发源是延迟合并发射,可能落在嵌套事件循环里:用队列调用在调用栈/嵌套循环展开后再排空, - // 那时 busy_ 已可靠为 false,重放才会真正执行(lambda 捕获的 ctrl 生命周期与应用一致,安全)。 - if (ctrl->checkedTmsPending_) - QMetaObject::invokeMethod( - ctrl, [ctrl] { ctrl->drainPendingCheckedTms(); }, Qt::QueuedConnection); - } -}; +WorkbenchNavController::~WorkbenchNavController() { abortAll(); } -void WorkbenchNavController::start() { - if (busy_) return; - BusyGuard guard(this, &busy_); - const auto ws = repo_.listWorkspaces(); - if (!ws.ok) { - emit loadFailed(QStringLiteral("workspaces"), QString::fromStdString(ws.error)); - return; - } - QString cur; - for (const auto& w : ws.value) - if (w.isCurrent) cur = QString::fromStdString(w.id); - if (cur.isEmpty() && !ws.value.empty()) cur = QString::fromStdString(ws.value.front().id); - currentWorkspaceId_ = cur.toStdString(); - emit workspacesLoaded(ws.value, cur); - loadProjectsAndStructure(); +bool WorkbenchNavController::anyInflight() const { + if (startStepReq_ || structReq_ || selDataReq_ || selFileReq_ || selDetailReq_ || + moreFilesReq_ || datasetReq_) + return true; + for (const auto& h : checkedInflight_) + if (h) return true; + return false; } -void WorkbenchNavController::loadProjectsAndStructure() { - const auto ps = repo_.pageProjects(std::string(), std::string(), 1, 10); // 下拉首页 10 - if (!ps.ok) { - emit loadFailed(QStringLiteral("projects"), QString::fromStdString(ps.error)); - return; +void WorkbenchNavController::emitBusyIfChanged() { + const bool now = anyInflight(); + if (now != lastBusy_) { + lastBusy_ = now; + emit busyChanged(now); } - lastProjects_ = ps.value.rows; - tmExceptionCache_.clear(); - currentParentId_.clear(); // 切项目/工作空间重置选中态(spec §6) - currentParentConfType_ = 0; - checkedTmsPending_ = false; // 丢弃跨项目的陈旧挂起重放 - pendingCheckedTms_.clear(); - QString curP; - if (!ps.value.rows.empty()) { - const auto& first = ps.value.rows.front(); - curP = QString::fromStdString(first.id); - currentProjectId_ = first.id; - currentProjectName_ = first.name; - currentCrsCode_ = first.crsCode; - } else { - currentProjectId_.clear(); - currentProjectName_.clear(); - currentCrsCode_.clear(); - } - emit projectsLoaded(ps.value.rows, curP, ps.value.total); +} - if (curP.isEmpty()) { - lastStructNodes_.clear(); - emit structureLoaded(QString(), {}); // 暂无项目 → 空树 - return; - } - const auto st = repo_.loadStructure(currentProjectId_); - if (!st.ok) { - emit loadFailed(QStringLiteral("structure"), QString::fromStdString(st.error)); - return; - } - lastStructNodes_ = st.value; - emit structureLoaded(QString::fromStdString(currentProjectName_), st.value); +void WorkbenchNavController::abortAll() { + if (startStepReq_) startStepReq_->abort(); + if (structReq_) structReq_->abort(); + if (selDataReq_) selDataReq_->abort(); + if (selFileReq_) selFileReq_->abort(); + if (selDetailReq_) selDetailReq_->abort(); + if (moreFilesReq_) moreFilesReq_->abort(); + if (datasetReq_) datasetReq_->abort(); + for (const auto& h : checkedInflight_) + if (h) h->abort(); + checkedInflight_.clear(); +} + +void WorkbenchNavController::resetSelectionState() { + tmExceptionCache_.clear(); + currentParentId_.clear(); + currentParentConfType_ = 0; + allDataRows_.clear(); + dataRootsShown_ = 0; + dataTotal_ = 0; +} + +// ── start / switchWorkspace 依赖链:listWorkspaces → pageProjects → loadStructure ── +// 用 NavRequest 续延(每级 done 内用业务值构造下一级),startStepReq_ 跟踪当前在飞级。 +// abort-and-replace:start/switchWorkspace 入口 abort 旧 startStepReq_ → 自然丢弃旧链迟到信号。 + +void WorkbenchNavController::start() { + if (startStepReq_) startStepReq_->abort(); + NavRequest* req = repo_.listWorkspacesAsync(); + startStepReq_ = req; + emitBusyIfChanged(); + QObject::connect(req, &NavRequest::done, this, [this, req](const QVariant& v) { + if (req != startStepReq_) return; // §5.0 身份比对 + startStepReq_.clear(); + const auto ws = qvariant_cast>(v); + QString cur; + for (const auto& w : ws) + if (w.isCurrent) cur = QString::fromStdString(w.id); + if (cur.isEmpty() && !ws.empty()) cur = QString::fromStdString(ws.front().id); + currentWorkspaceId_ = cur.toStdString(); + emit workspacesLoaded(ws, cur); + runProjectsAndStructure(); + }); + QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) { + if (req != startStepReq_) return; + startStepReq_.clear(); + emit loadFailed(QStringLiteral("workspaces"), msg); + emitBusyIfChanged(); + }); } void WorkbenchNavController::switchWorkspace(const QString& tenantId) { - if (tenantId.isEmpty() || busy_) return; - BusyGuard guard(this, &busy_); - const auto r = repo_.switchWorkspace(tenantId.toStdString()); - if (!r.ok) { - emit loadFailed(QStringLiteral("switchWorkspace"), QString::fromStdString(r.error)); - return; - } - currentWorkspaceId_ = tenantId.toStdString(); - loadProjectsAndStructure(); + if (tenantId.isEmpty()) return; + if (startStepReq_) startStepReq_->abort(); + NavRequest* req = repo_.switchWorkspaceAsync(tenantId.toStdString()); + startStepReq_ = req; + emitBusyIfChanged(); + const std::string tid = tenantId.toStdString(); + QObject::connect(req, &NavRequest::done, this, [this, req, tid](const QVariant&) { + if (req != startStepReq_) return; + startStepReq_.clear(); + currentWorkspaceId_ = tid; + runProjectsAndStructure(); + }); + QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) { + if (req != startStepReq_) return; + startStepReq_.clear(); + emit loadFailed(QStringLiteral("switchWorkspace"), msg); + emitBusyIfChanged(); + }); } +void WorkbenchNavController::runProjectsAndStructure() { + NavRequest* req = repo_.pageProjectsAsync(std::string(), std::string(), 1, 10); // 下拉首页 10 + startStepReq_ = req; + emitBusyIfChanged(); + QObject::connect(req, &NavRequest::done, this, [this, req](const QVariant& v) { + if (req != startStepReq_) return; + startStepReq_.clear(); + const auto page = qvariant_cast(v); + lastProjects_ = page.rows; + resetSelectionState(); + QString curP; + if (!page.rows.empty()) { + const auto& first = page.rows.front(); + curP = QString::fromStdString(first.id); + currentProjectId_ = first.id; + currentProjectName_ = first.name; + currentCrsCode_ = first.crsCode; + } else { + currentProjectId_.clear(); + currentProjectName_.clear(); + currentCrsCode_.clear(); + } + emit projectsLoaded(page.rows, curP, page.total); + if (curP.isEmpty()) { + lastStructNodes_.clear(); + emit structureLoaded(QString(), {}); // 暂无项目 → 空树 + emitBusyIfChanged(); + return; + } + NavRequest* st = repo_.loadStructureAsync(currentProjectId_); + startStepReq_ = st; + emitBusyIfChanged(); + QObject::connect(st, &NavRequest::done, this, [this, st](const QVariant& sv) { + if (st != startStepReq_) return; + startStepReq_.clear(); + const auto nodes = qvariant_cast>(sv); + lastStructNodes_ = nodes; + emit structureLoaded(QString::fromStdString(currentProjectName_), nodes); + emitBusyIfChanged(); + }); + QObject::connect(st, &NavRequest::failed, this, [this, st](const QString& msg) { + if (st != startStepReq_) return; + startStepReq_.clear(); + emit loadFailed(QStringLiteral("structure"), msg); + emitBusyIfChanged(); + }); + }); + QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) { + if (req != startStepReq_) return; + startStepReq_.clear(); + emit loadFailed(QStringLiteral("projects"), msg); + emitBusyIfChanged(); + }); +} + +// ── switchProject:单请求 loadStructure ── void WorkbenchNavController::switchProject(const QString& projectId) { - if (projectId.isEmpty() || busy_) return; - BusyGuard guard(this, &busy_); + if (projectId.isEmpty()) return; currentProjectId_ = projectId.toStdString(); for (const auto& p : lastProjects_) if (p.id == currentProjectId_) { currentProjectName_ = p.name; currentCrsCode_ = p.crsCode; } - const auto st = repo_.loadStructure(currentProjectId_); - if (!st.ok) { - emit loadFailed(QStringLiteral("structure"), QString::fromStdString(st.error)); - return; - } - lastStructNodes_ = st.value; - tmExceptionCache_.clear(); - currentParentId_.clear(); // 切项目/工作空间重置选中态(spec §6) - currentParentConfType_ = 0; - checkedTmsPending_ = false; // 丢弃跨项目的陈旧挂起重放 - pendingCheckedTms_.clear(); - emit structureLoaded(QString::fromStdString(currentProjectName_), st.value); + if (structReq_) structReq_->abort(); // abort-and-replace + NavRequest* req = repo_.loadStructureAsync(currentProjectId_); + structReq_ = req; + emitBusyIfChanged(); + QObject::connect(req, &NavRequest::done, this, [this, req](const QVariant& v) { + if (req != structReq_) return; // §5.0 身份比对 + structReq_.clear(); + const auto nodes = qvariant_cast>(v); + lastStructNodes_ = nodes; + resetSelectionState(); + emit structureLoaded(QString::fromStdString(currentProjectName_), nodes); + emitBusyIfChanged(); + }); + QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) { + if (req != structReq_) return; + structReq_.clear(); + emit loadFailed(QStringLiteral("structure"), msg); + emitBusyIfChanged(); + }); } +// ── selectObject:三并发(data 行 / file 行 / 对象详情),各自身份比对、独立 emit ── void WorkbenchNavController::selectObject(const QString& objectId, int confType) { - if (objectId.isEmpty() || busy_) return; - BusyGuard guard(this, &busy_); + if (objectId.isEmpty()) return; + if (selDataReq_) selDataReq_->abort(); // abort-and-replace 三路 + if (selFileReq_) selFileReq_->abort(); + if (selDetailReq_) selDetailReq_->abort(); currentParentId_ = objectId.toStdString(); currentParentConfType_ = confType; const std::string pid = currentProjectId_; dataPageNo_ = 1; filePageNo_ = 1; - const auto d = repo_.loadRows(pid, currentParentId_, confType, 3, dataPageNo_); - if (!d.ok) { - emit loadFailed(QStringLiteral("datasets"), QString::fromStdString(d.error)); - return; - } - dataTotal_ = d.value.total; - emit datasetsLoaded(objectId, d.value.rows, d.value.total, false); - const auto f = repo_.loadRows(pid, currentParentId_, confType, 1, filePageNo_); - if (!f.ok) { - emit loadFailed(QStringLiteral("files"), QString::fromStdString(f.error)); - return; - } - fileTotal_ = f.value.total; - emit filesLoaded(objectId, f.value.rows, f.value.total, false); + allDataRows_.clear(); + dataRootsShown_ = 0; - const auto detail = repo_.loadObjectDetail(currentParentId_, confType); - if (!detail.ok) { - emit loadFailed(QStringLiteral("objectDetail"), QString::fromStdString(detail.error)); - return; - } - emit objectDetailLoaded(objectId, detail.value); + // 数据页:一次取全(大 pageSize),再按根客户端分页——保证树完整(子节点不会跨服务端分页丢失父)。 + NavRequest* dReq = repo_.loadRowsAsync(pid, currentParentId_, confType, 3, 1, kFetchAllPageSize); + selDataReq_ = dReq; + NavRequest* fReq = repo_.loadRowsAsync(pid, currentParentId_, confType, 1, filePageNo_); + selFileReq_ = fReq; + NavRequest* detReq = repo_.loadObjectDetailAsync(currentParentId_, confType); + selDetailReq_ = detReq; + emitBusyIfChanged(); + + QObject::connect(dReq, &NavRequest::done, this, [this, dReq, objectId](const QVariant& v) { + if (dReq != selDataReq_) return; + selDataReq_.clear(); + const auto page = qvariant_cast(v); + if (static_cast(page.rows.size()) < page.total) + qWarning() << "[nav] data/page 未取全:listCount=" << page.rows.size() + << " total=" << page.total << " → 树可能不完整(pageSize 不足)"; + allDataRows_ = page.rows; // 全量缓存;按根分页由 emitNextDataRootPage 切 + dataRootsShown_ = 0; + emitNextDataRootPage(false); // 首页(append=false) + emitBusyIfChanged(); + }); + QObject::connect(dReq, &NavRequest::failed, this, [this, dReq](const QString& msg) { + if (dReq != selDataReq_) return; + selDataReq_.clear(); + emit loadFailed(QStringLiteral("datasets"), msg); + emitBusyIfChanged(); + }); + QObject::connect(fReq, &NavRequest::done, this, [this, fReq, objectId](const QVariant& v) { + if (fReq != selFileReq_) return; + selFileReq_.clear(); + const auto page = qvariant_cast(v); + fileTotal_ = page.total; + emit filesLoaded(objectId, page.rows, page.total, false); + emitBusyIfChanged(); + }); + QObject::connect(fReq, &NavRequest::failed, this, [this, fReq](const QString& msg) { + if (fReq != selFileReq_) return; + selFileReq_.clear(); + emit loadFailed(QStringLiteral("files"), msg); + emitBusyIfChanged(); + }); + QObject::connect(detReq, &NavRequest::done, this, [this, detReq, objectId](const QVariant& v) { + if (detReq != selDetailReq_) return; + selDetailReq_.clear(); + emit objectDetailLoaded(objectId, qvariant_cast(v)); + emitBusyIfChanged(); + }); + QObject::connect(detReq, &NavRequest::failed, this, [this, detReq](const QString& msg) { + if (detReq != selDetailReq_) return; + selDetailReq_.clear(); + emit loadFailed(QStringLiteral("objectDetail"), msg); + emitBusyIfChanged(); + }); } -void WorkbenchNavController::loadMoreData() { - if (currentParentId_.empty() || busy_) return; - BusyGuard guard(this, &busy_); - const auto d = repo_.loadRows(currentProjectId_, currentParentId_, currentParentConfType_, 3, ++dataPageNo_); - if (!d.ok) { - emit loadFailed(QStringLiteral("datasets"), QString::fromStdString(d.error)); - return; +// ── 数据页树形分页:从 allDataRows_ 按根切下一页(同步,无请求)── +// 根 = parentId 为空或不在本 TM 全量集合内(其父是源文件节点,不在 data/page 返回里)。 +// 每页取 kDataRootPageSize 个根 + 各自整棵子树;行序保持后端原序(便于稳定显示)。 +void WorkbenchNavController::emitNextDataRootPage(bool append) { + const QString parent = QString::fromStdString(currentParentId_); + // 本 TM 全部行 id 集合(判定谁是根)。 + std::unordered_set ids; + ids.reserve(allDataRows_.size()); + for (const auto& r : allDataRows_) ids.insert(r.id); + // 根索引(按原序)+ parentId→子索引表。 + std::vector rootIdx; + std::unordered_map> kids; + for (std::size_t i = 0; i < allDataRows_.size(); ++i) { + const std::string& p = allDataRows_[i].parentId; + if (p.empty() || ids.find(p) == ids.end()) + rootIdx.push_back(i); + else + kids[p].push_back(i); } - dataTotal_ = d.value.total; - emit datasetsLoaded(QString::fromStdString(currentParentId_), d.value.rows, d.value.total, true); + const int rootCount = static_cast(rootIdx.size()); + dataTotal_ = rootCount; + + // 取本页根 [shown, end) 的整棵子树(DFS 收集索引),再按原序输出保稳定。 + const int end = std::min(dataRootsShown_ + kDataRootPageSize, rootCount); + std::unordered_set picked; + for (int k = dataRootsShown_; k < end; ++k) { + std::vector stack{rootIdx[k]}; + while (!stack.empty()) { + const std::size_t cur = stack.back(); + stack.pop_back(); + if (!picked.insert(cur).second) continue; + auto it = kids.find(allDataRows_[cur].id); + if (it != kids.end()) + for (std::size_t c : it->second) stack.push_back(c); + } + } + std::vector out; + out.reserve(picked.size()); + for (std::size_t i = 0; i < allDataRows_.size(); ++i) + if (picked.count(i)) out.push_back(allDataRows_[i]); + + dataRootsShown_ = end; + emit datasetsLoaded(parent, out, rootCount, append); +} + +// ── loadMoreData:数据页树形——同步切下一页根(无请求)。loadMoreFiles:文件页服务端分页 ── +void WorkbenchNavController::loadMoreData() { + if (currentParentId_.empty()) return; + if (dataRootsShown_ >= dataTotal_) return; // 无更多根 + emitNextDataRootPage(true); } void WorkbenchNavController::loadMoreFiles() { - if (currentParentId_.empty() || busy_) return; - BusyGuard guard(this, &busy_); - const auto f = repo_.loadRows(currentProjectId_, currentParentId_, currentParentConfType_, 1, ++filePageNo_); - if (!f.ok) { - emit loadFailed(QStringLiteral("files"), QString::fromStdString(f.error)); - return; - } - fileTotal_ = f.value.total; - emit filesLoaded(QString::fromStdString(currentParentId_), f.value.rows, f.value.total, true); + if (currentParentId_.empty()) return; + if (moreFilesReq_) moreFilesReq_->abort(); + NavRequest* req = + repo_.loadRowsAsync(currentProjectId_, currentParentId_, currentParentConfType_, 1, ++filePageNo_); + moreFilesReq_ = req; + const QString parent = QString::fromStdString(currentParentId_); + emitBusyIfChanged(); + QObject::connect(req, &NavRequest::done, this, [this, req, parent](const QVariant& v) { + if (req != moreFilesReq_) return; + moreFilesReq_.clear(); + const auto page = qvariant_cast(v); + fileTotal_ = page.total; + emit filesLoaded(parent, page.rows, page.total, true); + emitBusyIfChanged(); + }); + QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) { + if (req != moreFilesReq_) return; + moreFilesReq_.clear(); + --filePageNo_; // 回滚:本请求失败,页号复位,避免下次 loadMoreFiles 跳页 + emit loadFailed(QStringLiteral("files"), msg); + emitBusyIfChanged(); + }); } +// ── selectDataset:单请求 ── +void WorkbenchNavController::selectDataset(const QString& dsObjectId) { + if (dsObjectId.isEmpty()) return; + if (datasetReq_) datasetReq_->abort(); + NavRequest* req = repo_.loadDatasetFormAsync(dsObjectId.toStdString()); + datasetReq_ = req; + emitBusyIfChanged(); + QObject::connect(req, &NavRequest::done, this, [this, req](const QVariant& v) { + if (req != datasetReq_) return; + datasetReq_.clear(); + emit datasetDetailLoaded(qvariant_cast(v)); + emitBusyIfChanged(); + }); + QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) { + if (req != datasetReq_) return; + datasetReq_.clear(); + emit loadFailed(QStringLiteral("datasetDetail"), msg); + emitBusyIfChanged(); + }); +} + +// ── setCheckedTms:未命中缓存项并发拉取,全到齐后组装;新勾选 abort 旧批(以最后一次为准)── void WorkbenchNavController::setCheckedTms(const QStringList& tmObjectIds) { - if (busy_) { // 触发源是延迟合并发射,可能落在别的同步操作的嵌套事件循环里: - pendingCheckedTms_ = tmObjectIds; // 不丢弃,记下最新一次请求,待空闲重放 - checkedTmsPending_ = true; + for (const auto& h : checkedInflight_) // abort-and-replace 旧批 + if (h) h->abort(); + checkedInflight_.clear(); + + // 入口去重:保留首次出现顺序,剔除重复 id(避免重复请求 + 异常树重复 group)。 + QStringList deduped; + QSet seen; + for (const QString& tmQ : tmObjectIds) { + if (seen.contains(tmQ)) continue; + seen.insert(tmQ); + deduped.push_back(tmQ); + } + + QStringList missing; + for (const QString& tmQ : deduped) { + const std::string tm = tmQ.toStdString(); + if (tmExceptionCache_.find(tm) == tmExceptionCache_.end()) missing.push_back(tmQ); + } + if (missing.isEmpty()) { // 全命中缓存:同步组装,busyChanged 不抖动 + assembleAndEmitExceptionTree(deduped); + emitBusyIfChanged(); return; } - BusyGuard guard(this, &busy_); + + // 并发拉取未命中项;每个 done 写缓存、清自身槽,全到齐后组装。计数用 shared 计数器。 + auto remaining = std::make_shared(missing.size()); + auto failedFlag = std::make_shared(false); + for (const QString& tmQ : missing) { + const std::string tm = tmQ.toStdString(); + NavRequest* req = repo_.loadExceptionsByTmAsync(tm); + checkedInflight_.push_back(req); + QObject::connect(req, &NavRequest::done, this, + [this, req, tm, remaining, failedFlag, deduped](const QVariant& v) { + // 身份比对:req 仍在当前批中才接受(旧批已 abort + 从 vector 移除)。 + bool inCurrent = false; + for (const auto& h : checkedInflight_) + if (h == req) inCurrent = true; + if (!inCurrent) return; + tmExceptionCache_[tm] = qvariant_cast>(v); + if (--(*remaining) == 0 && !*failedFlag) { + checkedInflight_.clear(); + assembleAndEmitExceptionTree(deduped); + emitBusyIfChanged(); + } + }); + QObject::connect(req, &NavRequest::failed, this, + [this, req, remaining, failedFlag](const QString& msg) { + bool inCurrent = false; + for (const auto& h : checkedInflight_) + if (h == req) inCurrent = true; + if (!inCurrent) return; + if (*failedFlag) return; // 仅首个失败发一次 + *failedFlag = true; + for (const auto& h : checkedInflight_) // abort 其余在飞 + if (h && h != req) h->abort(); + checkedInflight_.clear(); + emit loadFailed(QStringLiteral("exceptions"), msg); + emitBusyIfChanged(); + }); + } + emitBusyIfChanged(); +} + +void WorkbenchNavController::assembleAndEmitExceptionTree(const QStringList& tmObjectIds) { auto nameOf = [this](const std::string& id) -> std::string { for (const auto& n : lastStructNodes_) if (n.id == id) return n.name; @@ -197,14 +450,7 @@ void WorkbenchNavController::setCheckedTms(const QStringList& tmObjectIds) { for (const QString& tmQ : tmObjectIds) { const std::string tm = tmQ.toStdString(); auto it = tmExceptionCache_.find(tm); - if (it == tmExceptionCache_.end()) { - const auto ex = repo_.loadExceptionsByTm(tm); - if (!ex.ok) { - emit loadFailed(QStringLiteral("exceptions"), QString::fromStdString(ex.error)); - return; - } - it = tmExceptionCache_.emplace(tm, ex.value).first; - } + if (it == tmExceptionCache_.end()) continue; // 防御:理论上此时全命中 auto grouped = data::dto::groupExceptionsByConsortium(it->second); data::ObjectExceptionGroup g; g.objectId = tm; @@ -217,21 +463,4 @@ void WorkbenchNavController::setCheckedTms(const QStringList& tmObjectIds) { emit exceptionTreeLoaded(groups, total); } -void WorkbenchNavController::drainPendingCheckedTms() { - if (busy_ || !checkedTmsPending_) return; - checkedTmsPending_ = false; // 先清标志再重放,避免重入自旋 - setCheckedTms(pendingCheckedTms_); // 此时 busy_=false,会正常执行 -} - -void WorkbenchNavController::selectDataset(const QString& dsObjectId) { - if (dsObjectId.isEmpty() || busy_) return; - BusyGuard guard(this, &busy_); - const auto form = repo_.loadDatasetForm(dsObjectId.toStdString()); - if (!form.ok) { - emit loadFailed(QStringLiteral("datasetDetail"), QString::fromStdString(form.error)); - return; - } - emit datasetDetailLoaded(form.value); -} - } // namespace geopro::controller diff --git a/src/controller/WorkbenchNavController.hpp b/src/controller/WorkbenchNavController.hpp index 136fe78..6869e03 100644 --- a/src/controller/WorkbenchNavController.hpp +++ b/src/controller/WorkbenchNavController.hpp @@ -1,30 +1,39 @@ #pragma once #include +#include #include #include #include #include #include -#include "repo/IProjectRepository.hpp" +#include "repo/RepoTypes.hpp" + +namespace geopro::data { +class IAsyncProjectRepository; +class NavRequest; +} // namespace geopro::data namespace geopro::controller { -// 导航状态机:编排 IProjectRepository,持有当前 空间/项目 状态,经信号驱动 UI。不持有 widget。 +// 导航状态机:编排 IAsyncProjectRepository(异步句柄),持有当前 空间/项目 状态,经信号驱动 UI。 +// 不持有 widget。abort-and-replace + 句柄身份比对保证迟到信号被丢弃(spec §5.0)。 +// busyChanged 语义:「是否存在任一在飞句柄」(去抖:值变才发)。 class WorkbenchNavController : public QObject { Q_OBJECT public: - explicit WorkbenchNavController(data::IProjectRepository& repo, QObject* parent = nullptr); + explicit WorkbenchNavController(data::IAsyncProjectRepository& repo, QObject* parent = nullptr); + ~WorkbenchNavController() override; // 退出契约:abort 所有在飞句柄 - void start(); // 启动:拉空间 → 项目 → 结构 + void start(); // 启动:拉空间 → 项目 → 结构(依赖链) QString currentCrsCode() const { return QString::fromStdString(currentCrsCode_); } public slots: void switchWorkspace(const QString& tenantId); void switchProject(const QString& projectId); - void selectObject(const QString& objectId, int confType); // 单击对象→DS列表+对象详情 - void setCheckedTms(const QStringList& tmObjectIds); // 勾选叶子集→异常树 + void selectObject(const QString& objectId, int confType); // 单击对象→DS列表+对象详情(并发) + void setCheckedTms(const QStringList& tmObjectIds); // 勾选叶子集→异常树(并发,带缓存) void selectDataset(const QString& dsObjectId); // 单击DS→数据集动态表单 void loadMoreData(); void loadMoreFiles(); @@ -45,24 +54,42 @@ signals: void loadFailed(const QString& stage, const QString& message); private: - friend struct BusyGuard; // 允许在 guard 析构时排空挂起的勾选请求 - void loadProjectsAndStructure(); // start + switchWorkspace 共用 - void drainPendingCheckedTms(); // 空闲后重放最近一次被挂起的勾选集 + // start / switchWorkspace 依赖链:拉项目 → 拉结构(续延,复用)。 + void runProjectsAndStructure(); + void abortAll(); // 退出/重置时 abort 所有在飞句柄 + void resetSelectionState(); // 切项目/工作空间重置选中态(spec §6) + void emitBusyIfChanged(); // 据「是否存在任一在飞句柄」去抖发 busyChanged + bool anyInflight() const; // OR 所有在飞 QPointer / 集合 + void assembleAndEmitExceptionTree(const QStringList& tmObjectIds); // 缓存命中后组装异常树 + // 数据页树形分页:从 allDataRows_(一次取全的整棵)按「第一层节点(根)」切下一页, + // 每页 kDataRootPageSize 个根 + 各自整棵子树;total=根总数。append=false 首页、true 加载更多。 + void emitNextDataRootPage(bool append); + + data::IAsyncProjectRepository& repo_; + bool lastBusy_ = false; + + // 在飞句柄(QPointer 防悬垂;身份比对用): + QPointer startStepReq_; // start / switchWorkspace 依赖链当前在飞级 + QPointer structReq_; // switchProject + QPointer selDataReq_; // selectObject:data 行 + QPointer selFileReq_; // selectObject:file 行 + QPointer selDetailReq_; // selectObject:对象详情 + QPointer moreFilesReq_; // loadMoreFiles(数据页改客户端按根分页,无在飞句柄) + QPointer datasetReq_; + std::vector> checkedInflight_; // setCheckedTms:未命中缓存的并发批 - data::IProjectRepository& repo_; - bool busy_ = false; - bool checkedTmsPending_ = false; - QStringList pendingCheckedTms_; std::vector lastProjects_; std::string currentWorkspaceId_, currentProjectId_, currentProjectName_, currentCrsCode_; std::string currentParentId_; int currentParentConfType_ = 0; - std::vector lastStructNodes_; // tmId→name 解析 + std::vector lastStructNodes_; // tmId→name 解析 std::map> tmExceptionCache_; int dataPageNo_ = 0; int filePageNo_ = 0; - int dataTotal_ = 0; + int dataTotal_ = 0; // 数据页:根节点总数(树形分页单位) int fileTotal_ = 0; + std::vector allDataRows_; // 当前 TM 一次取全的所有数据行(树形按根客户端分页用) + int dataRootsShown_ = 0; // 已 emit 的根节点数(loadMoreData 续切) }; } // namespace geopro::controller diff --git a/src/core/model/ColorScale.cpp b/src/core/model/ColorScale.cpp index dce6ad6..609153d 100644 --- a/src/core/model/ColorScale.cpp +++ b/src/core/model/ColorScale.cpp @@ -40,6 +40,13 @@ std::vector ColorScale::stopValues() const { return v; } +std::vector> ColorScale::stops() const { + std::vector> result; + result.reserve(stops_.size()); + for (const auto& s : stops_) result.emplace_back(s.value, s.color); + return result; +} + Rgba ColorScale::colorAt(double value) const { if (std::isnan(value)) return nan_.value_or(Rgba{0, 0, 0, 0}); if (stops_.empty()) return Rgba{0, 0, 0, 0}; diff --git a/src/core/model/ColorScale.hpp b/src/core/model/ColorScale.hpp index b859478..db793a7 100644 --- a/src/core/model/ColorScale.hpp +++ b/src/core/model/ColorScale.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include namespace geopro::core { @@ -15,6 +16,8 @@ public: void addStop(double value, Rgba color); // 内部保持按 value 升序 Rgba colorAt(double value) const; // 含 under/over/NaN 处理 std::vector stopValues() const; // 升序分段值(供等值线用真实非均匀级) + // 返回升序断点 (value, color) 列表,供连续插值用。 + std::vector> stops() const; std::size_t stopCount() const { return stops_.size(); } void setUnder(Rgba c) { under_ = c; } void setOver(Rgba c) { over_ = c; } diff --git a/src/core/model/Field.hpp b/src/core/model/Field.hpp index e7c2a2b..95d7ca0 100644 --- a/src/core/model/Field.hpp +++ b/src/core/model/Field.hpp @@ -1,6 +1,7 @@ #pragma once #include #include +#include namespace geopro::core { // 规则三维标量场(IInterpolator 输出;render 层转 vtkImageData)。 @@ -24,12 +25,14 @@ private: }; // 规则二维网格剖面(x=距离 i, y=深度 j)。values 存 [j*nx + i],i 最快。 +// NaN 值=无效/测区外(凸包裁剪据此)。 class Grid { public: Grid(int nx, int ny) : nx_(nx), ny_(ny), values_(static_cast(nx) * ny, 0.0) {} int nx() const { return nx_; } int ny() const { return ny_; } double& valueAt(int i, int j) { return values_[static_cast(j) * nx_ + i]; } double valueAt(int i, int j) const { return values_[static_cast(j) * nx_ + i]; } + bool hasValue(int i, int j) const { return !std::isnan(valueAt(i, j)); } const std::vector& values() const { return values_; } std::vector& values() { return values_; } std::vector x, y; // 轴坐标(规则) @@ -46,6 +49,12 @@ private: struct ScatterField { std::vector x, y, z, v; // 对应样本 xlist/ylist/(hlist)/vlist std::vector projX, projY; // GIS 平面坐标 + // 可选元数据(measurement 散点用,反演留空)。a/b/m/n 与各点一一对应(hover 显示 + // A/B/M/N),electrodeX 为电极沿测线位置(y=0 处灰菱形 marker),electrodeNo 为对应 + // 电极编号(1-based,hover 显示 num,与 electrodeX 一一对应)。 + std::vector a, b, m, n; + std::vector electrodeX; + std::vector electrodeNo; }; } // namespace geopro::core diff --git a/src/core/model/detail/DetailPayloads.hpp b/src/core/model/detail/DetailPayloads.hpp new file mode 100644 index 0000000..762aba3 --- /dev/null +++ b/src/core/model/detail/DetailPayloads.hpp @@ -0,0 +1,123 @@ +#pragma once +#include +#include +#include +#include "model/Field.hpp" +#include "model/ColorScale.hpp" +#include "model/Anomaly.hpp" + +// 详情渲染载荷(纯数据,跨 QVariant 类型擦除传递)。无 Qt-widget 依赖。 +// 命名空间 geopro::core(与同目录 Field.hpp/ColorScale.hpp 一致)。 +namespace geopro::core { + +// 下拉项:字段码 + 显示名(measurement 工具条 x/y/v/method 下拉驱动;code 可空如 method)。 +struct FieldOption { + QString code; + QString name; +}; + +// measurement 散点工具条配置(来自服务端 scatterGraphConf;反演留空 → 视图渲染反演工具条)。 +// x/y/v/method 为各下拉的可选项(含 fieldCode+name);defaultX/Y/V/Method 为默认选中项的 +// fieldCode(method 的 fieldCode 全为 null,故 defaultMethod 用 name)。empty() 判定走 x 是否为空。 +struct ScatterToolbarConf { + std::vector x, y, v, method; + QString defaultX, defaultY, defaultV, defaultMethod; + bool empty() const { return x.empty(); } +}; + +// 散点载荷:反演原数据 / measurement 散点共用(≈ data::ChartParts)。 +// 两者上色一致——按数据 min/max 连续插值(Plotly cauto,含负异常值,RawDataChartView 据 v +// 有限值 min/max 设 setDataRange)。verticalLegend=true(measurement)时色阶图例画在右侧竖条 +// (离散带,1323→0 自上而下,对齐原版);false(默认,反演原数据)时画在底部横条。 +// toolbar 非空(measurement)时视图渲染 measurement 工具条;altX*/altY* 为 x/y 下拉本地重绘 +// 用的备选列(平距/斜距、伪深度/伪深度+高程),与 scatter.v/.a... 同序、一一对应,避免再发请求。 +struct ScatterPayload { + geopro::core::ScatterField scatter; + geopro::core::ColorScale scale; + bool verticalLegend = false; + ScatterToolbarConf toolbar; + std::vector altXHorizontal, altXSlope; // x 下拉:平距 / 斜距 + std::vector altYPseudo, altYElevationPseudo; // y 下拉:伪深度 / 伪深度+高程 +}; + +// 等值面载荷:grid(rows) + 色阶 + 异常(≈ data::GridParts)。 +// Grid 无默认构造,给占位初始化以满足 QVariant 对默认可构造的要求。 +struct ContourPayload { + geopro::core::Grid grid{1, 1}; + geopro::core::ColorScale scale; + std::vector anomalies; +}; + +// 列渲染种类:Text=预格式化文本(默认);Toggle=每行开关(蓝色药丸开关,ON=可见)。 +enum class TableColumnKind { Text, Toggle }; + +// 通用表格列定义。 +struct TableColumn { + QString code; + QString title; + int width = 0; + int sort = 0; + TableColumnKind kind = TableColumnKind::Text; +}; + +// 通用表格载荷:列定义 + 预格式化的行(每格 QString)+ 总数(分页用)。 +// 分页(dd_grid 列表,服务端分页 vxe-pager):pageSize>0 时视图渲染分页器,pageNo 为当前页(1 基); +// pageSize=0(默认)= 不分页(measurement/trajectory 全量列表,一次性返回所有行)。 +struct TablePayload { + std::vector columns; + std::vector> rows; + int total = 0; + int pageNo = 1; // 当前页(1 基);分页用 + int pageSize = 0; // 每页条数;>0 才渲染分页器(vxe-pager),0=不分页 +}; + +// 柱状图系列:名称(图例/legend)+ 各类目的 y 值 + 填充色(hex,如 #5470c6;数据色,两主题一致)。 +struct BarSeries { + QString name; + std::vector values; + QString color; +}; + +// 柱状图载荷(dd_ert_measurement_gr_data 接地电阻):类目(x 轴标签,如 "#1".."#40")+ +// 一个或多个系列(P1/P2,每系列一组按类目对齐的 y 值)+ 轴标题。 +struct BarPayload { + std::vector categories; + std::vector series; + QString xTitle; + QString yTitle; +}; + +// 折线图载荷(dd_trajectory_data 高程页签):单条平滑折线。 +// 类目(x 轴标签,如 "#1".."#40",来自 electrodeNo)+ 对齐的 y 值(高程)+ 系列名 + 线色(hex, +// 如 #5470c6,ECharts 默认蓝,两主题一致)+ 轴标题(x「电极号」/ y「高程」)+ 是否平滑。 +struct LinePayload { + std::vector categories; + std::vector y; + QString seriesName; + QString color; + QString xTitle; + QString yTitle; + bool smooth = true; +}; + +// 轨迹地图点(dd_trajectory_data 地图页签):电极号 + WGS84 经纬度。 +// 服务端 dd/ert/trajectory/line 直接返回 EPSG:4326 经纬(x=经度 lon、y=纬度 lat)。 +struct MapPoint { + int electrodeNo = 0; + double lon = 0; + double lat = 0; +}; + +// 轨迹地图载荷:一组电极经纬点(按电极号顺序)。视图据此在 Leaflet 上画橙色空心圈标记。 +struct MapPayload { + std::vector points; +}; + +} // namespace geopro::core + +Q_DECLARE_METATYPE(geopro::core::ScatterPayload) +Q_DECLARE_METATYPE(geopro::core::ContourPayload) +Q_DECLARE_METATYPE(geopro::core::TablePayload) +Q_DECLARE_METATYPE(geopro::core::BarPayload) +Q_DECLARE_METATYPE(geopro::core::LinePayload) +Q_DECLARE_METATYPE(geopro::core::MapPayload) diff --git a/src/data/CMakeLists.txt b/src/data/CMakeLists.txt index 803e557..a192760 100644 --- a/src/data/CMakeLists.txt +++ b/src/data/CMakeLists.txt @@ -4,8 +4,16 @@ add_library(geopro_data STATIC parse/SampleParsers.cpp repo/LocalSampleRepository.cpp dto/NavDto.cpp - api/ApiProjectRepository.cpp) + dto/DatasetChartDto.cpp + dto/MeasurementDto.cpp + dto/GrMeasurementDto.cpp + dto/TrajectoryDto.cpp + dto/GridDto.cpp + api/ApiProjectRepository.cpp + api/ApiDatasetRepository.cpp + api/DatasetLoadHandles.cpp + api/NavRequest.cpp) target_include_directories(geopro_data PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(geopro_data PUBLIC geopro_core geopro_net Qt6::Core PRIVATE nlohmann_json::nlohmann_json) target_compile_features(geopro_data PUBLIC cxx_std_17) -set_target_properties(geopro_data PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF) +set_target_properties(geopro_data PROPERTIES AUTOMOC ON AUTOUIC OFF AUTORCC OFF) diff --git a/src/data/README.md b/src/data/README.md index 4e706df..733dbb1 100644 --- a/src/data/README.md +++ b/src/data/README.md @@ -3,7 +3,7 @@ Repository 抽象(**异步契约**:QFuture/回调 + 取消 + 分页),DTO 与领域模型分离。 子目录(设计 §3、§6): -- `repo/` — IProjectRepository, IDatasetRepository +- `repo/` — IAsyncProjectRepository, IDatasetRepository - `local/` — LocalSampleRepository(M1,QtConcurrent 跑解析)+ 各格式解析器 - `api/` — ApiRepository(M1 骨架,签名对齐 pop-api) - `dto/` — 后端 JSON DTO + → model 映射 diff --git a/src/data/api/ApiDatasetRepository.cpp b/src/data/api/ApiDatasetRepository.cpp new file mode 100644 index 0000000..ee4472d --- /dev/null +++ b/src/data/api/ApiDatasetRepository.cpp @@ -0,0 +1,236 @@ +#include "api/ApiDatasetRepository.hpp" +#include +#include +#include +#include +#include +#include +#include "ApiClient.hpp" +#include "ApiBatch.hpp" +#include "api/DatasetLoadHandles.hpp" +#include "dto/DatasetChartDto.hpp" +#include "dto/GrMeasurementDto.hpp" +#include "dto/GridDto.hpp" +#include "dto/MeasurementDto.hpp" +#include "dto/TrajectoryDto.hpp" +#include "model/detail/DetailPayloads.hpp" + +namespace geopro::data { +namespace { + +QString enc(const std::string& s) { + return QString::fromUtf8(QUrl::toPercentEncoding(QString::fromStdString(s))); +} + +// 解析中间态(仅本 .cpp 内部使用,故置匿名命名空间,避免对外双份表示)。 +// 原数据加载结果:scatter + 散点色阶(type1)。 +struct ChartParts { + geopro::core::ScatterField scatter; + geopro::core::ColorScale scatterScale; +}; +// 网格数据加载结果:grid(rows) + 网格色阶(type2) + 异常。 +struct GridParts { + geopro::core::Grid grid{1, 1}; // Grid 无默认构造;占位 + geopro::core::ColorScale gridScale; + std::vector anomalies; +}; + +// 失败判定(原 must() 口径):业务码 != 200 或传输错误。 +bool isFailure(const geopro::net::ApiResponse& r) { + return r.code != 200 || !r.rawError.isEmpty(); +} + +QJsonObject colorBody(const std::string& dsId, int type) { + return QJsonObject{{"dsObjectId", QString::fromStdString(dsId)}, {"businessCode", ""}, {"type", type}}; +} + +// ── 共享批次构造:唯一端点定义处,old/new 路径复用,避免双份解析逻辑。 ── +// 反演原数据:index 0 = scatter(GET),1 = 散点色阶 type1(POST)。 +net::ApiBatch* inversionScatterBatch(net::ApiClient& api, const std::string& dsId) { + QList calls{ + api.getAsync(QStringLiteral("/business/dd/ert/inversion/getErtRawDataScatterGraph/%1").arg(enc(dsId))), + api.postJsonAsync(QStringLiteral("/business/lvl/colorGradation/getDetail"), colorBody(dsId, 1)), + }; + return new net::ApiBatch(calls, &isFailure); +} + +ChartParts parseScatterParts(const QList& r) { + ChartParts p; + p.scatter = dto::parseScatterGraph(r[0].data); + p.scatterScale = dto::parseColorBar(r[1].data); + return p; +} + +// 反演网格:index 0 = rows(GET,慢),1 = 色阶 type2(POST),2 = 异常(GET)。 +net::ApiBatch* inversionGridBatch(net::ApiClient& api, const std::string& dsId) { + QList calls{ + api.getAsync(QStringLiteral("/business/dd/ert/inversion/rows/%1").arg(enc(dsId))), + api.postJsonAsync(QStringLiteral("/business/lvl/colorGradation/getDetail"), colorBody(dsId, 2)), + api.getAsync(QStringLiteral("/business/exception/queryException/%1").arg(enc(dsId))), + }; + return new net::ApiBatch(calls, &isFailure); +} + +GridParts parseGridParts(const QList& r) { + GridParts p; + p.grid = dto::parseInversionGrid(r[0].data); + p.gridScale = dto::parseColorBar(r[1].data); + p.anomalies = dto::parseDatasetAnomalies(r[2].data.value(QStringLiteral("value")).toArray()); + return p; +} + +// measurement 散点:index 0 = scatter/graph(GET,query 参数),1 = 色阶 type3(POST,businessCode=R0)。 +net::ApiBatch* measurementScatterBatch(net::ApiClient& api, const std::string& dsId) { + const QString did = enc(dsId); + QList calls{ + api.getAsync(QStringLiteral("/business/dd/ert/measurement/scatter/graph?dsObjectId=%1&vFieldCode=").arg(did)), + api.postJsonAsync(QStringLiteral("/business/lvl/colorGradation/getDetail"), + QJsonObject{{"dsObjectId", QString::fromStdString(dsId)}, + {"businessCode", "R0"}, + {"type", 3}}), + }; + return new net::ApiBatch(calls, &isFailure); +} + +// measurement 列表:单请求 measurement/rows(GET,query 参数)。 +net::ApiBatch* measurementRowsBatch(net::ApiClient& api, const std::string& dsId) { + QList calls{ + api.getAsync(QStringLiteral("/business/dd/ert/measurement/rows?dsObjectId=%1").arg(enc(dsId))), + }; + return new net::ApiBatch(calls, &isFailure); +} + +// 接地电阻(gr):柱状图与列表同一端点 measurement/gr/rows(GET,query 参数)。 +// 响应 data 是 JSON 数组 → ApiResponseParse 包成 {value:[...]},取 r[0].data.value("value").toArray()。 +net::ApiBatch* grRowsBatch(net::ApiClient& api, const std::string& dsId) { + QList calls{ + api.getAsync(QStringLiteral("/business/dd/ert/measurement/gr/rows?dsObjectId=%1").arg(enc(dsId))), + }; + return new net::ApiBatch(calls, &isFailure); +} + +// gr 响应数组(包在 data.value 里)。 +QJsonArray grDataArray(const QList& r) { + return r[0].data.value(QStringLiteral("value")).toArray(); +} + +// 轨迹(trajectory):列表与高程同一端点 trajectory/rows(GET,query 参数)。 +// 响应 data = {rowList[40], gridHeaderDisplay[14], ...}。 +net::ApiBatch* trajectoryRowsBatch(net::ApiClient& api, const std::string& dsId) { + QList calls{ + api.getAsync(QStringLiteral("/business/dd/ert/trajectory/rows?dsObjectId=%1").arg(enc(dsId))), + }; + return new net::ApiBatch(calls, &isFailure); +} + +// 轨迹地图:单请求 trajectory/line(GET,query 参数)。frontCrsCode=EPSG:4326(原版发 EPSG%3A4326, +// 此处用 enc() 对整串 url 编码故 ':' → %3A)。响应 data = {electrodelList[...]}。 +net::ApiBatch* trajectoryLineBatch(net::ApiClient& api, const std::string& dsId) { + QList calls{ + api.getAsync(QStringLiteral("/business/dd/ert/trajectory/line?dsObjectId=%1&frontCrsCode=%2") + .arg(enc(dsId), enc("EPSG:4326"))), + }; + return new net::ApiBatch(calls, &isFailure); +} + +// dd_grid 白化数据列表(服务端分页):单请求 grid/rows(GET,query:dsObjectId/pageNo/pageSize)。 +// 响应 data = {rowList[{x,y,id}], gridHeaderDisplay[x,y], total}。 +net::ApiBatch* gridRowsBatch(net::ApiClient& api, const std::string& dsId, int pageNo, int pageSize) { + QList calls{ + api.getAsync(QStringLiteral("/business/dd/ert/grid/rows?dsObjectId=%1&pageNo=%2&pageSize=%3") + .arg(enc(dsId)) + .arg(pageNo) + .arg(pageSize)), + }; + return new net::ApiBatch(calls, &isFailure); +} + +} // namespace + +ApiDatasetRepository::ApiDatasetRepository(net::ApiClient& api) : api_(api) {} + +DetailLoad* ApiDatasetRepository::loadAsync(const std::string& loaderKey, const std::string& dsId, + int pageNo, int pageSize) { + if (loaderKey == "inversion.scatter") return makeInversionScatter(dsId); + if (loaderKey == "inversion.grid") return makeInversionGrid(dsId); + if (loaderKey == "ert_measurement.scatter") return makeMeasurementScatter(dsId); + if (loaderKey == "ert_measurement.rows") return makeMeasurementRows(dsId); + if (loaderKey == "gr.bar") return makeGrBar(dsId); + if (loaderKey == "gr.rows") return makeGrRows(dsId); + if (loaderKey == "traj.rows") return makeTrajectoryRows(dsId); + if (loaderKey == "traj.elev") return makeTrajectoryElevation(dsId); + if (loaderKey == "traj.map") return makeTrajectoryMap(dsId); + if (loaderKey == "grid.rows") return makeGridRows(dsId, pageNo, pageSize); + throw std::runtime_error("unknown loaderKey: " + loaderKey); +} + +DetailLoad* ApiDatasetRepository::makeInversionScatter(const std::string& dsId) { + // 复用同一批次 + 解析器,再映射为 ScatterPayload(不复制 JSON 解析逻辑)。 + return new ApiDetailLoad(inversionScatterBatch(api_, dsId), [](const QList& r) { + ChartParts p = parseScatterParts(r); + return QVariant::fromValue(core::ScatterPayload{p.scatter, p.scatterScale}); + }); +} + +DetailLoad* ApiDatasetRepository::makeInversionGrid(const std::string& dsId) { + return new ApiDetailLoad(inversionGridBatch(api_, dsId), [](const QList& r) { + GridParts p = parseGridParts(r); + return QVariant::fromValue(core::ContourPayload{p.grid, p.gridScale, p.anomalies}); + }); +} + +DetailLoad* ApiDatasetRepository::makeMeasurementScatter(const std::string& dsId) { + // index 0 = scatter/graph, 1 = colorBar(type3) → 离散上色的 ScatterPayload。 + return new ApiDetailLoad(measurementScatterBatch(api_, dsId), [](const QList& r) { + return QVariant::fromValue(dto::parseMeasurementScatter(r[0].data, r[1].data)); + }); +} + +DetailLoad* ApiDatasetRepository::makeMeasurementRows(const std::string& dsId) { + return new ApiDetailLoad(measurementRowsBatch(api_, dsId), [](const QList& r) { + return QVariant::fromValue(dto::parseMeasurementTable(r[0].data)); + }); +} + +DetailLoad* ApiDatasetRepository::makeGrBar(const std::string& dsId) { + return new ApiDetailLoad(grRowsBatch(api_, dsId), [](const QList& r) { + return QVariant::fromValue(dto::parseGrBar(grDataArray(r))); + }); +} + +DetailLoad* ApiDatasetRepository::makeGrRows(const std::string& dsId) { + return new ApiDetailLoad(grRowsBatch(api_, dsId), [](const QList& r) { + return QVariant::fromValue(dto::parseGrTable(grDataArray(r))); + }); +} + +DetailLoad* ApiDatasetRepository::makeTrajectoryRows(const std::string& dsId) { + return new ApiDetailLoad(trajectoryRowsBatch(api_, dsId), [](const QList& r) { + return QVariant::fromValue(dto::parseTrajectoryTable(r[0].data)); + }); +} + +DetailLoad* ApiDatasetRepository::makeTrajectoryElevation(const std::string& dsId) { + return new ApiDetailLoad(trajectoryRowsBatch(api_, dsId), [](const QList& r) { + return QVariant::fromValue(dto::parseTrajectoryElevation(r[0].data)); + }); +} + +DetailLoad* ApiDatasetRepository::makeTrajectoryMap(const std::string& dsId) { + return new ApiDetailLoad(trajectoryLineBatch(api_, dsId), [](const QList& r) { + return QVariant::fromValue(dto::parseTrajectoryMap(r[0].data)); + }); +} + +DetailLoad* ApiDatasetRepository::makeGridRows(const std::string& dsId, int pageNo, int pageSize) { + // 解析默认值:pageNo<=0→1,pageSize<=0→50(原版默认每页 50 条),再传入 URL 与解析器 + // (保证 URL 参数与「序号」列偏移一致)。 + const int pn = pageNo > 0 ? pageNo : 1; + const int ps = pageSize > 0 ? pageSize : 50; + return new ApiDetailLoad(gridRowsBatch(api_, dsId, pn, ps), + [pn, ps](const QList& r) { + return QVariant::fromValue(dto::parseGridTable(r[0].data, pn, ps)); + }); +} + +} // namespace geopro::data diff --git a/src/data/api/ApiDatasetRepository.hpp b/src/data/api/ApiDatasetRepository.hpp new file mode 100644 index 0000000..d31cd5d --- /dev/null +++ b/src/data/api/ApiDatasetRepository.hpp @@ -0,0 +1,25 @@ +#pragma once +#include "repo/IAsyncDatasetRepository.hpp" +namespace geopro::net { class ApiClient; } +namespace geopro::data { + +// 真实 API 实现 IAsyncDatasetRepository(ERT 反演)。每次加载返回自管理句柄。 +class ApiDatasetRepository : public IAsyncDatasetRepository { +public: + explicit ApiDatasetRepository(net::ApiClient& api); + DetailLoad* loadAsync(const std::string& loaderKey, const std::string& dsId, + int pageNo = 1, int pageSize = 0) override; +private: + DetailLoad* makeInversionScatter(const std::string& dsId); + DetailLoad* makeInversionGrid(const std::string& dsId); + DetailLoad* makeMeasurementScatter(const std::string& dsId); + DetailLoad* makeMeasurementRows(const std::string& dsId); + DetailLoad* makeGrBar(const std::string& dsId); + DetailLoad* makeGrRows(const std::string& dsId); + DetailLoad* makeTrajectoryRows(const std::string& dsId); + DetailLoad* makeTrajectoryElevation(const std::string& dsId); + DetailLoad* makeTrajectoryMap(const std::string& dsId); + DetailLoad* makeGridRows(const std::string& dsId, int pageNo, int pageSize); + net::ApiClient& api_; +}; +} // namespace geopro::data diff --git a/src/data/api/ApiProjectRepository.cpp b/src/data/api/ApiProjectRepository.cpp index 1bd7b14..273a977 100644 --- a/src/data/api/ApiProjectRepository.cpp +++ b/src/data/api/ApiProjectRepository.cpp @@ -6,6 +6,8 @@ #include #include "ApiClient.hpp" +#include "api/NavLoads.hpp" +#include "api/NavRequest.hpp" #include "dto/NavDto.hpp" namespace geopro::data { @@ -13,13 +15,8 @@ namespace geopro::data { namespace { constexpr int kCodeSuccess = 200; -bool ok(const net::ApiResponse& r) { return r.code == kCodeSuccess; } - -std::string errorOf(const net::ApiResponse& r, const char* fallback) { - if (!r.msg.isEmpty()) return r.msg.toStdString(); - if (!r.rawError.isEmpty()) return r.rawError.toStdString(); - return fallback; -} +// 异步失败谓词:业务码非 200 或网络错误。 +bool isFailureA(const net::ApiResponse& r) { return r.code != kCodeSuccess || !r.rawError.isEmpty(); } // 后端 id 进 URL 前做百分号编码(不可信外部数据:防 ? # & / 空格 破坏路径/查询)。 QString enc(const std::string& s) { @@ -29,54 +26,59 @@ QString enc(const std::string& s) { ApiProjectRepository::ApiProjectRepository(net::ApiClient& api) : api_(api) {} -RepoResult> ApiProjectRepository::listWorkspaces() { - const net::ApiResponse r = - api_.get(QStringLiteral("/business/system/tenant/enterprise/joined/list")); - if (!ok(r)) return {false, {}, errorOf(r, "listWorkspaces failed")}; - return {true, dto::parseWorkspaces(r.data.value(QStringLiteral("value")).toArray()), {}}; +// ── 异步实现(薄封装:解析器在 try 内见 ApiNavRequest)── + +NavRequest* ApiProjectRepository::listWorkspacesAsync() { + auto* call = api_.getAsync(QStringLiteral("/business/system/tenant/enterprise/joined/list")); + return new ApiNavRequest(call, [](const net::ApiResponse& r) { + return QVariant::fromValue(dto::parseWorkspaces(r.data.value(QStringLiteral("value")).toArray())); + }, &isFailureA); } -RepoResult ApiProjectRepository::switchWorkspace(const std::string& tenantId) { +NavRequest* ApiProjectRepository::switchWorkspaceAsync(const std::string& tenantId) { const QString path = QStringLiteral("/business/system/tenant/enterprise/switch/%1").arg(enc(tenantId)); - const net::ApiResponse r = api_.postJson(path, QJsonObject{}); - if (!ok(r)) return {false, false, errorOf(r, "switchWorkspace failed")}; - // 切换空间返回新 accessToken:必须重新注入,后续请求才落到新空间。 - const QString token = r.data.value(QStringLiteral("accessToken")).toString(); - if (!token.isEmpty()) api_.setToken(token); - return {true, true, {}}; + auto* call = api_.postJsonAsync(path, QJsonObject{}); + // 切换空间返回新 accessToken:必须重新注入(与同步版一致),后续请求才落到新空间。 + return new ApiNavRequest(call, [this](const net::ApiResponse& r) { + const QString token = r.data.value(QStringLiteral("accessToken")).toString(); + if (!token.isEmpty()) api_.setToken(token); + return QVariant::fromValue(true); + }, &isFailureA); } -RepoResult ApiProjectRepository::pageProjects(const std::string& nameFilter, - const std::string& typeId, int pageNo, - int pageSize) { +NavRequest* ApiProjectRepository::pageProjectsAsync(const std::string& nameFilter, + const std::string& typeId, int pageNo, + int pageSize) { QJsonObject body{{QStringLiteral("projectName"), QString::fromStdString(nameFilter)}, {QStringLiteral("pageNo"), pageNo}, {QStringLiteral("pageSize"), pageSize}}; if (!typeId.empty()) body[QStringLiteral("projectTypeId")] = QString::fromStdString(typeId); - const net::ApiResponse r = api_.postJson(QStringLiteral("/business/my/profile/project/page"), body); - if (!ok(r)) return {false, {}, errorOf(r, "pageProjects failed")}; - return {true, dto::parseProjectPage(r.data), {}}; + auto* call = api_.postJsonAsync(QStringLiteral("/business/my/profile/project/page"), body); + return new ApiNavRequest(call, [](const net::ApiResponse& r) { + return QVariant::fromValue(dto::parseProjectPage(r.data)); + }, &isFailureA); } -RepoResult> ApiProjectRepository::listProjectTypes() { - const net::ApiResponse r = api_.get(QStringLiteral("/business/project/type/list")); - if (!ok(r)) return {false, {}, errorOf(r, "listProjectTypes failed")}; - return {true, dto::parseProjectTypes(r.data.value(QStringLiteral("value")).toArray()), {}}; +NavRequest* ApiProjectRepository::listProjectTypesAsync() { + auto* call = api_.getAsync(QStringLiteral("/business/project/type/list")); + return new ApiNavRequest(call, [](const net::ApiResponse& r) { + return QVariant::fromValue(dto::parseProjectTypes(r.data.value(QStringLiteral("value")).toArray())); + }, &isFailureA); } -RepoResult> ApiProjectRepository::loadStructure(const std::string& projectId) { - // 项目结构(项目根 + GS + TM;不含 DS)。比 projectWorkbench 干净。 +NavRequest* ApiProjectRepository::loadStructureAsync(const std::string& projectId) { const QString path = QStringLiteral("/business/projectStruct/queryProjectStruct/%1").arg(enc(projectId)); - const net::ApiResponse r = api_.get(path); - if (!ok(r)) return {false, {}, errorOf(r, "loadStructure failed")}; - return {true, dto::parseStructNodes(r.data.value(QStringLiteral("value")).toArray()), {}}; + auto* call = api_.getAsync(path); + return new ApiNavRequest(call, [](const net::ApiResponse& r) { + return QVariant::fromValue(dto::parseStructNodes(r.data.value(QStringLiteral("value")).toArray())); + }, &isFailureA); } -RepoResult ApiProjectRepository::loadRows(const std::string& projectId, - const std::string& parentId, int parentConfType, - int classifyType, int pageNo) { +NavRequest* ApiProjectRepository::loadRowsAsync(const std::string& projectId, + const std::string& parentId, int parentConfType, + int classifyType, int pageNo, int pageSize) { const QString path = (classifyType == 1) ? QStringLiteral("/business/dsObject/file/page") : QStringLiteral("/business/dsObject/data/page"); const QJsonObject body{ @@ -85,38 +87,39 @@ RepoResult ApiProjectRepository::loadRows(const std::string& projectId, {QStringLiteral("structParentConfType"), parentConfType}, {QStringLiteral("classifyTypeList"), QJsonArray{classifyType}}, {QStringLiteral("pageNo"), pageNo}, - {QStringLiteral("pageSize"), 5}}; - const net::ApiResponse r = api_.postJson(path, body); - if (!ok(r)) return {false, {}, errorOf(r, "loadRows failed")}; - return {true, dto::parseDsPage(r.data), {}}; + {QStringLiteral("pageSize"), pageSize}}; + auto* call = api_.postJsonAsync(path, body); + return new ApiNavRequest(call, [](const net::ApiResponse& r) { + return QVariant::fromValue(dto::parseDsPage(r.data)); + }, &isFailureA); } -RepoResult ApiProjectRepository::loadObjectDetail(const std::string& objectId, - int confType) { +NavRequest* ApiProjectRepository::loadObjectDetailAsync(const std::string& objectId, int confType) { const QString path = (confType == 1) ? QStringLiteral("/business/gsObject/getGsObjectDetail/%1").arg(enc(objectId)) : QStringLiteral("/business/tmObject/getDetail/%1").arg(enc(objectId)); - const net::ApiResponse r = api_.get(path); - if (!ok(r)) return {false, {}, errorOf(r, "loadObjectDetail failed")}; - return {true, dto::parseDynamicForm(r.data), {}}; + auto* call = api_.getAsync(path); + return new ApiNavRequest(call, [](const net::ApiResponse& r) { + return QVariant::fromValue(dto::parseDynamicForm(r.data)); + }, &isFailureA); } -RepoResult ApiProjectRepository::loadDatasetForm(const std::string& dsObjectId) { - const QString path = - QStringLiteral("/business/dsObject/dynamicForm/%1").arg(enc(dsObjectId)); - const net::ApiResponse r = api_.get(path); - if (!ok(r)) return {false, {}, errorOf(r, "loadDatasetForm failed")}; - return {true, dto::parseDynamicForm(r.data), {}}; +NavRequest* ApiProjectRepository::loadDatasetFormAsync(const std::string& dsObjectId) { + const QString path = QStringLiteral("/business/dsObject/dynamicForm/%1").arg(enc(dsObjectId)); + auto* call = api_.getAsync(path); + return new ApiNavRequest(call, [](const net::ApiResponse& r) { + return QVariant::fromValue(dto::parseDynamicForm(r.data)); + }, &isFailureA); } -RepoResult> ApiProjectRepository::loadExceptionsByTm( - const std::string& tmObjectId) { +NavRequest* ApiProjectRepository::loadExceptionsByTmAsync(const std::string& tmObjectId) { const QString path = QStringLiteral("/business/exception/queryExceptionByTmObjectId/%1").arg(enc(tmObjectId)); - const net::ApiResponse r = api_.get(path); - if (!ok(r)) return {false, {}, errorOf(r, "loadExceptionsByTm failed")}; - return {true, dto::parseExceptions(r.data.value(QStringLiteral("value")).toArray()), {}}; + auto* call = api_.getAsync(path); + return new ApiNavRequest(call, [](const net::ApiResponse& r) { + return QVariant::fromValue(dto::parseExceptions(r.data.value(QStringLiteral("value")).toArray())); + }, &isFailureA); } } // namespace geopro::data diff --git a/src/data/api/ApiProjectRepository.hpp b/src/data/api/ApiProjectRepository.hpp index dd27b4b..28f10c9 100644 --- a/src/data/api/ApiProjectRepository.hpp +++ b/src/data/api/ApiProjectRepository.hpp @@ -1,26 +1,30 @@ #pragma once -#include "repo/IProjectRepository.hpp" +#include "repo/IAsyncProjectRepository.hpp" namespace geopro::net { class ApiClient; } namespace geopro::data { -// 用共享会话 ApiClient 实现导航仓储(同步阻塞)。token 由调用方注入 ApiClient。 -class ApiProjectRepository : public IProjectRepository { +class NavRequest; + +// 用共享会话 ApiClient 实现导航异步仓储。token 由调用方注入 ApiClient。 +class ApiProjectRepository : public IAsyncProjectRepository { public: explicit ApiProjectRepository(net::ApiClient& api); - RepoResult> listWorkspaces() override; - RepoResult switchWorkspace(const std::string& tenantId) override; - RepoResult pageProjects(const std::string& nameFilter, const std::string& typeId, - int pageNo, int pageSize) override; - RepoResult> listProjectTypes() override; - RepoResult> loadStructure(const std::string& projectId) override; - RepoResult loadRows(const std::string& projectId, const std::string& parentId, - int parentConfType, int classifyType, int pageNo) override; - RepoResult loadObjectDetail(const std::string& objectId, int confType) override; - RepoResult loadDatasetForm(const std::string& dsObjectId) override; - RepoResult> loadExceptionsByTm(const std::string& tmObjectId) override; + // ── 异步 ── 返回 NavRequest*(薄封装,一方法一请求)。 + NavRequest* listWorkspacesAsync() override; + NavRequest* switchWorkspaceAsync(const std::string& tenantId) override; + NavRequest* pageProjectsAsync(const std::string& nameFilter, const std::string& typeId, + int pageNo, int pageSize) override; + NavRequest* listProjectTypesAsync() override; + NavRequest* loadStructureAsync(const std::string& projectId) override; + NavRequest* loadRowsAsync(const std::string& projectId, const std::string& parentId, + int parentConfType, int classifyType, int pageNo, + int pageSize = 5) override; + NavRequest* loadObjectDetailAsync(const std::string& objectId, int confType) override; + NavRequest* loadDatasetFormAsync(const std::string& dsObjectId) override; + NavRequest* loadExceptionsByTmAsync(const std::string& tmObjectId) override; private: net::ApiClient& api_; diff --git a/src/data/api/DatasetLoadHandles.cpp b/src/data/api/DatasetLoadHandles.cpp new file mode 100644 index 0000000..f5c136d --- /dev/null +++ b/src/data/api/DatasetLoadHandles.cpp @@ -0,0 +1,47 @@ +#include "api/DatasetLoadHandles.hpp" +#include + +namespace geopro::data { + +namespace { +QString reasonOf(const geopro::net::ApiResponse& r) { + return r.msg.isEmpty() ? r.rawError : r.msg; +} +} // namespace + +ApiDetailLoad::ApiDetailLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent) + : DetailLoad(parent), batch_(batch), parse_(std::move(parse)) { + QObject::connect(batch, &geopro::net::ApiBatch::succeeded, this, + [this](const QList& resps) { + if (aborted_) return; // §5.0 + QVariant payload; + try { + payload = parse_(resps); // 仅解析在 try 内:下游 done 处理器抛出不应误报为解析失败 + } catch (const std::exception& e) { + emit failed(QString::fromUtf8(e.what())); + deleteLater(); + return; + } catch (...) { // 非 std 异常跨信号槽会 terminate,兜底转 failed + emit failed(QStringLiteral("解析失败:未知异常")); + deleteLater(); + return; + } + emit done(payload); + deleteLater(); + }); + QObject::connect(batch, &geopro::net::ApiBatch::failed, this, + [this](int, const geopro::net::ApiResponse& r) { + if (aborted_) return; + emit failed(reasonOf(r)); + deleteLater(); + }); +} + +void ApiDetailLoad::abort() { + if (aborted_) return; + aborted_ = true; + if (batch_) batch_->abort(); + deleteLater(); +} + +} // namespace geopro::data diff --git a/src/data/api/DatasetLoadHandles.hpp b/src/data/api/DatasetLoadHandles.hpp new file mode 100644 index 0000000..7e4d867 --- /dev/null +++ b/src/data/api/DatasetLoadHandles.hpp @@ -0,0 +1,38 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include "ApiBatch.hpp" + +namespace geopro::data { + +// ── 通用详情句柄(tab 引擎):载荷经 QVariant 类型擦除,单一 done(QVariant)。 ── +// 可测试缝(类比 IApiCall):仓储返回基类指针,控制器/测试只依赖它。 +class DetailLoad : public QObject { + Q_OBJECT +public: + using QObject::QObject; + ~DetailLoad() override = default; + virtual void abort() = 0; +signals: + void done(const QVariant& payload); + void failed(const QString& message); +}; + +// Api 实现:包 ApiBatch + 注入解析器(返回 QVariant 载荷)。逻辑与 ApiChartLoad 等价。 +class ApiDetailLoad : public DetailLoad { + Q_OBJECT +public: + using Parser = std::function&)>; + ApiDetailLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent = nullptr); + void abort() override; +private: + QPointer batch_; + Parser parse_; + bool aborted_ = false; +}; + +} // namespace geopro::data diff --git a/src/data/api/NavLoads.hpp b/src/data/api/NavLoads.hpp new file mode 100644 index 0000000..e4238d6 --- /dev/null +++ b/src/data/api/NavLoads.hpp @@ -0,0 +1,14 @@ +#pragma once +#include +#include +#include "repo/RepoTypes.hpp" + +// 导航异步返回类型经 QVariant 承载:同线程直连仅需 Q_DECLARE_METATYPE(无需 qRegisterMetaType)。 +Q_DECLARE_METATYPE(std::vector) +Q_DECLARE_METATYPE(geopro::data::ProjectListPage) +Q_DECLARE_METATYPE(std::vector) +Q_DECLARE_METATYPE(std::vector) +Q_DECLARE_METATYPE(geopro::data::DsPage) +Q_DECLARE_METATYPE(geopro::data::DynamicForm) +Q_DECLARE_METATYPE(std::vector) +// bool 已内置 QMetaType。 diff --git a/src/data/api/NavRequest.cpp b/src/data/api/NavRequest.cpp new file mode 100644 index 0000000..5b3eb57 --- /dev/null +++ b/src/data/api/NavRequest.cpp @@ -0,0 +1,47 @@ +#include "api/NavRequest.hpp" +#include + +namespace geopro::data { + +namespace { +QString reasonOf(const geopro::net::ApiResponse& r) { + return r.msg.isEmpty() ? r.rawError : r.msg; +} +} // namespace + +ApiNavRequest::ApiNavRequest(geopro::net::IApiCall* call, Parser parse, Predicate isFailure, + QObject* parent) + : NavRequest(parent), call_(call), parse_(std::move(parse)), isFailure_(std::move(isFailure)) { + QObject::connect(call, &geopro::net::IApiCall::finished, this, + [this](const geopro::net::ApiResponse& resp) { + if (aborted_) return; // §5.0 入口守卫 + if (isFailure_(resp)) { + emit failed(reasonOf(resp)); + deleteLater(); + return; + } + QVariant out; + try { + out = parse_(resp); // 仅解析在 try 内(下游 done 处理器抛出不误报) + } catch (const std::exception& e) { + emit failed(QString::fromUtf8(e.what())); + deleteLater(); + return; + } catch (...) { + emit failed(QStringLiteral("解析失败:未知异常")); + deleteLater(); + return; + } + emit done(out); + deleteLater(); + }); +} + +void ApiNavRequest::abort() { + if (aborted_) return; + aborted_ = true; + if (call_) call_->abort(); + deleteLater(); +} + +} // namespace geopro::data diff --git a/src/data/api/NavRequest.hpp b/src/data/api/NavRequest.hpp new file mode 100644 index 0000000..173c007 --- /dev/null +++ b/src/data/api/NavRequest.hpp @@ -0,0 +1,39 @@ +#pragma once +#include +#include +#include +#include +#include +#include "IApiCall.hpp" + +namespace geopro::data { + +// 单请求异步句柄(抽象基,可测试缝):payload 经 QVariant 承载,控制器侧 qvariant_cast 取出。 +class NavRequest : public QObject { + Q_OBJECT +public: + using QObject::QObject; + ~NavRequest() override = default; + virtual void abort() = 0; +signals: + void done(const QVariant& value); + void failed(const QString& message); +}; + +// Api 实现:包一个 IApiCall + 注入的解析器(ApiResponse → QVariant)+ 失败谓词。 +class ApiNavRequest : public NavRequest { + Q_OBJECT +public: + using Parser = std::function; + using Predicate = std::function; + ApiNavRequest(geopro::net::IApiCall* call, Parser parse, Predicate isFailure, + QObject* parent = nullptr); // 持有非拥有引用(QPointer);call 完成(finished)或 abort 后自行 deleteLater 自管理生命周期,本类不得 delete 它 + void abort() override; +private: + QPointer call_; + Parser parse_; + Predicate isFailure_; + bool aborted_ = false; +}; + +} // namespace geopro::data diff --git a/src/data/dto/DatasetChartDto.cpp b/src/data/dto/DatasetChartDto.cpp new file mode 100644 index 0000000..5bc6ead --- /dev/null +++ b/src/data/dto/DatasetChartDto.cpp @@ -0,0 +1,81 @@ +#include "dto/DatasetChartDto.hpp" +#include +#include +#include +namespace geopro::data::dto { +using namespace geopro::core; + +static double num(const QJsonValue& v, double def = 0.0) { + if (v.isDouble()) return v.toDouble(); + if (v.isString()) { bool ok = false; double d = v.toString().toDouble(&ok); return ok ? d : def; } + return def; +} + +Grid parseInversionGrid(const QJsonObject& data) { + const QJsonArray x = data.value("x").toArray(); + const QJsonArray y = data.value("y").toArray(); + const QJsonArray v = data.value("v").toArray(); // [ny][nx] + const int nx = x.size(), ny = y.size(); + Grid g(nx < 1 ? 1 : nx, ny < 1 ? 1 : ny); + g.x.clear(); for (auto e : x) g.x.push_back(num(e)); + g.y.clear(); for (auto e : y) g.y.push_back(num(e)); + if (v.size() != ny) // 服务端 v 行数与 y 不符:下方越界处填 NaN,记录便于排查(非静默)。 + qWarning("parseInversionGrid: v rows=%d != ny=%d (缺失行将填 NaN)", v.size(), ny); + for (int j = 0; j < ny; ++j) { + const QJsonArray row = v.at(j).toArray(); + for (int i = 0; i < nx; ++i) { + const QJsonValue cell = row.at(i); + g.valueAt(i, j) = (cell.isNull() || cell.isUndefined()) ? std::nan("") : num(cell, std::nan("")); + } + } + g.vmin = num(data.value("vmin")); g.vmax = num(data.value("vmax")); + return g; +} + +ScatterField parseScatterGraph(const QJsonObject& data) { + ScatterField s; + auto fill = [&](const char* key, std::vector& dst) { + for (auto e : data.value(key).toArray()) dst.push_back(num(e)); }; + fill("xlist", s.x); fill("ylist", s.y); fill("hlist", s.z); fill("vlist", s.v); + fill("projectXList", s.projX); fill("projectYList", s.projY); + return s; +} + +ColorScale parseColorBar(const QJsonObject& data) { + ColorScale cs; + const QJsonArray bar = data.value("properties").toObject().value("colorBar").toArray(); + for (auto e : bar) { + const QJsonArray pair = e.toArray(); + if (pair.size() < 2) continue; + const double val = num(pair.at(0)); + const std::string rgba = pair.at(1).toString().toStdString(); + // API colorBar 颜色为混合格式:hex(#RRGGBB) 与 CSS rgba(r,g,b,a),其中 a 是 0–1 浮点 + // (实测 "rgba(0, 0, 170, 1)")。须用 Unit 标度(a*255),否则 a=1 被当字节 → alpha≈1 近透明。 + cs.addStop(val, parseColor(rgba, AlphaScale::Unit)); + } + return cs; +} + +std::vector parseDatasetAnomalies(const QJsonArray& arr) { + std::vector out; + for (auto e : arr) { + const QJsonObject o = e.toObject(); + Anomaly a; + a.name = o.value("exceptionName").toString().toStdString(); + a.typeName = o.value("exceptionTypeName").toString().toStdString(); + const int mt = o.value("exceptionMarkType").toInt(2); // 1=点 2=线 3=面 + a.markType = (mt >= 1 && mt <= 3) ? static_cast(mt) + : AnomalyMarkType::Polyline; // 越界值兜底为线 + const QJsonObject lg = o.value("legend").toObject(); + a.lineColor = lg.value("polylineColor").toString("#000000").toStdString(); + a.lineWidth = lg.value("polylineWidth").toDouble(1.0); + a.dashed = lg.value("polylineShape").toString() == "dash"; + for (auto c : o.value("location").toObject().value("coordinate").toArray()) { + const QJsonObject p = c.toObject(); + a.localPts.push_back(Vec2{p.value("x").toDouble(), p.value("y").toDouble()}); + } + out.push_back(std::move(a)); + } + return out; +} +} // namespace geopro::data::dto diff --git a/src/data/dto/DatasetChartDto.hpp b/src/data/dto/DatasetChartDto.hpp new file mode 100644 index 0000000..c06dca5 --- /dev/null +++ b/src/data/dto/DatasetChartDto.hpp @@ -0,0 +1,19 @@ +#pragma once +#include +#include +#include +#include "model/Field.hpp" +#include "model/ColorScale.hpp" +#include "model/Anomaly.hpp" +namespace geopro::data::dto { + +// inversion/rows 的 data{ x,y,v[ny][nx],z,elevation,vmin,vmax } → Grid(v 缺省/非数→NaN)。 +geopro::core::Grid parseInversionGrid(const QJsonObject& data); +// getErtRawDataScatterGraph 的 data{ xlist,ylist,hlist,vlist,projectXList,projectYList } → ScatterField。 +geopro::core::ScatterField parseScatterGraph(const QJsonObject& data); +// colorGradation/getDetail 的 data.properties.colorBar [[值,"rgba()"],…] → ColorScale。 +geopro::core::ColorScale parseColorBar(const QJsonObject& data); +// queryException 的 data 数组 → Anomaly[](location.coordinate / legend)。 +std::vector parseDatasetAnomalies(const QJsonArray& arr); + +} // namespace geopro::data::dto diff --git a/src/data/dto/GrMeasurementDto.cpp b/src/data/dto/GrMeasurementDto.cpp new file mode 100644 index 0000000..962b591 --- /dev/null +++ b/src/data/dto/GrMeasurementDto.cpp @@ -0,0 +1,87 @@ +#include "dto/GrMeasurementDto.hpp" + +#include +#include + +namespace geopro::data::dto { +using namespace geopro::core; + +namespace { + +// ECharts 默认蓝(柱填充,数据色——浅/暗主题一致)。 +const QString kP1Color = QStringLiteral("#5470c6"); +const QString kP2Color = QStringLiteral("#91cc75"); // ECharts 默认绿(备用 P2 系列) + +// 把 JSON 数值/字符串预格式化为单元格 QString(整数不带小数点;null/缺省 → 空串)。 +QString cellText(const QJsonValue& v) { + if (v.isDouble()) { + const double d = v.toDouble(); + if (d == static_cast(static_cast(d))) + return QString::number(static_cast(d)); + return QString::number(d, 'g', 10); + } + if (v.isString()) return v.toString(); + return QString(); // null/undefined +} + +} // namespace + +BarPayload parseGrBar(const QJsonArray& rows) { + BarPayload p; + p.xTitle = QStringLiteral("电极点"); + p.yTitle = QStringLiteral("电阻(单位:欧姆)"); + + BarSeries p1{QStringLiteral("P1"), {}, kP1Color}; + BarSeries p2{QStringLiteral("P2"), {}, kP2Color}; + bool hasP2 = false; // 任一行 p2Rg 非空 → 建 P2 系列(分组柱)。 + + for (const auto& e : rows) { + const QJsonObject o = e.toObject(); + p.categories.push_back( + QStringLiteral("#") + QString::number(o.value(QStringLiteral("electrodeId")).toInt())); + const QJsonValue p1v = o.value(QStringLiteral("p1Rg")); + p1.values.push_back(p1v.isDouble() ? p1v.toDouble() : 0.0); // null/缺省 → 0(与 P2 对称防脏数据) + + const QJsonValue p2v = o.value(QStringLiteral("p2Rg")); + if (p2v.isDouble()) hasP2 = true; + p2.values.push_back(p2v.isDouble() ? p2v.toDouble() : 0.0); + } + + p.series.push_back(std::move(p1)); // P1 恒有 + if (hasP2) p.series.push_back(std::move(p2)); + return p; +} + +TablePayload parseGrTable(const QJsonArray& rows) { + TablePayload t; + + // 7 固定列(硬编码:gr 响应无列定义)。code = JSON 字段名,title = 原版列标题。 + const struct { const char* code; const char* title; } kCols[] = { + {"electrodeId", "ID"}, + {"testDate", "日期"}, + {"testTime", "时间"}, + {"p1Rg", "P1 Rg(Ω)"}, + {"p1RgStatus", "P1状态"}, + {"p2Rg", "P2 Rg(Ω)"}, + {"p2RgStatus", "P2状态"}, + }; + for (const auto& c : kCols) { + TableColumn col; + col.code = QString::fromUtf8(c.code); + col.title = QString::fromUtf8(c.title); + t.columns.push_back(col); + } + + for (const auto& e : rows) { + const QJsonObject o = e.toObject(); + std::vector cells; + cells.reserve(t.columns.size()); + for (const auto& col : t.columns) cells.push_back(cellText(o.value(col.code))); + t.rows.push_back(std::move(cells)); + } + + t.total = static_cast(t.rows.size()); + return t; +} + +} // namespace geopro::data::dto diff --git a/src/data/dto/GrMeasurementDto.hpp b/src/data/dto/GrMeasurementDto.hpp new file mode 100644 index 0000000..61d3483 --- /dev/null +++ b/src/data/dto/GrMeasurementDto.hpp @@ -0,0 +1,19 @@ +#pragma once +#include +#include "model/detail/DetailPayloads.hpp" + +namespace geopro::data::dto { + +// dd_ert_measurement_gr_data 接地电阻:两页签同一端点 measurement/gr/rows,data 是 JSON 数组 +// (每元素 {electrodeId,testDate,testTime,p1Rg,p1RgStatus,p2Rg,p2RgStatus},无 gridHeaderDisplay)。 +// +// 柱状图:类目 = "#"+electrodeId("#1".."#40");系列 P1 = p1Rg(恒有,色 #5470c6); +// 系列 P2 = p2Rg,仅当任一行 p2Rg 非空才建(本样本全 null → 只有 P1)。 +// x 轴标题「电极点」,y 轴标题「电阻(单位:欧姆)」。 +geopro::core::BarPayload parseGrBar(const QJsonArray& rows); + +// 列表:7 固定列(gr 无 gridHeaderDisplay,故硬编码列标题/列码): +// ID | 日期 | 时间 | P1 Rg(Ω) | P1状态 | P2 Rg(Ω) | P2状态。 +geopro::core::TablePayload parseGrTable(const QJsonArray& rows); + +} // namespace geopro::data::dto diff --git a/src/data/dto/GridDto.cpp b/src/data/dto/GridDto.cpp new file mode 100644 index 0000000..1187a12 --- /dev/null +++ b/src/data/dto/GridDto.cpp @@ -0,0 +1,34 @@ +#include "dto/GridDto.hpp" + +#include + +#include "dto/TrajectoryDto.hpp" // parseGridHeaderTable(通用 gridHeaderDisplay+rowList 解析器)复用 + +namespace geopro::data::dto { +using namespace geopro::core; + +TablePayload parseGridTable(const QJsonObject& data, int pageNo, int pageSize) { + const int pn = pageNo > 0 ? pageNo : 1; + const int ps = pageSize > 0 ? pageSize : 0; + + TablePayload t = parseGridHeaderTable(data); // x / y 列 + 各行(按 columnSort) + + // 前插「序号」列(vxe seq 列:居中、按页偏移自增)。 + TableColumn seq; + seq.code = QStringLiteral("__seq"); + seq.title = QStringLiteral("序号"); + seq.sort = 0; + t.columns.insert(t.columns.begin(), seq); + + const int base = (pn - 1) * ps; // 本页首行的全局序号偏移(ps=0 → 偏移 0,从 1 起) + for (size_t i = 0; i < t.rows.size(); ++i) + t.rows[i].insert(t.rows[i].begin(), QString::number(base + static_cast(i) + 1)); + + // 总数:dd_grid 用 data.total(parseGridHeaderTable 默认回退本批行数,这里以服务端总数覆盖)。 + t.total = data.value(QStringLiteral("total")).toInt(t.total); + t.pageNo = pn; + t.pageSize = ps; + return t; +} + +} // namespace geopro::data::dto diff --git a/src/data/dto/GridDto.hpp b/src/data/dto/GridDto.hpp new file mode 100644 index 0000000..0728d6d --- /dev/null +++ b/src/data/dto/GridDto.hpp @@ -0,0 +1,18 @@ +#pragma once +#include +#include "model/detail/DetailPayloads.hpp" + +namespace geopro::data::dto { + +// dd_grid(白化数据)列表。端点 dd/ert/grid/rows?dsObjectId=&pageNo=&pageSize= +// (服务端分页;data 形如 {rowList[{x,y,id}], gridHeaderDisplay[x,y], total})。 +// +// 渲染(实测 vxe-table 确认):序号 / x / y 三列,全部居中、同色灰字(无特殊着色); +// 底部 vxe-pager(上一页/页码/下一页 + 前往 N 页 + 每页条数选择 [50,100,500,1000] + 共 N 条记录)。 +// +// 复用通用 parseGridHeaderTable(gridHeaderDisplay→x/y 列 + rowList→行),再前插「序号」列 +// (vxe seq 列:按页偏移自增 = (pageNo-1)*pageSize + 行内序号);total 取 data.total(非 __rowTotal); +// 回填 pageNo/pageSize 供视图渲染分页器。pageNo/pageSize 为本次请求参数(仓储已解析默认值后传入)。 +geopro::core::TablePayload parseGridTable(const QJsonObject& data, int pageNo, int pageSize); + +} // namespace geopro::data::dto diff --git a/src/data/dto/MeasurementDto.cpp b/src/data/dto/MeasurementDto.cpp new file mode 100644 index 0000000..d206c02 --- /dev/null +++ b/src/data/dto/MeasurementDto.cpp @@ -0,0 +1,162 @@ +#include "dto/MeasurementDto.hpp" + +#include +#include +#include "dto/DatasetChartDto.hpp" // parseColorBar + +namespace geopro::data::dto { +using namespace geopro::core; + +namespace { + +// 位置数组列序(rows[i] 为 17 列定位数组)。 +constexpr int kColHorizontalDistance = 0; // x 备选:平距 +constexpr int kColSlopeDistance = 1; // x:斜距(默认) +constexpr int kColPseudoDepth = 3; // y:伪深度(负,向下,默认) +constexpr int kColElevationPseudoDepth = 9; // y 备选:伪深度+高程 +constexpr int kColA = 10; +constexpr int kColB = 11; +constexpr int kColM = 12; +constexpr int kColN = 13; +constexpr int kColValue = 16; // 色值:选中 v(默认视电阻率 R0) + +double numAt(const QJsonArray& arr, int i) { + if (i < 0 || i >= arr.size()) return 0.0; + const QJsonValue v = arr.at(i); + if (v.isDouble()) return v.toDouble(); + if (v.isString()) { bool ok = false; double d = v.toString().toDouble(&ok); return ok ? d : 0.0; } + return 0.0; +} + +// 把 JSON 值预格式化为单元格 QString(数字按原值,null/缺省→空串)。 +QString cellText(const QJsonValue& v) { + if (v.isDouble()) { + const double d = v.toDouble(); + // 整数值不带小数点(与列表观感一致);非整数保留有效位。 + if (d == static_cast(static_cast(d))) return QString::number(static_cast(d)); + return QString::number(d, 'g', 10); + } + if (v.isString()) return v.toString(); + if (v.isBool()) return v.toBool() ? QStringLiteral("true") : QStringLiteral("false"); + return QString(); // null/undefined +} + +// scatterGraphConf 的某一组下拉项 → FieldOption 列表(保留服务端顺序)。 +std::vector parseOptions(const QJsonArray& arr) { + std::vector out; + out.reserve(arr.size()); + for (const auto& e : arr) { + const QJsonObject o = e.toObject(); + out.push_back({o.value(QStringLiteral("fieldCode")).toString(), + o.value(QStringLiteral("name")).toString()}); + } + return out; +} + +} // namespace + +ScatterPayload parseMeasurementScatter(const QJsonObject& scatterData, const QJsonObject& colorBarData) { + ScatterPayload p; + // 上色与反演原数据同路径:连续插值(cauto)。色值 col16 原样保留(含负异常值), + // RawDataChartView 据 v 有限值 min/max 设 setDataRange → cmin<0,与原版 Plotly 一致。 + // 图例与反演统一为底部横条(verticalLegend 保持默认 false)。 + + const QJsonArray rows = scatterData.value(QStringLiteral("scatterGraphData")) + .toObject() + .value(QStringLiteral("rows")) + .toArray(); + auto& s = p.scatter; + for (const auto& e : rows) { + const QJsonArray row = e.toArray(); + s.x.push_back(numAt(row, kColSlopeDistance)); + s.y.push_back(numAt(row, kColPseudoDepth)); + s.v.push_back(numAt(row, kColValue)); + s.a.push_back(numAt(row, kColA)); + s.b.push_back(numAt(row, kColB)); + s.m.push_back(numAt(row, kColM)); + s.n.push_back(numAt(row, kColN)); + // x/y 下拉本地重绘备选列(与上面 push 同序、一一对应 → 视图换 x/y 无需再请求)。 + p.altXHorizontal.push_back(numAt(row, kColHorizontalDistance)); + p.altXSlope.push_back(numAt(row, kColSlopeDistance)); + p.altYPseudo.push_back(numAt(row, kColPseudoDepth)); + p.altYElevationPseudo.push_back(numAt(row, kColElevationPseudoDepth)); + } + + // 工具条下拉配置(scatterGraphConf):驱动 measurement 工具条 x/y/v/method 下拉与默认选中。 + // x/y/method 默认取首项(斜距 / 伪深度 / 线性);v 默认取“视电阻率”(R0),回退末-… 改为首项。 + const QJsonObject conf = scatterData.value(QStringLiteral("scatterGraphConf")).toObject(); + auto& tb = p.toolbar; + tb.x = parseOptions(conf.value(QStringLiteral("x")).toArray()); + tb.y = parseOptions(conf.value(QStringLiteral("y")).toArray()); + tb.v = parseOptions(conf.value(QStringLiteral("v")).toArray()); + tb.method = parseOptions(conf.value(QStringLiteral("vcalculationMethod")).toArray()); + // 默认选中:x=斜距(slopeDistance)、y=伪深度(pseudoDepth)、v=视电阻率(R0)、method=线性。 + tb.defaultX = QStringLiteral("slopeDistance"); + tb.defaultY = QStringLiteral("pseudoDepth"); + tb.defaultV = QStringLiteral("R0"); + if (!tb.method.empty()) tb.defaultMethod = tb.method.front().name; // 线性 + + // 电极沿测线位置(y=0 处灰菱形):取 slopeDistance 与 x(斜距)同量纲; + // electrodeNo 为电极编号(1-based),与 electrodeX 一一对应,供 hover 显示 num。 + for (const auto& e : scatterData.value(QStringLiteral("electrodeList")).toArray()) { + const QJsonObject eo = e.toObject(); + s.electrodeX.push_back(eo.value(QStringLiteral("slopeDistance")).toDouble()); + s.electrodeNo.push_back(eo.value(QStringLiteral("electrodeNo")).toDouble()); + } + + p.scale = parseColorBar(colorBarData); // 复用既有混合格式解析器(AlphaScale::Unit) + return p; +} + +TablePayload parseMeasurementTable(const QJsonObject& data) { + TablePayload t; + + // 列定义:优先 filedList,其次 gridHeaderDisplay(grid/trajectory 列表用同形状)。 + QJsonArray cols = data.value(QStringLiteral("filedList")).toArray(); + if (cols.isEmpty()) cols = data.value(QStringLiteral("gridHeaderDisplay")).toArray(); + for (const auto& e : cols) { + const QJsonObject c = e.toObject(); + TableColumn col; + col.code = c.value(QStringLiteral("fieldCode")).toString(); + col.title = c.value(QStringLiteral("name")).toString(); + if (col.title.isEmpty()) col.title = col.code; + t.columns.push_back(col); + } + + // 末尾追加“隐藏/显示”开关列(原版每行右侧蓝色药丸开关,全部 ON=可见)。 + // 仅 measurement 列表有此列(filedList 驱动时);grid/trajectory 走 gridHeaderDisplay 不追加。 + const bool hasToggleCol = !data.value(QStringLiteral("filedList")).toArray().isEmpty(); + if (hasToggleCol) { + TableColumn toggle; + toggle.code = QStringLiteral("displayStatus"); + toggle.title = QStringLiteral("隐藏/显示"); + toggle.kind = TableColumnKind::Toggle; + t.columns.push_back(toggle); + } + + // 行:每格按列码先查 vmap,再查行对象顶层。 + // Toggle 列:displayStatus==0 ⇒ ON/可见("1"),否则 OFF("0")。 + const QJsonArray rowList = data.value(QStringLiteral("rowList")).toArray(); + for (const auto& e : rowList) { + const QJsonObject obj = e.toObject(); + const QJsonObject vmap = obj.value(QStringLiteral("vmap")).toObject(); + std::vector cells; + cells.reserve(t.columns.size()); + for (const auto& col : t.columns) { + if (col.kind == TableColumnKind::Toggle) { + const int status = obj.value(QStringLiteral("displayStatus")).toInt(0); + cells.push_back(status == 0 ? QStringLiteral("1") : QStringLiteral("0")); + continue; + } + const QJsonValue v = vmap.contains(col.code) ? vmap.value(col.code) : obj.value(col.code); + cells.push_back(cellText(v)); + } + t.rows.push_back(std::move(cells)); + } + + // 总数(分页用):measurement 用 __rowListTotal,回退到本批行数。 + t.total = data.value(QStringLiteral("__rowListTotal")).toInt(static_cast(t.rows.size())); + return t; +} + +} // namespace geopro::data::dto diff --git a/src/data/dto/MeasurementDto.hpp b/src/data/dto/MeasurementDto.hpp new file mode 100644 index 0000000..3f9f468 --- /dev/null +++ b/src/data/dto/MeasurementDto.hpp @@ -0,0 +1,20 @@ +#pragma once +#include +#include "model/detail/DetailPayloads.hpp" + +namespace geopro::data::dto { + +// dd_ert_measurement_data 散点伪剖面: +// scatterData = scatter/graph 的 data{ electrodeList[], scatterGraphData{rows[位置数组]} } +// colorBarData = colorGradation/getDetail{type:3} 的 data{ properties.colorBar } +// 位置数组列序:[1]斜距(x) [3]伪深度(y,负向下) [10]a [11]b [12]m [13]n [16]视电阻率(色值)。 +// 上色与反演原数据同路径:连续插值(cauto),色值含负异常值不过滤;色阶复用 +// DatasetChartDto::parseColorBar。图例画在右侧竖条(verticalLegend=true)。 +geopro::core::ScatterPayload parseMeasurementScatter(const QJsonObject& scatterData, + const QJsonObject& colorBarData); + +// 通用列表:data{ filedList[列定义] | gridHeaderDisplay, rowList[键控对象,含 vmap] } → TablePayload。 +// 列码命中 vmap 时从 vmap 取值,否则从行对象顶层取;值预格式化为 QString。可复用于 grid/trajectory 列表。 +geopro::core::TablePayload parseMeasurementTable(const QJsonObject& data); + +} // namespace geopro::data::dto diff --git a/src/data/dto/NavDto.cpp b/src/data/dto/NavDto.cpp index 2f91e4a..6f38f88 100644 --- a/src/data/dto/NavDto.cpp +++ b/src/data/dto/NavDto.cpp @@ -123,6 +123,9 @@ std::vector parseDsRows(const QJsonArray& arr) { d.typeName = str(o, "name"); // 注意:name 字段=ds类型名 d.ddCode = str(o, "ddCode"); d.createTime = str(o, "createTime"); + // 数据集树父节点:sourceShowParentId 是“显示树”父(=派生数据挂源数据下),回退 parentId。 + d.parentId = str(o, "sourceShowParentId"); + if (d.parentId.empty()) d.parentId = str(o, "parentId"); const QJsonObject f = o.value(QStringLiteral("file")).toObject(); d.fileName = str(f, "name"); d.fileUrl = str(f, "url"); diff --git a/src/data/dto/TrajectoryDto.cpp b/src/data/dto/TrajectoryDto.cpp new file mode 100644 index 0000000..c4ff1f2 --- /dev/null +++ b/src/data/dto/TrajectoryDto.cpp @@ -0,0 +1,111 @@ +#include "dto/TrajectoryDto.hpp" + +#include + +#include +#include + +namespace geopro::data::dto { +using namespace geopro::core; + +namespace { + +// ECharts 默认蓝(折线色,数据色——浅/暗主题一致)。 +const QString kLineColor = QStringLiteral("#5470c6"); + +// 把 JSON 值预格式化为单元格 QString(整数不带小数点;null/缺省→空串)。 +QString cellText(const QJsonValue& v) { + if (v.isDouble()) { + const double d = v.toDouble(); + if (d == static_cast(static_cast(d))) + return QString::number(static_cast(d)); + return QString::number(d, 'g', 10); + } + if (v.isString()) return v.toString(); + if (v.isBool()) return v.toBool() ? QStringLiteral("true") : QStringLiteral("false"); + return QString(); // null/undefined +} + +} // namespace + +TablePayload parseGridHeaderTable(const QJsonObject& data) { + TablePayload t; + + // 列定义:gridHeaderDisplay,按 columnSort 升序。 + QJsonArray header = data.value(QStringLiteral("gridHeaderDisplay")).toArray(); + std::vector cols; + cols.reserve(header.size()); + for (const auto& e : header) cols.push_back(e.toObject()); + std::stable_sort(cols.begin(), cols.end(), [](const QJsonObject& a, const QJsonObject& b) { + return a.value(QStringLiteral("columnSort")).toInt() < + b.value(QStringLiteral("columnSort")).toInt(); + }); + for (const auto& c : cols) { + TableColumn col; + col.code = c.value(QStringLiteral("columnCode")).toString(); + col.title = c.value(QStringLiteral("columnNameChn")).toString(); + if (col.title.isEmpty()) col.title = col.code; + col.width = c.value(QStringLiteral("columnWidth")).toInt(); + col.sort = c.value(QStringLiteral("columnSort")).toInt(); + t.columns.push_back(col); + } + + // 行:每格按列码取行对象顶层值。 + const QJsonArray rowList = data.value(QStringLiteral("rowList")).toArray(); + for (const auto& e : rowList) { + const QJsonObject obj = e.toObject(); + std::vector cells; + cells.reserve(t.columns.size()); + for (const auto& col : t.columns) cells.push_back(cellText(obj.value(col.code))); + t.rows.push_back(std::move(cells)); + } + + // 总数(分页用):trajectory 用 __rowTotal,回退本批行数。 + t.total = data.value(QStringLiteral("__rowTotal")).toInt(static_cast(t.rows.size())); + return t; +} + +TablePayload parseTrajectoryTable(const QJsonObject& data) { + return parseGridHeaderTable(data); +} + +LinePayload parseTrajectoryElevation(const QJsonObject& data) { + LinePayload p; + p.seriesName = QStringLiteral("高程"); + p.color = kLineColor; + p.xTitle = QStringLiteral("电极号"); + p.yTitle = QStringLiteral("高程"); + p.smooth = true; + + const QJsonArray rowList = data.value(QStringLiteral("rowList")).toArray(); + for (const auto& e : rowList) { + const QJsonObject obj = e.toObject(); + p.categories.push_back(QStringLiteral("#") + + QString::number(obj.value(QStringLiteral("electrodeNo")).toInt())); + p.y.push_back(obj.value(QStringLiteral("elevation")).toDouble()); + } + return p; +} + +MapPayload parseTrajectoryMap(const QJsonObject& data) { + MapPayload m; + const QJsonArray list = data.value(QStringLiteral("electrodelList")).toArray(); + for (const auto& e : list) { + const QJsonObject obj = e.toObject(); + const QJsonObject coord = obj.value(QStringLiteral("electrodeCoordinate")).toObject(); + const QJsonArray geom = coord.value(QStringLiteral("geometryCoordinates")).toArray(); + if (geom.isEmpty()) continue; // 跳过缺坐标 + const QJsonObject pt = geom.first().toObject(); + const QJsonValue xv = pt.value(QStringLiteral("x")); + const QJsonValue yv = pt.value(QStringLiteral("y")); + if (!xv.isDouble() || !yv.isDouble()) continue; // 跳过 null/非数值 + MapPoint p; + p.electrodeNo = obj.value(QStringLiteral("electrodeNo")).toInt(); + p.lon = xv.toDouble(); + p.lat = yv.toDouble(); + m.points.push_back(p); + } + return m; +} + +} // namespace geopro::data::dto diff --git a/src/data/dto/TrajectoryDto.hpp b/src/data/dto/TrajectoryDto.hpp new file mode 100644 index 0000000..4a85566 --- /dev/null +++ b/src/data/dto/TrajectoryDto.hpp @@ -0,0 +1,29 @@ +#pragma once +#include +#include "model/detail/DetailPayloads.hpp" + +namespace geopro::data::dto { + +// dd_trajectory_data(ERT 电极坐标 轨迹)。三页签同一端点 dd/ert/trajectory/rows +// (data 形如 {rowList[40], gridHeaderDisplay[14]})。 + +// 通用「gridHeaderDisplay + rowList → TablePayload」解析器(可复用:trajectory 列表 / 未来 dd_grid)。 +// 列定义来自 gridHeaderDisplay(按 columnSort 升序,title=columnNameChn、code=columnCode、 +// width=columnWidth),行来自 rowList(每格按 columnCode 取顶层值,预格式化为 QString;null→空串)。 +// total 优先取 data.__rowTotal,回退本批行数。 +geopro::core::TablePayload parseGridHeaderTable(const QJsonObject& data); + +// 列表页签:直接复用通用解析器(14 列:电极号/平距/斜距/经度·东坐标/纬度·北坐标/高程/线号/ +// 道号/里程/深度/水深/日期/时间/解状态)。 +geopro::core::TablePayload parseTrajectoryTable(const QJsonObject& data); + +// 高程页签:单条平滑折线。类目 "#"+electrodeNo("#1".."#40"),y=elevation, +// x 轴标题「电极号」、y 轴标题「高程」,线色 #5470c6(ECharts 默认蓝),smooth=true。 +geopro::core::LinePayload parseTrajectoryElevation(const QJsonObject& data); + +// 地图页签:电极经纬点。来自 dd/ert/trajectory/line 端点 +// (data.electrodelList[].electrodeCoordinate.geometryCoordinates[0] → {x=lon, y=lat})。 +// 跳过缺失/null 坐标的电极。服务端直接返回 EPSG:4326 经纬,无需投影变换。 +geopro::core::MapPayload parseTrajectoryMap(const QJsonObject& data); + +} // namespace geopro::data::dto diff --git a/src/data/repo/IAsyncDatasetRepository.hpp b/src/data/repo/IAsyncDatasetRepository.hpp new file mode 100644 index 0000000..0f8c6bd --- /dev/null +++ b/src/data/repo/IAsyncDatasetRepository.hpp @@ -0,0 +1,19 @@ +#pragma once +#include + +namespace geopro::data { + +class DetailLoad; + +// 数据集详情异步仓储抽象。返回自管理句柄(完成/失败后 deleteLater)。 +class IAsyncDatasetRepository { +public: + virtual ~IAsyncDatasetRepository() = default; + // 通用页签加载(tab 引擎):按 loaderKey 分派,载荷经 QVariant 类型擦除。 + // pageNo/pageSize 仅分页型 loaderKey(如 grid.rows)使用;其余 loaderKey 忽略。 + // 默认 pageNo=1/pageSize=0 → 非分页加载(分页 loaderKey 自行解析默认每页条数)。 + virtual DetailLoad* loadAsync(const std::string& loaderKey, const std::string& dsId, + int pageNo = 1, int pageSize = 0) = 0; +}; + +} // namespace geopro::data diff --git a/src/data/repo/IAsyncProjectRepository.hpp b/src/data/repo/IAsyncProjectRepository.hpp new file mode 100644 index 0000000..d7d354a --- /dev/null +++ b/src/data/repo/IAsyncProjectRepository.hpp @@ -0,0 +1,31 @@ +#pragma once +#include + +namespace geopro::data { + +class NavRequest; + +// 导航异步仓储抽象(薄封装:一方法一请求,返回自管理句柄 emit done(QVariant)/failed(msg))。 +// 汇聚/链式编排由 WorkbenchNavController 负责(它知道完整序列与状态)。 +// 方法与同步 IProjectRepository 一一对应(加 Async 后缀消歧:同名不同返回类型不能同类共存); +// payload 类型见各方法注释(控制器 qvariant_cast)。 +class IAsyncProjectRepository { +public: + virtual ~IAsyncProjectRepository() = default; + virtual NavRequest* listWorkspacesAsync() = 0; // std::vector + virtual NavRequest* switchWorkspaceAsync(const std::string& tenantId) = 0; // bool(解析器内 setToken 副作用) + virtual NavRequest* pageProjectsAsync(const std::string& nameFilter, + const std::string& typeId, int pageNo, int pageSize) = 0; // ProjectListPage + virtual NavRequest* listProjectTypesAsync() = 0; // std::vector + virtual NavRequest* loadStructureAsync(const std::string& projectId) = 0; // std::vector + // pageSize 默认 5(文件页/数据页服务端分页);数据页树形需一次取全→控制器传大 pageSize 取整棵子树, + // 再按“第一层节点(根)”客户端分页(详见 WorkbenchNavController::emitNextDataRootPage)。 + virtual NavRequest* loadRowsAsync(const std::string& projectId, const std::string& parentId, + int parentConfType, int classifyType, int pageNo, + int pageSize = 5) = 0; // DsPage + virtual NavRequest* loadObjectDetailAsync(const std::string& objectId, int confType) = 0; // DynamicForm + virtual NavRequest* loadDatasetFormAsync(const std::string& dsObjectId) = 0; // DynamicForm + virtual NavRequest* loadExceptionsByTmAsync(const std::string& tmObjectId) = 0; // std::vector +}; + +} // namespace geopro::data diff --git a/src/data/repo/IDatasetRepository.hpp b/src/data/repo/IDatasetRepository.hpp index 96e3455..e1955c1 100644 --- a/src/data/repo/IDatasetRepository.hpp +++ b/src/data/repo/IDatasetRepository.hpp @@ -15,6 +15,7 @@ public: virtual geopro::core::Grid loadGrid(const std::string& dsId) = 0; virtual geopro::core::ScatterField loadScatter(const std::string& dsId) = 0; virtual geopro::core::ColorScale loadColorScale(const std::string& dsId) = 0; + virtual geopro::core::ColorScale loadScatterColorScale(const std::string& dsId) = 0; virtual std::vector loadAnomalies(const std::string& dsId) = 0; }; diff --git a/src/data/repo/IProjectRepository.hpp b/src/data/repo/IProjectRepository.hpp deleted file mode 100644 index 0c7c0af..0000000 --- a/src/data/repo/IProjectRepository.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once -#include -#include -#include "repo/RepoTypes.hpp" - -namespace geopro::data { - -// 仓储结果信封:网络可失败,故用显式 Result 而非抛异常,便于 UI 出错误/空状态。 -template -struct RepoResult { - bool ok = false; - T value{}; - std::string error; -}; - -// 导航仓储抽象(同步;呼应既有 IDatasetRepository 风格)。 -class IProjectRepository { -public: - virtual ~IProjectRepository() = default; - virtual RepoResult> listWorkspaces() = 0; - virtual RepoResult switchWorkspace(const std::string& tenantId) = 0; - // 项目分页:nameFilter 名称模糊(可空)、typeId 类型过滤(空=不限)、pageNo 从 1 起。 - virtual RepoResult pageProjects(const std::string& nameFilter, - const std::string& typeId, int pageNo, - int pageSize) = 0; - // 项目类型列表(弹窗类型过滤下拉)。 - virtual RepoResult> listProjectTypes() = 0; - virtual RepoResult> loadStructure(const std::string& projectId) = 0; - // 按结构父节点分页拉数据/文件行:parentConfType 1=GS 2=TM;classifyType 3=数据 1=文件; - // pageNo 从 1 起,pageSize 固定 5。 - virtual RepoResult loadRows(const std::string& projectId, const std::string& parentId, - int parentConfType, int classifyType, int pageNo) = 0; - // 对象详情:confType 1=GS(getGsObjectDetail) 2=TM(tmObject/getDetail) → 动态表单。 - virtual RepoResult loadObjectDetail(const std::string& objectId, int confType) = 0; - // 数据集详情:dsObject/dynamicForm/{dsObjectId} → 动态表单。 - virtual RepoResult loadDatasetForm(const std::string& dsObjectId) = 0; - // 单 TM 异常列表(含异常体归属字段)。 - virtual RepoResult> loadExceptionsByTm(const std::string& tmObjectId) = 0; -}; - -} // namespace geopro::data diff --git a/src/data/repo/LocalSampleRepository.hpp b/src/data/repo/LocalSampleRepository.hpp index 0d54179..183f612 100644 --- a/src/data/repo/LocalSampleRepository.hpp +++ b/src/data/repo/LocalSampleRepository.hpp @@ -18,8 +18,7 @@ public: std::vector loadAnomalies(const std::string& dsId) override; // 散点原数据(#17)专属色阶:剖面原数据自带的色阶文件(与网格色阶不同:分段值范围更大)。 - // 具体类专有方法(不进 IDatasetRepository 接口);散点点为不透明,alpha 量纲差异无影响。 - geopro::core::ColorScale loadScatterColorScale(const std::string& dsId); + geopro::core::ColorScale loadScatterColorScale(const std::string& dsId) override; // DEM/影像 GeoTIFF 的绝对路径(供 render::buildTerrain 经 GDAL 读)。 std::string demPath() const; diff --git a/src/data/repo/RepoTypes.hpp b/src/data/repo/RepoTypes.hpp index f1ecb2a..47261f5 100644 --- a/src/data/repo/RepoTypes.hpp +++ b/src/data/repo/RepoTypes.hpp @@ -5,8 +5,11 @@ namespace geopro::data { struct DsNode { std::string id, name, ddType; }; // data/page 或 file/page 的一条 ds。数据行只用 dsName/typeName/ddCode;文件行另含 file*。 +// parentId = 数据集树的父节点 id(取 sourceShowParentId,回退 parentId);空或不在本批=树根。 +// 原版数据列表是树:源「原始数据」为根,派生「反演/接地电阻」挂其下。 struct DsRow { std::string id, dsName, typeName, ddCode, createTime; + std::string parentId; std::string fileName, fileUrl; long long fileSize = 0; }; diff --git a/src/net/ApiBatch.cpp b/src/net/ApiBatch.cpp new file mode 100644 index 0000000..f9d8c7f --- /dev/null +++ b/src/net/ApiBatch.cpp @@ -0,0 +1,43 @@ +#include "ApiBatch.hpp" + +namespace geopro::net { + +ApiBatch::ApiBatch(QList calls, Predicate isFailure, QObject* parent) + : QObject(parent), isFailure_(std::move(isFailure)) { + Q_ASSERT(!calls.isEmpty()); // 契约:至少一个 call(空 batch 永不发 succeeded,调用方会挂起) + Q_ASSERT(isFailure_); // 契约:必须提供失败谓词(否则首个 finished 即 bad_function_call) + responses_.resize(calls.size()); + remaining_ = static_cast(calls.size()); + for (int i = 0; i < calls.size(); ++i) { + IApiCall* c = calls[i]; + calls_.append(c); + QObject::connect(c, &IApiCall::finished, this, [this, i](const ApiResponse& resp) { + if (aborted_) return; // §5.0 入口守卫 + if (isFailure_(resp)) { + aborted_ = true; // 此后 remaining_ 不再维护:迟到 finished 全被入口守卫挡掉 + for (const auto& other : calls_) { // fail-fast:abort 其余在飞 + if (other) other->abort(); + } + emit failed(i, resp); + deleteLater(); + return; + } + responses_[i] = resp; + if (--remaining_ == 0) { + emit succeeded(responses_); + deleteLater(); + } + }); + } +} + +void ApiBatch::abort() { + if (aborted_) return; + aborted_ = true; + for (const auto& c : calls_) { + if (c) c->abort(); + } + deleteLater(); +} + +} // namespace geopro::net diff --git a/src/net/ApiBatch.hpp b/src/net/ApiBatch.hpp new file mode 100644 index 0000000..da17a40 --- /dev/null +++ b/src/net/ApiBatch.hpp @@ -0,0 +1,29 @@ +#pragma once +#include +#include +#include +#include +#include "IApiCall.hpp" + +namespace geopro::net { + +// 并发汇聚 N 个 IApiCall:全成功 → succeeded(按下标对齐);任一失败 → fail-fast: +// failed(index,resp) + abort 其余在飞 call。安全不变量见 spec §5.0。 +class ApiBatch : public QObject { + Q_OBJECT +public: + using Predicate = std::function; + ApiBatch(QList calls, Predicate isFailure, QObject* parent = nullptr); // 持有非拥有引用(QPointer 列表);各 call 完成或 abort 后自行 deleteLater,本类不得 delete 它们 + void abort(); +signals: + void succeeded(const QList& responses); + void failed(int index, const geopro::net::ApiResponse& resp); +private: + QList> calls_; + QList responses_; + Predicate isFailure_; + int remaining_ = 0; + bool aborted_ = false; +}; + +} // namespace geopro::net diff --git a/src/net/ApiCall.cpp b/src/net/ApiCall.cpp new file mode 100644 index 0000000..cbbe880 --- /dev/null +++ b/src/net/ApiCall.cpp @@ -0,0 +1,42 @@ +#include "ApiCall.hpp" + +#include +#include +#include "ApiResponseParse.hpp" + +namespace geopro::net { + +ApiCall::ApiCall(QNetworkReply* reply, QObject* parent) : IApiCall(parent), reply_(reply) { + Q_ASSERT(reply); // 契约:不接受 null reply(NAM get/post 正常不返回 null) + QObject::connect(reply_, &QNetworkReply::finished, this, &ApiCall::onFinished); +} + +void ApiCall::onFinished() { + if (aborted_) return; // §5.0 入口守卫:迟到信号闸门 + QNetworkReply* reply = reply_.data(); // 快照:意图明确 + 防御 reply_ 中途被置空 + if (!reply) return; + ApiResponse resp = buildResponse(reply); + const QString url = reply->url().toString(); // 先快照 URL(reply 即将 deleteLater) + reply->deleteLater(); + // 错误判定口径同 spec §7:code != 200 || !rawError.isEmpty()。 + if (resp.rawError.isEmpty() && resp.code == 200) + qInfo("[net] %s http=%d code=%d", qUtf8Printable(url), resp.httpStatus, resp.code); + else + qWarning("[net] %s http=%d code=%d err=%s", qUtf8Printable(url), resp.httpStatus, resp.code, + qUtf8Printable(resp.rawError.isEmpty() ? resp.msg : resp.rawError)); + emit finished(resp); + deleteLater(); +} + +void ApiCall::abort() { + if (aborted_) return; + aborted_ = true; // 先置标志(权威闸门) + if (reply_) { + QObject::disconnect(reply_, nullptr, this, nullptr); // 尽力而为:断开未派发连接 + reply_->abort(); // 会再触发一次 finished,已被 disconnect+aborted_ 双挡 + reply_->deleteLater(); + } + deleteLater(); // 禁止同步 delete +} + +} // namespace geopro::net diff --git a/src/net/ApiCall.hpp b/src/net/ApiCall.hpp new file mode 100644 index 0000000..94cb3f6 --- /dev/null +++ b/src/net/ApiCall.hpp @@ -0,0 +1,22 @@ +#pragma once +#include +#include "IApiCall.hpp" + +class QNetworkReply; + +namespace geopro::net { + +// 包一个 QNetworkReply 的 IApiCall 实现。reply 完成 → buildResponse → emit finished → 自删。 +// 安全不变量(spec §5.0):abort 后绝不 emit;销毁一律 deleteLater,禁止同步 delete。 +class ApiCall : public IApiCall { + Q_OBJECT +public: + explicit ApiCall(QNetworkReply* reply, QObject* parent = nullptr); // 接管 reply + void abort() override; +private: + void onFinished(); + QPointer reply_; + bool aborted_ = false; +}; + +} // namespace geopro::net diff --git a/src/net/ApiChain.cpp b/src/net/ApiChain.cpp new file mode 100644 index 0000000..1590fc0 --- /dev/null +++ b/src/net/ApiChain.cpp @@ -0,0 +1,56 @@ +#include "ApiChain.hpp" +#include + +namespace geopro::net { + +ApiChain::ApiChain(QList steps, Predicate isFailure, QObject* parent) + : QObject(parent), steps_(std::move(steps)), isFailure_(std::move(isFailure)) { + Q_ASSERT(!steps_.isEmpty()); // 契约:至少一步(空链永不发 succeeded) + Q_ASSERT(isFailure_); + // 首步同步契约:此处同步执行首个 step 工厂。调用方须在 new ApiChain 之前连接 + // succeeded/failed,且首个工厂不得同步抛出、其 IApiCall 不得同步 emit finished。 + // 详见 ApiChain.hpp 首步同步契约说明。 + runNext(); +} + +void ApiChain::runNext() { + if (aborted_) return; + if (index_ >= steps_.size()) { // 全部完成 + emit succeeded(responses_); + deleteLater(); + return; + } + IApiCall* call = nullptr; + try { + call = steps_[index_](responses_); // 工厂可抛(如 RSA 失败):转 failed + } catch (const std::exception& e) { + ApiResponse r; + r.rawError = QString::fromUtf8(e.what()); // 保留原因;登录 RSA 文案在 AuthService 层包装 + aborted_ = true; + emit failed(index_, r); + deleteLater(); + return; + } + current_ = call; + QObject::connect(call, &IApiCall::finished, this, [this](const ApiResponse& resp) { + if (aborted_) return; // §5.0 入口守卫 + if (isFailure_(resp)) { + aborted_ = true; + emit failed(index_, resp); + deleteLater(); + return; + } + responses_.append(resp); + ++index_; + runNext(); // 链式推进 + }); +} + +void ApiChain::abort() { + if (aborted_) return; + aborted_ = true; + if (current_) current_->abort(); // abort 当前在飞步骤 + deleteLater(); +} + +} // namespace geopro::net diff --git a/src/net/ApiChain.hpp b/src/net/ApiChain.hpp new file mode 100644 index 0000000..281aa7c --- /dev/null +++ b/src/net/ApiChain.hpp @@ -0,0 +1,49 @@ +#pragma once +#include +#include +#include +#include +#include "IApiCall.hpp" + +namespace geopro::net { + +// 顺序执行 N 个步骤(依赖链):每步工厂用既往响应构造下一 IApiCall(工厂可抛 std::exception)。 +// 任一步失败 → fail-fast:failed(index,resp) + abort 当前在飞 + deleteLater。 +// 全部成功 → succeeded(按序响应)。安全不变量见 spec §5.0(aborted_ 闸门 + 一律 deleteLater)。 +// 与 ApiBatch 对称:同 Predicate、同信号面、同安全约束。 +// 注意:Part A(导航)暂未接入生产调用,首个生产使用见 Part B 登录(AuthService 串行链); +// 已由 tests/net/test_api_chain.cpp 覆盖,请勿当死代码删除。 +// +// ── 首步同步契约(调用方必读)────────────────────────────────────────────────── +// ctor 内同步调用 runNext(),立即执行首个 step 工厂。 +// 因此调用方须在 new ApiChain(...) 之前连接 succeeded/failed 信号——否则若首步工厂 +// 同步抛异常(catch 后 emit failed),信号会在连接建立前发出而丢失。 +// 契约: +// 1. 首个 step 工厂不得同步抛异常(生产路径:verifyCodeCheck 不抛,满足)。 +// 2. 首个 step 工厂返回的 IApiCall 不得同步 emit finished(生产路径:真实网络请求,满足)。 +// 若将来需要可同步完成的首步,应将 ctor 内 runNext() 改为 +// QMetaObject::invokeMethod(this, &ApiChain::runNext, Qt::QueuedConnection) +// 以推迟首拍到事件循环,届时调用方已完成信号连接。 +// ──────────────────────────────────────────────────────────────────────────────── +class ApiChain : public QObject { + Q_OBJECT +public: + // 工厂:入参为已完成步骤的响应(按序),返回本步 IApiCall(持有非拥有引用 QPointer;IApiCall 完成或 abort 后自行 deleteLater,本类不得 delete 它)。可抛 std::exception。 + using StepFactory = std::function& prior)>; + using Predicate = std::function; + ApiChain(QList steps, Predicate isFailure, QObject* parent = nullptr); + void abort(); +signals: + void succeeded(const QList& responses); + void failed(int index, const geopro::net::ApiResponse& resp); +private: + void runNext(); // 构造并连接下一步(工厂抛出 → emit failed) + QList steps_; + Predicate isFailure_; + QList responses_; + QPointer current_; + int index_ = 0; + bool aborted_ = false; +}; + +} // namespace geopro::net diff --git a/src/net/ApiClient.cpp b/src/net/ApiClient.cpp index 30fc5b4..8945278 100644 --- a/src/net/ApiClient.cpp +++ b/src/net/ApiClient.cpp @@ -1,10 +1,8 @@ #include "ApiClient.hpp" -#include -#include +#include "ApiCall.hpp" + #include -#include -#include #include #include #include @@ -14,36 +12,9 @@ namespace geopro::net { namespace { -constexpr int kHttpStatusUnset = 0; const char* const kContentTypeJson = "application/json"; const char* const kTokenHeader = "geomativeauthorization"; -// 把响应体 JSON 解析进信封。解析失败或非对象时把原文/错误写入 rawError。 -void parseBody(const QByteArray& body, ApiResponse& resp) { - if (body.isEmpty()) { - resp.rawError = QStringLiteral("empty response body"); - return; - } - QJsonParseError perr{}; - const QJsonDocument doc = QJsonDocument::fromJson(body, &perr); - if (perr.error != QJsonParseError::NoError || !doc.isObject()) { - resp.rawError = QStringLiteral("JSON parse error: %1; body: %2") - .arg(perr.errorString(), QString::fromUtf8(body)); - return; - } - const QJsonObject obj = doc.object(); - resp.code = obj.value(QStringLiteral("code")).toInt(); - resp.msg = obj.value(QStringLiteral("msg")).toString(); - const QJsonValue dataVal = obj.value(QStringLiteral("data")); - if (dataVal.isObject()) { - resp.data = dataVal.toObject(); - } else { - // data 可能是标量(如 verifyCodeCheck 返回 true)。包成 {"value": } - // 便于上层统一通过 data["value"] 取用,同时不丢信息。 - resp.data = QJsonObject{{QStringLiteral("value"), dataVal}}; - } -} - } // namespace struct ApiClient::Impl { @@ -61,27 +32,6 @@ struct ApiClient::Impl { } return req; } - - // 阻塞等待 reply 完成,解析为 ApiResponse。调用方负责 reply->deleteLater()。 - static ApiResponse await(QNetworkReply* reply) { - QEventLoop loop; - QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - loop.exec(); - - ApiResponse resp; - const QVariant statusVar = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); - resp.httpStatus = statusVar.isValid() ? statusVar.toInt() : kHttpStatusUnset; - - const QByteArray body = reply->readAll(); - if (reply->error() != QNetworkReply::NoError) { - resp.rawError = reply->errorString(); - } - // 即便有传输层错误,服务端仍可能回带 JSON 体(如 4xx);尽量解析。 - if (!body.isEmpty()) { - parseBody(body, resp); - } - return resp; - } }; ApiClient::ApiClient(QString baseUrl) : impl_(std::make_unique(std::move(baseUrl))) {} @@ -90,21 +40,17 @@ ApiClient::~ApiClient() = default; void ApiClient::setToken(const QString& token) { impl_->token = token; } -ApiResponse ApiClient::get(const QString& path) { +IApiCall* ApiClient::getAsync(const QString& path) { QNetworkRequest req = impl_->buildRequest(path); QNetworkReply* reply = impl_->nam.get(req); - ApiResponse resp = Impl::await(reply); - reply->deleteLater(); - return resp; + return new ApiCall(reply); } -ApiResponse ApiClient::postJson(const QString& path, const QJsonObject& body) { +IApiCall* ApiClient::postJsonAsync(const QString& path, const QJsonObject& body) { QNetworkRequest req = impl_->buildRequest(path); const QByteArray payload = QJsonDocument(body).toJson(QJsonDocument::Compact); QNetworkReply* reply = impl_->nam.post(req, payload); - ApiResponse resp = Impl::await(reply); - reply->deleteLater(); - return resp; + return new ApiCall(reply); } } // namespace geopro::net diff --git a/src/net/ApiClient.hpp b/src/net/ApiClient.hpp index 77c1965..64091cb 100644 --- a/src/net/ApiClient.hpp +++ b/src/net/ApiClient.hpp @@ -8,6 +8,8 @@ class QNetworkAccessManager; namespace geopro::net { +class IApiCall; + // 服务端统一响应信封:{code, data, msg}。 // httpStatus 为 HTTP 状态码(如 200);code/data/msg 解析自响应体 JSON。 // 网络层错误(连接失败、超时、JSON 解析失败)写入 rawError,此时 httpStatus 可能为 0。 @@ -19,7 +21,7 @@ struct ApiResponse { QString rawError; }; -// QtNetwork 的同步 HTTP 封装。 +// QtNetwork 的异步 HTTP 封装。 // 内部持有【唯一一个】QNetworkAccessManager 成员,默认共享 cookie jar, // 因此同一 ApiClient 实例发出的多次请求处于同一会话(共享 JSESSIONID)。 // 这是登录流程 getImageCode -> verifyCodeCheck -> login2 串联的关键。 @@ -34,9 +36,9 @@ public: // 设置令牌;注入请求头 geomativeauthorization。token 值本身已含 "Geomative " 前缀。 void setToken(const QString& token); - // 同步 GET / POST(JSON)。用 QNetworkReply + QEventLoop 阻塞等待响应。 - ApiResponse get(const QString& path); - ApiResponse postJson(const QString& path, const QJsonObject& body); + // 异步 GET / POST(JSON):立即返回自管理句柄,不阻塞。调用方连 IApiCall::finished。 + IApiCall* getAsync(const QString& path); + IApiCall* postJsonAsync(const QString& path, const QJsonObject& body); private: struct Impl; @@ -44,3 +46,6 @@ private: }; } // namespace geopro::net + +#include +Q_DECLARE_METATYPE(geopro::net::ApiResponse) diff --git a/src/net/ApiResponseParse.cpp b/src/net/ApiResponseParse.cpp new file mode 100644 index 0000000..69aee57 --- /dev/null +++ b/src/net/ApiResponseParse.cpp @@ -0,0 +1,53 @@ +#include "ApiResponseParse.hpp" + +#include +#include +#include +#include +#include +#include + +namespace geopro::net { + +namespace { +constexpr int kHttpStatusUnset = 0; + +void parseBody(const QByteArray& body, ApiResponse& resp) { + if (body.isEmpty()) { + resp.rawError = QStringLiteral("empty response body"); + return; + } + QJsonParseError perr{}; + const QJsonDocument doc = QJsonDocument::fromJson(body, &perr); + if (perr.error != QJsonParseError::NoError || !doc.isObject()) { + resp.rawError = QStringLiteral("JSON parse error: %1; body: %2") + .arg(perr.errorString(), QString::fromUtf8(body)); + return; + } + const QJsonObject obj = doc.object(); + resp.code = obj.value(QStringLiteral("code")).toInt(); + resp.msg = obj.value(QStringLiteral("msg")).toString(); + const QJsonValue dataVal = obj.value(QStringLiteral("data")); + if (dataVal.isObject()) { + resp.data = dataVal.toObject(); + } else { + resp.data = QJsonObject{{QStringLiteral("value"), dataVal}}; + } +} +} // namespace + +ApiResponse buildResponse(QNetworkReply* reply) { + ApiResponse resp; + const QVariant statusVar = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + resp.httpStatus = statusVar.isValid() ? statusVar.toInt() : kHttpStatusUnset; + const QByteArray body = reply->readAll(); + if (reply->error() != QNetworkReply::NoError) { + resp.rawError = reply->errorString(); + } + if (!body.isEmpty()) { + parseBody(body, resp); + } + return resp; +} + +} // namespace geopro::net diff --git a/src/net/ApiResponseParse.hpp b/src/net/ApiResponseParse.hpp new file mode 100644 index 0000000..19204a8 --- /dev/null +++ b/src/net/ApiResponseParse.hpp @@ -0,0 +1,12 @@ +#pragma once +#include "ApiClient.hpp" // for geopro::net::ApiResponse + +class QNetworkReply; + +namespace geopro::net { + +// 从一个【已完成】的 reply 读取状态码/响应体/错误并解析为 ApiResponse。 +// 不等待、不删除 reply(调用方负责生命周期)。供同步 await 与异步 ApiCall 共用。 +ApiResponse buildResponse(QNetworkReply* reply); + +} // namespace geopro::net diff --git a/src/net/AuthLoads.cpp b/src/net/AuthLoads.cpp new file mode 100644 index 0000000..b92498e --- /dev/null +++ b/src/net/AuthLoads.cpp @@ -0,0 +1,78 @@ +#include "AuthLoads.hpp" + +#include "ApiChain.hpp" +#include "ApiClient.hpp" // geopro::net::ApiResponse +#include "IApiCall.hpp" + +namespace geopro::net { + +namespace { + +// 统一的失败文案:优先服务端 msg,否则回退传输层 rawError,最后给通用文案。 +QString reasonOf(const ApiResponse& resp, const QString& fallback) { + if (!resp.msg.isEmpty()) return resp.msg; + if (!resp.rawError.isEmpty()) return resp.rawError; + return fallback; +} + +} // namespace + +CaptchaLoad::CaptchaLoad(IApiCall* call, QObject* parent) : QObject(parent), call_(call) { + QObject::connect(call_, &IApiCall::finished, this, [this](const ApiResponse& resp) { + if (aborted_) return; // §5.0 入口守卫 + if (resp.code != 200 || !resp.rawError.isEmpty()) { + aborted_ = true; // 终态置位:到达 failed 终态后 abort() 早退 + emit failed(reasonOf(resp, QStringLiteral("获取验证码失败"))); + deleteLater(); + return; + } + AuthService::Captcha cap; + cap.codeId = resp.data.value(QStringLiteral("id")).toString(); + cap.code = resp.data.value(QStringLiteral("code")).toString(); + aborted_ = true; // 终态置位:到达 done 终态后 abort() 早退 + emit done(cap); + deleteLater(); + }); +} + +void CaptchaLoad::abort() { + if (aborted_) return; + aborted_ = true; + if (call_) call_->abort(); + deleteLater(); +} + +LoginLoad::LoginLoad(ApiChain* chain, QObject* parent) : QObject(parent), chain_(chain) { + QObject::connect(chain_, &ApiChain::succeeded, this, + [this](const QList& responses) { + if (aborted_) return; // §5.0 入口守卫 + const QString token = + responses.isEmpty() + ? QString() + : responses.last().data.value(QStringLiteral("accessToken")).toString(); + if (token.isEmpty()) { + aborted_ = true; // 终态置位:到达 failed 终态后 abort() 早退 + emit failed(QStringLiteral("登录成功但缺少 accessToken")); + deleteLater(); + return; + } + aborted_ = true; // 终态置位:到达 done 终态后 abort() 早退 + emit done(token); + deleteLater(); + }); + QObject::connect(chain_, &ApiChain::failed, this, [this](int, const ApiResponse& resp) { + if (aborted_) return; // §5.0 入口守卫 + aborted_ = true; // 终态置位:到达 failed 终态后 abort() 早退 + emit failed(reasonOf(resp, QStringLiteral("登录失败"))); + deleteLater(); + }); +} + +void LoginLoad::abort() { + if (aborted_) return; + aborted_ = true; + if (chain_) chain_->abort(); + deleteLater(); +} + +} // namespace geopro::net diff --git a/src/net/AuthLoads.hpp b/src/net/AuthLoads.hpp new file mode 100644 index 0000000..b35a962 --- /dev/null +++ b/src/net/AuthLoads.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include + +#include "AuthService.hpp" // geopro::net::AuthService::Captcha + +namespace geopro::net { + +class IApiCall; +class ApiChain; + +// 验证码加载句柄(net 层,自管理):接管一个 getImageCode 的 IApiCall, +// 完成后解析 {data.id, data.code} -> done(Captcha);失败 -> failed(msg)。 +// 安全不变量见 spec §5.0:aborted_ 入口守卫 + 一律 deleteLater(禁同步 delete)。 +class CaptchaLoad : public QObject { + Q_OBJECT +public: + explicit CaptchaLoad(IApiCall* call, QObject* parent = nullptr); + void abort(); +signals: + void done(const geopro::net::AuthService::Captcha& captcha); + void failed(const QString& message); + +private: + QPointer call_; + bool aborted_ = false; +}; + +// 登录加载句柄(net 层,自管理):接管一个 verifyCodeCheck->RSA->login2 的 ApiChain, +// succeeded 取末步 data.accessToken -> done(token);缺 token 或 failed -> failed(msg)。 +class LoginLoad : public QObject { + Q_OBJECT +public: + explicit LoginLoad(ApiChain* chain, QObject* parent = nullptr); + void abort(); +signals: + void done(const QString& token); + void failed(const QString& message); + +private: + QPointer chain_; + bool aborted_ = false; +}; + +} // namespace geopro::net diff --git a/src/net/AuthService.cpp b/src/net/AuthService.cpp index e946093..734836b 100644 --- a/src/net/AuthService.cpp +++ b/src/net/AuthService.cpp @@ -2,9 +2,11 @@ #include #include -#include +#include +#include "ApiChain.hpp" #include "ApiClient.hpp" +#include "AuthLoads.hpp" #include "crypto/RsaEncryptor.hpp" namespace geopro::net { @@ -17,64 +19,39 @@ const char* const kPathImageCode = "/business/system/personalUser/getImageCode"; const char* const kPathVerifyCode = "/business/system/personalUser/verifyCodeCheck"; const char* const kPathLogin = "/admin/tenant/auth/login2"; -// 统一的错误信息:优先用服务端 msg,否则回退到传输层 rawError,最后给通用文案。 -QString errorFrom(const ApiResponse& resp, const QString& fallback) { - if (!resp.msg.isEmpty()) return resp.msg; - if (!resp.rawError.isEmpty()) return resp.rawError; - return fallback; -} - } // namespace AuthService::AuthService(ApiClient& api, std::string rsaPublicKeyPem) : api_(api), rsaPublicKeyPem_(std::move(rsaPublicKeyPem)) {} -AuthService::Captcha AuthService::fetchCaptcha() { - const ApiResponse resp = api_.get(QString::fromLatin1(kPathImageCode)); - Captcha cap; - if (resp.code != kCodeSuccess) { - return cap; // 失败时返回空 codeId/code,调用方据此判断 - } - cap.codeId = resp.data.value(QStringLiteral("id")).toString(); - cap.code = resp.data.value(QStringLiteral("code")).toString(); - return cap; +CaptchaLoad* AuthService::fetchCaptchaAsync() { + IApiCall* call = api_.getAsync(QString::fromLatin1(kPathImageCode)); + return new CaptchaLoad(call); } -AuthService::LoginResult AuthService::login(const QString& username, const QString& password, - const QString& code, const QString& codeId) { - // 1) 校验验证码(与 captcha 同会话)。 - const QJsonObject verifyBody{{QStringLiteral("code"), code}, - {QStringLiteral("codeId"), codeId}}; - const ApiResponse verify = api_.postJson(QString::fromLatin1(kPathVerifyCode), verifyBody); - if (verify.code != kCodeSuccess) { - return {false, QString(), errorFrom(verify, QStringLiteral("verifyCodeCheck failed"))}; - } +LoginLoad* AuthService::loginAsync(const QString& username, const QString& password, + const QString& code, const QString& codeId) { + // 失败判定与同步版一致:服务端 code != 200 或存在传输层 rawError。 + auto isFailure = [](const ApiResponse& r) { return r.code != kCodeSuccess || !r.rawError.isEmpty(); }; - // 2) RSA 加密密码(PKCS#1 v1.5 -> base64)。 - std::string encrypted; - try { + // step1:校验验证码(与 captcha 同会话)。 + ApiChain::StepFactory step1 = [this, code, codeId](const QList&) -> IApiCall* { + const QJsonObject body{{QStringLiteral("code"), code}, {QStringLiteral("codeId"), codeId}}; + return api_.postJsonAsync(QString::fromLatin1(kPathVerifyCode), body); + }; + + // step2:RSA 加密密码(PKCS#1 v1.5 -> base64,可抛 std::exception → ApiChain 转 failed)-> login2。 + ApiChain::StepFactory step2 = [this, username, password](const QList&) -> IApiCall* { RsaEncryptor enc(rsaPublicKeyPem_); - encrypted = enc.encryptBase64(password.toStdString()); - } catch (const std::exception& e) { - return {false, QString(), - QStringLiteral("RSA encryption failed: %1").arg(QString::fromUtf8(e.what()))}; - } + const std::string encrypted = enc.encryptBase64(password.toStdString()); + const QJsonObject body{{QStringLiteral("username"), username}, + {QStringLiteral("password"), QString::fromStdString(encrypted)}, + {QStringLiteral("checkCode"), QString()}}; + return api_.postJsonAsync(QString::fromLatin1(kPathLogin), body); + }; - // 3) login2:checkCode 传空串。 - const QJsonObject loginBody{ - {QStringLiteral("username"), username}, - {QStringLiteral("password"), QString::fromStdString(encrypted)}, - {QStringLiteral("checkCode"), QString()}}; - const ApiResponse login = api_.postJson(QString::fromLatin1(kPathLogin), loginBody); - if (login.code != kCodeSuccess) { - return {false, QString(), errorFrom(login, QStringLiteral("login2 failed"))}; - } - - const QString token = login.data.value(QStringLiteral("accessToken")).toString(); - if (token.isEmpty()) { - return {false, QString(), QStringLiteral("login2 succeeded but accessToken missing")}; - } - return {true, token, QString()}; + auto* chain = new ApiChain({step1, step2}, isFailure); + return new LoginLoad(chain); } } // namespace geopro::net diff --git a/src/net/AuthService.hpp b/src/net/AuthService.hpp index 23573fc..fe1f5cd 100644 --- a/src/net/AuthService.hpp +++ b/src/net/AuthService.hpp @@ -6,9 +6,13 @@ namespace geopro::net { class ApiClient; +class CaptchaLoad; +class LoginLoad; // 登录编排:复刻已实测通过的 getImageCode -> verifyCodeCheck -> RSA -> login2 流程。 // 依赖外部注入的 ApiClient(单实例、共享会话);RSA 公钥 PEM 由调用方读取后传入。 +// 异步:fetchCaptchaAsync/loginAsync 立即返回自管理句柄(net 层 CaptchaLoad/LoginLoad), +// 不阻塞 UI;句柄 done/failed 后自 deleteLater。AuthService 自身只创建句柄,无需是 QObject。 class AuthService { public: AuthService(ApiClient& api, std::string rsaPublicKeyPem); @@ -18,18 +22,14 @@ public: QString codeId; QString code; }; - Captcha fetchCaptcha(); - struct LoginResult { - bool ok = false; - QString token; - QString error; - }; + // 异步拉验证码:GET getImageCode;返回句柄,连 CaptchaLoad::done(Captcha)/failed(QString)。 + CaptchaLoad* fetchCaptchaAsync(); - // 校验验证码 -> RSA 加密密码 -> login2。成功返回 {true, accessToken, ""}; - // 任一步失败返回 {false, "", <服务端 msg 或本地错误>}。 - LoginResult login(const QString& username, const QString& password, const QString& code, - const QString& codeId); + // 异步登录:verifyCodeCheck -> RSA 加密密码 -> login2(依赖链)。 + // 返回句柄,连 LoginLoad::done(token)/failed(QString)。 + LoginLoad* loginAsync(const QString& username, const QString& password, const QString& code, + const QString& codeId); private: ApiClient& api_; @@ -37,3 +37,6 @@ private: }; } // namespace geopro::net + +#include +Q_DECLARE_METATYPE(geopro::net::AuthService::Captcha) diff --git a/src/net/CMakeLists.txt b/src/net/CMakeLists.txt index 4741214..46e3d2f 100644 --- a/src/net/CMakeLists.txt +++ b/src/net/CMakeLists.txt @@ -3,8 +3,14 @@ find_package(Qt6 COMPONENTS Core Network REQUIRED) add_library(geopro_net STATIC crypto/RsaEncryptor.cpp ApiClient.cpp - AuthService.cpp) + ApiResponseParse.cpp + IApiCall.cpp + ApiCall.cpp + ApiBatch.cpp + ApiChain.cpp + AuthService.cpp + AuthLoads.cpp) target_include_directories(geopro_net PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(geopro_net PUBLIC OpenSSL::SSL OpenSSL::Crypto Qt6::Core Qt6::Network) target_compile_features(geopro_net PUBLIC cxx_std_17) -set_target_properties(geopro_net PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF) +set_target_properties(geopro_net PROPERTIES AUTOMOC ON AUTOUIC OFF AUTORCC OFF) diff --git a/src/net/IApiCall.cpp b/src/net/IApiCall.cpp new file mode 100644 index 0000000..d93de1e --- /dev/null +++ b/src/net/IApiCall.cpp @@ -0,0 +1,6 @@ +#include "IApiCall.hpp" + +// IApiCall 是纯虚抽象句柄,无需额外实现。 +// 此 .cpp 存在是为了让 AUTOMOC 扫描到 Q_OBJECT 宏并生成 moc_IApiCall.cpp。 +namespace geopro::net { +} // namespace geopro::net diff --git a/src/net/IApiCall.hpp b/src/net/IApiCall.hpp new file mode 100644 index 0000000..db607c5 --- /dev/null +++ b/src/net/IApiCall.hpp @@ -0,0 +1,18 @@ +#pragma once +#include +#include "ApiClient.hpp" // geopro::net::ApiResponse + +namespace geopro::net { + +// 单个异步请求的抽象句柄(可测试缝)。ApiBatch 仅依赖此抽象,单测可注入假 call。 +class IApiCall : public QObject { + Q_OBJECT +public: + using QObject::QObject; + ~IApiCall() override = default; + virtual void abort() = 0; // 置 aborted_、中断、deleteLater;abort 后绝不 emit finished +signals: + void finished(const geopro::net::ApiResponse& resp); // 成功/错误均经此(错误写 rawError) +}; + +} // namespace geopro::net diff --git a/src/render/CMakeLists.txt b/src/render/CMakeLists.txt index 9250e21..0d25920 100644 --- a/src/render/CMakeLists.txt +++ b/src/render/CMakeLists.txt @@ -1,7 +1,7 @@ find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry FiltersModeling RenderingCore RenderingOpenGL2 RenderingVolumeOpenGL2 InteractionStyle InteractionWidgets IOImage) find_package(GDAL CONFIG REQUIRED) add_library(geopro_render STATIC - Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp VoxelFromScatters.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp actors/ElectrodeActor.cpp actors/TerrainActor.cpp) + Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp VoxelFromScatters.cpp ContourBands.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp actors/ElectrodeActor.cpp actors/TerrainActor.cpp) target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES} GDAL::GDAL) target_compile_features(geopro_render PUBLIC cxx_std_17) diff --git a/src/render/ContourBands.cpp b/src/render/ContourBands.cpp new file mode 100644 index 0000000..872d3a5 --- /dev/null +++ b/src/render/ContourBands.cpp @@ -0,0 +1,173 @@ +#include "ContourBands.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace geopro::render { +using geopro::core::Grid; +using geopro::core::ColorScale; +using geopro::core::Vec2; +using geopro::core::Rgba; + +namespace { + +// 双线性上采样:nx×ny → ((nx-1)*k+1)×((ny-1)*k+1)。NaN 单元参与的目标点置 NaN。 +Grid upsampleBilinear(const Grid& g, int k) { + if (k <= 1) return g; + const int nx = g.nx(), ny = g.ny(); + const int Nx = (nx - 1) * k + 1, Ny = (ny - 1) * k + 1; + Grid out(Nx, Ny); + out.x.resize(Nx); out.y.resize(Ny); + for (int I = 0; I < Nx; ++I) { double fi = double(I) / k; int i0 = std::min(int(fi), nx - 2); + double t = fi - i0; out.x[I] = g.x[i0] * (1 - t) + g.x[i0 + 1] * t; } + for (int J = 0; J < Ny; ++J) { double fj = double(J) / k; int j0 = std::min(int(fj), ny - 2); + double t = fj - j0; out.y[J] = g.y[j0] * (1 - t) + g.y[j0 + 1] * t; } + for (int J = 0; J < Ny; ++J) { + double fj = double(J) / k; int j0 = std::min(int(fj), ny - 2); double tj = fj - j0; + for (int I = 0; I < Nx; ++I) { + double fi = double(I) / k; int i0 = std::min(int(fi), nx - 2); double ti = fi - i0; + double v00 = g.valueAt(i0, j0), v10 = g.valueAt(i0 + 1, j0); + double v01 = g.valueAt(i0, j0 + 1), v11 = g.valueAt(i0 + 1, j0 + 1); + if (std::isnan(v00) || std::isnan(v10) || std::isnan(v01) || std::isnan(v11)) + out.valueAt(I, J) = std::nan(""); + else + out.valueAt(I, J) = (v00 * (1 - ti) + v10 * ti) * (1 - tj) + + (v01 * (1 - ti) + v11 * ti) * tj; + } + } + out.vmin = g.vmin; out.vmax = g.vmax; + return out; +} + +// 3x3 盒式平滑(NaN 跳过);strength 0..1 线性混合原值。 +Grid smoothGrid(const Grid& g, double strength) { + if (strength <= 0) return g; + const int nx = g.nx(), ny = g.ny(); + Grid out = g; + for (int j = 0; j < ny; ++j) + for (int i = 0; i < nx; ++i) { + double v = g.valueAt(i, j); + if (std::isnan(v)) continue; + double sum = 0; int n = 0; + for (int dj = -1; dj <= 1; ++dj) for (int di = -1; di <= 1; ++di) { + int ii = i + di, jj = j + dj; + if (ii < 0 || ii >= nx || jj < 0 || jj >= ny) continue; + double w = g.valueAt(ii, jj); if (std::isnan(w)) continue; sum += w; ++n; + } + if (n) out.valueAt(i, j) = v * (1 - strength) + (sum / n) * strength; + } + return out; +} + +// 把含任一 NaN 顶点的网格单元(quad)从网格中剔除→凸包裁剪。 +vtkSmartPointer toCellGrid(const Grid& g) { + const int nx = g.nx(), ny = g.ny(); + auto ug = vtkSmartPointer::New(); + vtkNew pts; pts->SetNumberOfPoints(static_cast(nx) * ny); + vtkNew sc; sc->SetName("v"); + sc->SetNumberOfTuples(static_cast(nx) * ny); + for (int j = 0; j < ny; ++j) for (int i = 0; i < nx; ++i) { + const vtkIdType id = static_cast(j) * nx + i; + pts->SetPoint(id, g.x[i], g.y[j], 0.0); + const double v = g.valueAt(i, j); + sc->SetValue(id, std::isnan(v) ? 0.0 : v); + } + ug->SetPoints(pts); + ug->GetPointData()->SetScalars(sc); + auto idAt = [nx](int i, int j) { return static_cast(j) * nx + i; }; + for (int j = 0; j < ny - 1; ++j) for (int i = 0; i < nx - 1; ++i) { + if (!g.hasValue(i, j) || !g.hasValue(i+1, j) || !g.hasValue(i, j+1) || !g.hasValue(i+1, j+1)) + continue; // 含 NaN 的 quad 不入网格 → 凸包裁剪 + vtkIdType quad[4] = { idAt(i,j), idAt(i+1,j), idAt(i+1,j+1), idAt(i,j+1) }; + ug->InsertNextCell(VTK_QUAD, 4, quad); + } + return ug; +} + +} // namespace + +ContourBandsResult buildContourBands(const Grid& g, const ColorScale& cs, const ContourOptions& opt) { + ContourBandsResult out; + const int nx = g.nx(), ny = g.ny(); + if (nx < 2 || ny < 2 || g.x.size() < 2 || g.y.size() < 2) return out; + + const std::vector stops = cs.stopValues(); + if (stops.size() < 2) return out; + + Grid work = smoothGrid(upsampleBilinear(g, std::max(1, opt.upsample)), opt.smooth); + vtkNew surf; surf->SetInputData(toCellGrid(work)); + vtkNew banded; + banded->SetInputConnection(surf->GetOutputPort()); + banded->SetNumberOfContours(static_cast(stops.size())); + for (int i = 0; i < static_cast(stops.size()); ++i) banded->SetValue(i, stops[i]); + if (opt.makeLines) banded->GenerateContourEdgesOn(); + banded->SetScalarModeToValue(); + banded->Update(); + + // port0:色带多边形(cell 标量=带的代表值)→ 按 ColorScale 上色。 + vtkPolyData* poly = banded->GetOutput(); + vtkDataArray* cellScalars = poly->GetCellData()->GetScalars(); + const vtkIdType nCells = poly->GetNumberOfCells(); + for (vtkIdType c = 0; c < nCells; ++c) { + vtkCell* cell = poly->GetCell(c); + vtkPoints* cp = cell->GetPoints(); + if (cp->GetNumberOfPoints() < 3) continue; + BandPolygon bp; + const double val = cellScalars ? cellScalars->GetTuple1(c) : 0.0; + bp.color = cs.colorAt(val); + for (vtkIdType p = 0; p < cp->GetNumberOfPoints(); ++p) { + double xyz[3]; cp->GetPoint(p, xyz); + bp.ring.push_back(Vec2{xyz[0], xyz[1]}); + } + out.bands.push_back(std::move(bp)); + } + + // port1:等值线。banded 边输出是大量 2 点短线段——用 vtkStripper 连成长折线, + // 便于沿线标注与减少绘制调用。仅 makeLines=true 时生成。 + if (opt.makeLines) { + vtkPolyData* edges = banded->GetContourEdgesOutput(); + if (edges) { + vtkNew stripper; + stripper->SetInputData(edges); + stripper->JoinContiguousSegmentsOn(); + // 样条平滑:把折线拟合成平滑曲线并按弧长重采样(对齐原版圆滑等值线)。 + // 重采样间距取数据 x 跨度的 ~1/300,足够平滑且点数可控。 + const double xspan = g.x.empty() ? 1.0 : (g.x.back() - g.x.front()); + vtkNew spline; + spline->SetInputConnection(stripper->GetOutputPort()); + spline->SetSubdivideToLength(); + spline->SetLength(xspan > 0 ? xspan / 300.0 : 1.0); + spline->Update(); + vtkPolyData* joined = spline->GetOutput(); + joined->BuildCells(); + const vtkIdType nLines = joined->GetNumberOfCells(); + for (vtkIdType c = 0; c < nLines; ++c) { + vtkCell* cell = joined->GetCell(c); + vtkPoints* cp = cell->GetPoints(); + ContourLine cl; cl.level = 0.0; + for (vtkIdType p = 0; p < cp->GetNumberOfPoints(); ++p) { + double xyz[3]; cp->GetPoint(p, xyz); + cl.pts.push_back(Vec2{xyz[0], xyz[1]}); + } + // 不再 DP 简化:样条已平滑且点数受重采样间距控制;简化会重新折线化。 + if (cl.pts.size() >= 2) out.lines.push_back(std::move(cl)); + } + } + } + return out; +} +} // namespace geopro::render diff --git a/src/render/ContourBands.hpp b/src/render/ContourBands.hpp new file mode 100644 index 0000000..188d4e0 --- /dev/null +++ b/src/render/ContourBands.hpp @@ -0,0 +1,32 @@ +#pragma once +#include +#include +#include "model/Field.hpp" +#include "model/ColorScale.hpp" +#include "model/Anomaly.hpp" +namespace geopro::render { + +struct BandPolygon { + geopro::core::Rgba color; + std::vector ring; // 单环多边形(局部坐标,y=高程向上为正) +}; +struct ContourLine { + double level = std::numeric_limits::quiet_NaN(); // NaN=未解析(与真实 level 0 区分) + std::vector pts; +}; +struct ContourBandsResult { + std::vector bands; + std::vector lines; +}; +struct ContourOptions { + int upsample = 2; // 双线性上采样倍数(1=不采样) + double smooth = 0.3; // 平滑强度 0..1(0=不平滑) + bool makeLines = true; +}; + +// core::Grid(NaN=无效)+ ColorScale 分段 → 色带多边形 + 等值线几何。 +// VTK 仅作算法(vtkBandedPolyDataContourFilter),无 render window。 +ContourBandsResult buildContourBands(const geopro::core::Grid& g, + const geopro::core::ColorScale& cs, + const ContourOptions& opt = {}); +} // namespace geopro::render diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7296aaf..cf0a2d8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -37,16 +37,32 @@ target_link_libraries(geopro_tests PRIVATE geopro_core) target_sources(geopro_tests PRIVATE data/test_parsers.cpp) target_sources(geopro_tests PRIVATE data/test_local_repo.cpp) target_sources(geopro_tests PRIVATE data/test_nav_dto.cpp) +target_sources(geopro_tests PRIVATE data/test_dataset_chart_dto.cpp) +target_sources(geopro_tests PRIVATE data/test_measurement_dto.cpp) +target_sources(geopro_tests PRIVATE data/test_gr_dto.cpp) +target_sources(geopro_tests PRIVATE data/test_trajectory_dto.cpp) +target_sources(geopro_tests PRIVATE data/test_grid_dto.cpp) +target_sources(geopro_tests PRIVATE data/test_dataset_load_handles.cpp) +# 通用仓储分派离线单测(loadAsync 分派 + QVariant payload round-trip)。 +target_sources(geopro_tests PRIVATE data/test_async_repo_dispatch.cpp) +# NavRequest 离线单测(QVariant payload: done/failed/abort 闸门)。 +target_sources(geopro_tests PRIVATE data/test_nav_request.cpp) target_link_libraries(geopro_tests PRIVATE geopro_data) # net 层:RSA 加密器。测试需直接用 OpenSSL 生成/解密密钥,故显式 find_package # 并链接 OpenSSL(geopro_net 的 PUBLIC 链接通常已传递,这里显式以防头文件找不到)。 find_package(OpenSSL REQUIRED) -find_package(Qt6 COMPONENTS Core Network REQUIRED) +find_package(Qt6 COMPONENTS Core Network Test REQUIRED) target_sources(geopro_tests PRIVATE net/test_rsa.cpp) # 端到端登录连通测试(ApiClient + AuthService),需 Qt6::Core/Network 与事件循环。 target_sources(geopro_tests PRIVATE net/test_auth.cpp) -target_link_libraries(geopro_tests PRIVATE geopro_net OpenSSL::SSL OpenSSL::Crypto Qt6::Core Qt6::Network) +# ApiBatch 离线单测(QSignalSpy 需 Qt6::Test)。 +target_sources(geopro_tests PRIVATE net/test_api_batch.cpp) +# ApiChain 离线单测(顺序依赖链:顺序/失败短路/abort闸门/工厂抛异常)。 +target_sources(geopro_tests PRIVATE net/test_api_chain.cpp) +# AuthLoads 离线单测(CaptchaLoad/LoginLoad 句柄:done/failed/abort 闸门)。 +target_sources(geopro_tests PRIVATE net/test_auth_loads.cpp) +target_link_libraries(geopro_tests PRIVATE geopro_net OpenSSL::SSL OpenSSL::Crypto Qt6::Core Qt6::Network Qt6::Test) # geopro_data 链 Qt6::Core,测试 exe 运行(含 gtest 发现)需要 Qt6Core.dll 等运行时 # DLL 在旁。复用 app 同样的 TARGET_RUNTIME_DLLS POST_BUILD 拷贝。 @@ -61,6 +77,7 @@ endif() # 需 vtkLookupTable(VTK::CommonCore);geopro_render 已 PUBLIC 传递其余 VTK 组件。 find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel RenderingCore) target_sources(geopro_tests PRIVATE render/test_color_lut.cpp) +target_sources(geopro_tests PRIVATE render/test_contour_bands.cpp) # dd_voxel:buildVoxel(ScalarVolume->vtkImageData->GPU 体绘制) 构建不崩 + dims 正确。 target_sources(geopro_tests PRIVATE render/test_voxel_build.cpp) # dd_voxel 回归:buildVoxelFromScatters(散点 projX/Y -EPSG:4547-> 世界系 + IDW) 配准+充填(需 PROJ_DATA)。 @@ -78,4 +95,28 @@ target_sources(geopro_tests PRIVATE render/test_terrain.cpp) target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES}) vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES}) +# Qwt 集成冒烟(仅当 qwt 目标存在,即 external/qwt-src 已拉取)。 +if(TARGET qwt) + target_sources(geopro_tests PRIVATE render/test_qwt_smoke.cpp) + target_link_libraries(geopro_tests PRIVATE qwt) +endif() + +target_include_directories(geopro_tests PRIVATE ${CMAKE_SOURCE_DIR}/src/app) +# tests/ 在 include 路径:net/test_api_batch.cpp 用 #include "net/FakeApiCall.hpp"。 +target_include_directories(geopro_tests PRIVATE ${CMAKE_SOURCE_DIR}/tests) +target_sources(geopro_tests PRIVATE app/test_chart_strategy_registry.cpp) +# ColorMapService 测试(geopro_desktop 是可执行文件,直接把源加入测试目标) +target_sources(geopro_tests PRIVATE + app/test_colormap_service.cpp + ${CMAKE_SOURCE_DIR}/src/app/panels/chart/ColorMapService.cpp +) +# 散点 hover 文本格式(inline 纯函数,无需链 qwt/实例化 ScatterHoverTip)。 +target_sources(geopro_tests PRIVATE app/test_scatter_hover.cpp) + +# controller 层:DatasetDetailController 编排集成测试(QSignalSpy 验证 datasetOpened/tabReady/loadFailed)。 +find_package(Qt6 COMPONENTS Test REQUIRED) +target_sources(geopro_tests PRIVATE controller/test_dataset_detail_controller.cpp) +target_sources(geopro_tests PRIVATE controller/test_workbench_nav_controller.cpp) +target_link_libraries(geopro_tests PRIVATE geopro_controller Qt6::Test) + add_subdirectory(spike) # spike S3: banded contour 渲染验证 diff --git a/tests/app/test_chart_strategy_registry.cpp b/tests/app/test_chart_strategy_registry.cpp new file mode 100644 index 0000000..9a508b6 --- /dev/null +++ b/tests/app/test_chart_strategy_registry.cpp @@ -0,0 +1,109 @@ +#include +#include "DatasetDetailTab.hpp" +#include "IDatasetChartStrategy.hpp" // geopro::controller(控制器层) +#include "panels/chart/MeasurementStrategy.hpp" +#include "panels/chart/GrMeasurementStrategy.hpp" +#include "panels/chart/TrajectoryStrategy.hpp" +#include "panels/chart/GridStrategy.hpp" +using namespace geopro::controller; +namespace { +struct Fake : IDatasetChartStrategy { + std::string ddCode() const override { return "dd_inversion_data"; } + std::vector tabs() const override { + return { + {QStringLiteral("原数据"), ViewKind::Scatter, + QStringLiteral("inversion.scatter"), /*lazy*/ false, /*paginated*/ false}, + {QStringLiteral("网格数据"), ViewKind::FilledContour, + QStringLiteral("inversion.grid"), /*lazy*/ true, /*paginated*/ false}, + }; + } +}; +} +TEST(ChartStrategyRegistry, FindsRegisteredAndDegradesUnknown) { + ChartStrategyRegistry reg; + reg.add(std::make_unique()); + EXPECT_TRUE(reg.supports("dd_inversion_data")); + EXPECT_NE(reg.find("dd_inversion_data"), nullptr); + EXPECT_FALSE(reg.supports("dd_unknown")); + EXPECT_EQ(reg.find("dd_unknown"), nullptr); +} +TEST(ChartStrategyRegistry, ExposesTabSpecsFromStrategy) { + ChartStrategyRegistry reg; + reg.add(std::make_unique()); + auto* s = reg.find("dd_inversion_data"); + ASSERT_NE(s, nullptr); + const auto tabs = s->tabs(); + ASSERT_EQ(tabs.size(), 2u); + EXPECT_FALSE(tabs[0].lazy); // 原数据非 lazy + EXPECT_TRUE(tabs[1].lazy); // 网格数据 lazy + EXPECT_EQ(tabs[1].kind, ViewKind::FilledContour); +} + +TEST(MeasurementStrategy, DrivesTwoTabsScatterAndTable) { + geopro::app::MeasurementStrategy s; + EXPECT_EQ(s.ddCode(), "dd_ert_measurement_data"); + const auto tabs = s.tabs(); + ASSERT_EQ(tabs.size(), 2u); + // 散点图:Scatter,非 lazy。 + EXPECT_EQ(tabs[0].title.toStdString(), std::string("散点图")); + EXPECT_EQ(tabs[0].kind, ViewKind::Scatter); + EXPECT_FALSE(tabs[0].lazy); + EXPECT_EQ(tabs[0].loaderKey.toStdString(), "ert_measurement.scatter"); + // 数据列表:Table,lazy。 + EXPECT_EQ(tabs[1].title.toStdString(), std::string("数据列表")); + EXPECT_EQ(tabs[1].kind, ViewKind::Table); + EXPECT_TRUE(tabs[1].lazy); + EXPECT_EQ(tabs[1].loaderKey.toStdString(), "ert_measurement.rows"); +} + +TEST(GrMeasurementStrategy, DrivesBarAndTableTabs) { + geopro::app::GrMeasurementStrategy s; + EXPECT_EQ(s.ddCode(), "dd_ert_measurement_gr_data"); + const auto tabs = s.tabs(); + ASSERT_EQ(tabs.size(), 2u); + // 柱状图:Bar,非 lazy。 + EXPECT_EQ(tabs[0].title.toStdString(), std::string("柱状图")); + EXPECT_EQ(tabs[0].kind, ViewKind::Bar); + EXPECT_FALSE(tabs[0].lazy); + EXPECT_EQ(tabs[0].loaderKey.toStdString(), "gr.bar"); + // 列表:Table,lazy。 + EXPECT_EQ(tabs[1].title.toStdString(), std::string("列表")); + EXPECT_EQ(tabs[1].kind, ViewKind::Table); + EXPECT_TRUE(tabs[1].lazy); + EXPECT_EQ(tabs[1].loaderKey.toStdString(), "gr.rows"); +} + +TEST(TrajectoryStrategy, DrivesMapTableElevationTabs) { + geopro::app::TrajectoryStrategy s; + EXPECT_EQ(s.ddCode(), "dd_trajectory_data"); + const auto tabs = s.tabs(); + ASSERT_EQ(tabs.size(), 3u); + // 地图:WebMap,首位/非 lazy(默认页,loaderKey traj.map → MapPayload;端点 trajectory/line)。 + EXPECT_EQ(tabs[0].title.toStdString(), std::string("地图")); + EXPECT_EQ(tabs[0].kind, ViewKind::WebMap); + EXPECT_FALSE(tabs[0].lazy); + EXPECT_EQ(tabs[0].loaderKey.toStdString(), "traj.map"); + // 列表:Table,lazy。 + EXPECT_EQ(tabs[1].title.toStdString(), std::string("列表")); + EXPECT_EQ(tabs[1].kind, ViewKind::Table); + EXPECT_TRUE(tabs[1].lazy); + EXPECT_EQ(tabs[1].loaderKey.toStdString(), "traj.rows"); + // 高程:LineProfile,非 lazy。 + EXPECT_EQ(tabs[2].title.toStdString(), std::string("高程")); + EXPECT_EQ(tabs[2].kind, ViewKind::LineProfile); + EXPECT_FALSE(tabs[2].lazy); + EXPECT_EQ(tabs[2].loaderKey.toStdString(), "traj.elev"); +} + +TEST(GridStrategy, DrivesSinglePaginatedListTab) { + geopro::app::GridStrategy s; + EXPECT_EQ(s.ddCode(), "dd_grid"); + const auto tabs = s.tabs(); + ASSERT_EQ(tabs.size(), 1u); + // 列表:Table,非 lazy,分页(paginated)。loaderKey grid.rows → TablePayload。 + EXPECT_EQ(tabs[0].title.toStdString(), std::string("列表")); + EXPECT_EQ(tabs[0].kind, ViewKind::Table); + EXPECT_FALSE(tabs[0].lazy); + EXPECT_TRUE(tabs[0].paginated); + EXPECT_EQ(tabs[0].loaderKey.toStdString(), "grid.rows"); +} diff --git a/tests/app/test_colormap_service.cpp b/tests/app/test_colormap_service.cpp new file mode 100644 index 0000000..c5a7e3d --- /dev/null +++ b/tests/app/test_colormap_service.cpp @@ -0,0 +1,84 @@ +#include +#include +#include "panels/chart/ColorMapService.hpp" +using namespace geopro; + +TEST(ColorMapService, ContinuousInterpolatesBetweenStops) { + core::ColorScale cs; + cs.addStop(0.0, core::Rgba{0, 0, 0, 255}); + cs.addStop(100.0, core::Rgba{255, 255, 255, 255}); + app::ColorMapService svc(cs); + EXPECT_NEAR(svc.normalized(50.0), 0.5, 1e-9); + auto c = svc.colorAtContinuous(50.0); // 黑白中点应约为灰 + EXPECT_GT(c.r, 100); EXPECT_LT(c.r, 160); +} + +TEST(ColorMapService, NormalizedClampsOutOfRange) { + core::ColorScale cs; + cs.addStop(0.0, core::Rgba{0, 0, 0, 255}); + cs.addStop(100.0, core::Rgba{255, 255, 255, 255}); + app::ColorMapService svc(cs); + EXPECT_NEAR(svc.normalized(-10.0), 0.0, 1e-9); + EXPECT_NEAR(svc.normalized(110.0), 1.0, 1e-9); +} + +TEST(ColorMapService, ColorAtContinuousAtExtremes) { + core::ColorScale cs; + cs.addStop(0.0, core::Rgba{10, 20, 30, 255}); + cs.addStop(100.0, core::Rgba{200, 210, 220, 255}); + app::ColorMapService svc(cs); + auto cMin = svc.colorAtContinuous(0.0); + EXPECT_EQ(cMin.r, 10); + EXPECT_EQ(cMin.g, 20); + EXPECT_EQ(cMin.b, 30); + auto cMax = svc.colorAtContinuous(100.0); + EXPECT_EQ(cMax.r, 200); + EXPECT_EQ(cMax.g, 210); + EXPECT_EQ(cMax.b, 220); +} + +// 复刻原版 Plotly:散点颜色按**数据 min/max(cauto)**归一化,而色阶形状(断点位置)仍按 +// colorBar 断点值范围。数据范围窄于色阶范围时,应把整段光谱铺满数据范围(而非压进一小段)。 +TEST(ColorMapService, DataRangeDecouplesFromColorscaleShape) { + core::ColorScale cs; + cs.addStop(0.0, core::Rgba{0, 0, 255, 255}); // blue pos 0.0 + cs.addStop(500.0, core::Rgba{0, 255, 0, 255}); // green pos 0.5 + cs.addStop(1000.0, core::Rgba{255, 0, 0, 255}); // red pos 1.0 + app::ColorMapService svc(cs); + + // 默认数据范围 = 断点范围 [0,1000]:值 250 → 归一化 0.25 → 蓝绿之间(非纯绿)。 + EXPECT_NEAR(svc.normalized(250.0), 0.25, 1e-9); + + // 收窄数据范围到 [0,500](模拟 cauto 取数据 min/max): + svc.setDataRange(0.0, 500.0); + EXPECT_NEAR(svc.normalized(250.0), 0.5, 1e-9); + // 值 250 → 归一化 0.5 → 落在色阶位置 0.5 = 纯绿。 + auto mid = svc.colorAtContinuous(250.0); + EXPECT_EQ(mid.r, 0); EXPECT_EQ(mid.g, 255); EXPECT_EQ(mid.b, 0); + // 数据范围两端铺到色阶两端:0→蓝、500→红。 + auto lo = svc.colorAtContinuous(0.0); + EXPECT_EQ(lo.b, 255); EXPECT_EQ(lo.r, 0); + auto hi = svc.colorAtContinuous(500.0); + EXPECT_EQ(hi.r, 255); EXPECT_EQ(hi.b, 0); +} + +// 脏数据健壮性:NaN 值取色不得崩溃(曾因 upper_bound 用 NaN 比较返回 end() 后解引用越界)。 +TEST(ColorMapService, ColorAtContinuousNaNSafe) { + core::ColorScale cs; + cs.addStop(0.0, core::Rgba{11, 22, 33, 255}); + cs.addStop(100.0, core::Rgba{200, 210, 220, 255}); + app::ColorMapService svc(cs); + auto c = svc.colorAtContinuous(std::nan("")); // 不崩 → 回退首断点色 + EXPECT_EQ(c.r, 11); + EXPECT_EQ(c.g, 22); + EXPECT_EQ(c.b, 33); +} + +TEST(ColorMapService, ScaleRefReturnsOriginal) { + core::ColorScale cs; + cs.addStop(0.0, core::Rgba{0, 0, 0, 255}); + cs.addStop(50.0, core::Rgba{128, 0, 0, 255}); + cs.addStop(100.0, core::Rgba{255, 0, 0, 255}); + app::ColorMapService svc(cs); + EXPECT_EQ(svc.scale().stopCount(), 3u); +} diff --git a/tests/app/test_scatter_hover.cpp b/tests/app/test_scatter_hover.cpp new file mode 100644 index 0000000..bff1fa9 --- /dev/null +++ b/tests/app/test_scatter_hover.cpp @@ -0,0 +1,32 @@ +#include +#include "panels/chart/ScatterHoverTip.hpp" + +using geopro::app::electrodeHoverText; +using geopro::app::measurementHoverText; +using geopro::app::scatterHoverText; + +// 对齐原版 Plotly hovertemplate: +// X: %{x:.3f}
Y: %{y:.3f}
值: %{marker.color:.3f} +TEST(ScatterHoverTip, FormatsXYValueWith3Decimals) { + const QString t = scatterHoverText(12.3456, 24.0, 60.77); + EXPECT_EQ(t, QStringLiteral("X: 12.346
Y: 24.000
值: 60.770")); +} + +TEST(ScatterHoverTip, RoundsAndPadsToFixed3) { + // 负值、整数、需补零各覆盖一次 + EXPECT_EQ(scatterHoverText(-1.0, 0.5, 1.0), + QStringLiteral("X: -1.000
Y: 0.500
值: 1.000")); +} + +// measurement 浮动框:X/Y/Value/a/b/m/n 多行(对齐原版 Plotly 浮动框)。 +TEST(ScatterHoverTip, MeasurementFloatingTipShowsXYValueABMN) { + const QString t = measurementHoverText(2.283, -1.2, 242.952988, 1, 4, 2, 3); + EXPECT_EQ(t, QStringLiteral("X: 2.283
Y: -1.200
Value: 242.953" + "
a: 1
b: 4
m: 2
n: 3")); +} + +// 电极浮动框:x(7 位有效数字)/ y: 0 / num(电极编号),对齐原版 Plotly 电极 trace。 +TEST(ScatterHoverTip, ElectrodeFloatingTipShowsXYNum) { + const QString t = electrodeHoverText(5.123837209632222, 4); + EXPECT_EQ(t, QStringLiteral("x: 5.123837
y: 0
num: 4")); +} diff --git a/tests/controller/test_dataset_detail_controller.cpp b/tests/controller/test_dataset_detail_controller.cpp new file mode 100644 index 0000000..b175c96 --- /dev/null +++ b/tests/controller/test_dataset_detail_controller.cpp @@ -0,0 +1,201 @@ +#include +#include +#include +#include "DatasetDetailController.hpp" +#include "DatasetDetailTab.hpp" +#include "IDatasetChartStrategy.hpp" +#include "repo/IAsyncDatasetRepository.hpp" +#include "api/DatasetLoadHandles.hpp" +using namespace geopro; + +namespace { +// 反演策略桩:散点(非 lazy) + 网格(lazy) 两页签。 +struct InversionStrategy : controller::IDatasetChartStrategy { + std::string ddCode() const override { return "dd_inversion_data"; } + std::vector tabs() const override { + return { + {QStringLiteral("原数据"), controller::ViewKind::Scatter, + QStringLiteral("inversion.scatter"), /*lazy*/ false, /*paginated*/ false}, + {QStringLiteral("网格数据"), controller::ViewKind::FilledContour, + QStringLiteral("inversion.grid"), /*lazy*/ true, /*paginated*/ false}, + }; + } +}; +// 单页签策略桩(仅一个非 lazy 页签)。 +struct SingleTabStrategy : controller::IDatasetChartStrategy { + std::string ddCode() const override { return "dd_scatter_only"; } + std::vector tabs() const override { + return {{QStringLiteral("散点"), controller::ViewKind::Scatter, + QStringLiteral("scatter.only"), /*lazy*/ false, /*paginated*/ false}}; + } +}; +// 注册了反演策略的注册表(多数用例复用)。 +controller::ChartStrategyRegistry makeInversionRegistry() { + controller::ChartStrategyRegistry reg; + reg.add(std::make_unique()); + return reg; +} + +// 桩详情句柄:不声明 Q_OBJECT —— 发射继承自 data::DetailLoad 的信号、override abort。 +struct StubDetailLoad : data::DetailLoad { + bool aborted = false; + void abort() override { aborted = true; } + void fireDone() { emit done(QVariant{}); } + void fireFailed() { emit failed(QStringLiteral("x")); } +}; +// 桩仓储:每个 loaderKey 都造一个新句柄,记录最近一个用于 fire。 +struct StubAsyncRepo : data::IAsyncDatasetRepository { + StubDetailLoad* last = nullptr; + int lastPageNo = 0, lastPageSize = 0; // 记录最近一次分页参数(验证 loadTabPaged 透传) + data::DetailLoad* loadAsync(const std::string&, const std::string&, int pageNo, + int pageSize) override { + lastPageNo = pageNo; + lastPageSize = pageSize; + last = new StubDetailLoad; + return last; + } +}; +} // namespace + +TEST(DatasetDetailController, OpenEmitsDatasetOpenedWithTabsAndDdCode) { + StubAsyncRepo repo; + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); + QSignalSpy spy(&c, &controller::DatasetDetailController::datasetOpened); + c.openDataset("ds1", "dd_inversion_data", "名称"); + ASSERT_EQ(spy.count(), 1); + const auto args = spy.takeFirst(); + EXPECT_EQ(args.at(0).toString(), QStringLiteral("ds1")); + EXPECT_EQ(args.at(1).toString(), QStringLiteral("dd_inversion_data")); // ddCode 透传 + EXPECT_EQ(args.at(2).toString(), QStringLiteral("名称")); +} + +TEST(DatasetDetailController, OpenLoadsNonLazyTabsOnly) { + StubAsyncRepo repo; + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); + QSignalSpy started(&c, &controller::DatasetDetailController::tabLoadStarted); + c.openDataset("ds1", "dd_inversion_data"); + // 反演:tab0 非 lazy 自动加载,tab1 lazy 不加载。 + ASSERT_EQ(started.count(), 1); + EXPECT_EQ(started.takeFirst().at(1).toInt(), 0); +} + +TEST(DatasetDetailController, EmitsTabReadyOnDone) { + StubAsyncRepo repo; + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); + QSignalSpy spy(&c, &controller::DatasetDetailController::tabReady); + c.openDataset("ds1", "dd_inversion_data"); + repo.last->fireDone(); + ASSERT_EQ(spy.count(), 1); + EXPECT_EQ(spy.takeFirst().at(1).toInt(), 0); // tabIndex 0 +} + +TEST(DatasetDetailController, EmitsLoadFailedOnFailed) { + StubAsyncRepo repo; + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); + QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed); + c.openDataset("ds1", "dd_inversion_data"); + repo.last->fireFailed(); + EXPECT_EQ(spy.count(), 1); +} + +TEST(DatasetDetailController, UnsupportedTypeFailsImmediately) { + StubAsyncRepo repo; + auto reg = makeInversionRegistry(); // 注册了反演,但未注册 dd_other + controller::DatasetDetailController c(repo, reg); + QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed); + c.openDataset("ds1", "dd_other"); + EXPECT_EQ(spy.count(), 1); + EXPECT_EQ(repo.last, nullptr); // 未发起加载 +} + +// 空注册表 → 任意 ddCode 都不支持 → loadFailed,不发起加载。 +TEST(DatasetDetailController, EmptyRegistryFailsAnyType) { + StubAsyncRepo repo; + controller::ChartStrategyRegistry reg; // 空 + controller::DatasetDetailController c(repo, reg); + QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed); + c.openDataset("ds1", "dd_inversion_data"); + EXPECT_EQ(spy.count(), 1); + EXPECT_EQ(repo.last, nullptr); +} + +// 越界 tabIndex → loadTab 静默不加载。 +TEST(DatasetDetailController, LoadTabOutOfRangeDoesNothing) { + StubAsyncRepo repo; + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); + c.loadTab("ds1", "dd_inversion_data", 99); + EXPECT_EQ(repo.last, nullptr); +} + +// lazy 页签经 loadTab 触发后加载。 +TEST(DatasetDetailController, LoadTabLazyTabStartsLoad) { + StubAsyncRepo repo; + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); + QSignalSpy ready(&c, &controller::DatasetDetailController::tabReady); + c.loadTab("ds1", "dd_inversion_data", 1); // lazy 网格页 + ASSERT_NE(repo.last, nullptr); + repo.last->fireDone(); + ASSERT_EQ(ready.count(), 1); + EXPECT_EQ(ready.takeFirst().at(1).toInt(), 1); +} + +// loadTab(非分页)用默认页参(pageNo=1/pageSize=0);loadTabPaged 透传分页参数到仓储。 +TEST(DatasetDetailController, LoadTabUsesDefaultPageParams) { + StubAsyncRepo repo; + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); + c.loadTab("ds1", "dd_inversion_data", 0); + EXPECT_EQ(repo.lastPageNo, 1); + EXPECT_EQ(repo.lastPageSize, 0); +} + +TEST(DatasetDetailController, LoadTabPagedThreadsPageParams) { + StubAsyncRepo repo; + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); + c.loadTabPaged("ds1", "dd_inversion_data", 0, 3, 100); + ASSERT_NE(repo.last, nullptr); + EXPECT_EQ(repo.lastPageNo, 3); + EXPECT_EQ(repo.lastPageSize, 100); +} + +TEST(DatasetDetailController, AbortsPreviousOnSameSlotReload) { + StubAsyncRepo repo; + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); + c.loadTab("dsA", "dd_inversion_data", 0); + StubDetailLoad* a = repo.last; + c.loadTab("dsB", "dd_inversion_data", 0); // 同槽位替换 + EXPECT_TRUE(a->aborted); // 旧句柄被 abort +} + +TEST(DatasetDetailController, DropsLateSignalFromAbortedLoad) { + StubAsyncRepo repo; + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); + QSignalSpy spy(&c, &controller::DatasetDetailController::tabReady); + c.loadTab("dsA", "dd_inversion_data", 0); + StubDetailLoad* a = repo.last; + c.loadTab("dsB", "dd_inversion_data", 0); // 同槽位替换 + StubDetailLoad* b = repo.last; + a->fireDone(); // 旧句柄迟到 → 身份比对丢弃 + EXPECT_EQ(spy.count(), 0); + b->fireDone(); // 当前句柄 → 正常 + EXPECT_EQ(spy.count(), 1); +} + +TEST(DatasetDetailController, FocusEmitsFocusRequested) { + StubAsyncRepo repo; + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); + QSignalSpy spy(&c, &controller::DatasetDetailController::focusRequested); + c.focusDataset("ds1"); + ASSERT_EQ(spy.count(), 1); + EXPECT_EQ(spy.takeFirst().at(0).toString(), QStringLiteral("ds1")); +} diff --git a/tests/controller/test_workbench_nav_controller.cpp b/tests/controller/test_workbench_nav_controller.cpp new file mode 100644 index 0000000..a74ba2f --- /dev/null +++ b/tests/controller/test_workbench_nav_controller.cpp @@ -0,0 +1,263 @@ +#include +#include +#include +#include + +#include "WorkbenchNavController.hpp" +#include "api/NavLoads.hpp" +#include "api/NavRequest.hpp" +#include "repo/IAsyncProjectRepository.hpp" + +using namespace geopro; + +namespace { +// 桩句柄:不声明 Q_OBJECT —— 发射继承自 data::NavRequest 的 done/failed、override abort 记录。 +struct StubNavRequest : data::NavRequest { + bool aborted = false; + void abort() override { aborted = true; } + void fireDone(const QVariant& v) { emit done(v); } + void fireFailed() { emit failed(QStringLiteral("x")); } +}; + +struct StubAsyncRepo : data::IAsyncProjectRepository { + StubNavRequest* lastWorkspaces = nullptr; + StubNavRequest* lastSwitchWs = nullptr; + StubNavRequest* lastProjects = nullptr; + StubNavRequest* lastStructure = nullptr; + StubNavRequest* lastData = nullptr; + StubNavRequest* lastFile = nullptr; + StubNavRequest* lastDetail = nullptr; + StubNavRequest* lastDataset = nullptr; + std::vector exceptions; // setCheckedTms 并发批 + + data::NavRequest* listWorkspacesAsync() override { return lastWorkspaces = new StubNavRequest; } + data::NavRequest* switchWorkspaceAsync(const std::string&) override { + return lastSwitchWs = new StubNavRequest; + } + data::NavRequest* pageProjectsAsync(const std::string&, const std::string&, int, int) override { + return lastProjects = new StubNavRequest; + } + data::NavRequest* listProjectTypesAsync() override { return new StubNavRequest; } + data::NavRequest* loadStructureAsync(const std::string&) override { + return lastStructure = new StubNavRequest; + } + data::NavRequest* loadRowsAsync(const std::string&, const std::string&, int, int classifyType, + int, int) override { + auto* r = new StubNavRequest; + if (classifyType == 1) + lastFile = r; + else + lastData = r; + return r; + } + data::NavRequest* loadObjectDetailAsync(const std::string&, int) override { + return lastDetail = new StubNavRequest; + } + data::NavRequest* loadDatasetFormAsync(const std::string&) override { + return lastDataset = new StubNavRequest; + } + data::NavRequest* loadExceptionsByTmAsync(const std::string&) override { + auto* r = new StubNavRequest; + exceptions.push_back(r); + return r; + } +}; + +QVariant wsVar() { + return QVariant::fromValue(std::vector{{"w1", "WS", 2, true}}); +} +QVariant pageVar() { + data::ProjectSummary p; + p.id = "p1"; + p.name = "P1"; + return QVariant::fromValue(data::ProjectListPage{{p}, 1}); +} +QVariant emptyPageVar() { return QVariant::fromValue(data::ProjectListPage{{}, 0}); } +QVariant nodesVar() { return QVariant::fromValue(std::vector{}); } +QVariant dsPageVar() { return QVariant::fromValue(data::DsPage{{}, 0}); } +QVariant formVar() { return QVariant::fromValue(data::DynamicForm{}); } +QVariant exVar() { return QVariant::fromValue(std::vector{}); } +} // namespace + +// start() 依赖链:workspaces → projects → structure,逐级 emit 既有信号。 +TEST(WorkbenchNavController, StartChainEmitsWorkspacesThenProjectsThenStructure) { + StubAsyncRepo repo; + controller::WorkbenchNavController c(repo); + QSignalSpy wsSpy(&c, &controller::WorkbenchNavController::workspacesLoaded); + QSignalSpy psSpy(&c, &controller::WorkbenchNavController::projectsLoaded); + QSignalSpy stSpy(&c, &controller::WorkbenchNavController::structureLoaded); + c.start(); + repo.lastWorkspaces->fireDone(wsVar()); + EXPECT_EQ(wsSpy.count(), 1); + repo.lastProjects->fireDone(pageVar()); + EXPECT_EQ(psSpy.count(), 1); + repo.lastStructure->fireDone(nodesVar()); + EXPECT_EQ(stSpy.count(), 1); +} + +// busyChanged 反映在飞:发起→true,最后完成→false。 +TEST(WorkbenchNavController, BusyChangedReflectsInflight) { + StubAsyncRepo repo; + controller::WorkbenchNavController c(repo); + QSignalSpy busySpy(&c, &controller::WorkbenchNavController::busyChanged); + c.start(); + ASSERT_GE(busySpy.count(), 1); + EXPECT_TRUE(busySpy.takeFirst().at(0).toBool()); // 首次 true + repo.lastWorkspaces->fireDone(wsVar()); + repo.lastProjects->fireDone(pageVar()); + repo.lastStructure->fireDone(nodesVar()); + EXPECT_FALSE(busySpy.last().at(0).toBool()); // 末尾 false +} + +// 空项目链:projects 空 → structure 发空树 → busy 复位(不发结构请求)。 +TEST(WorkbenchNavController, StartWithNoProjectsEmitsEmptyStructureAndClearsBusy) { + StubAsyncRepo repo; + controller::WorkbenchNavController c(repo); + QSignalSpy stSpy(&c, &controller::WorkbenchNavController::structureLoaded); + QSignalSpy busySpy(&c, &controller::WorkbenchNavController::busyChanged); + c.start(); + repo.lastWorkspaces->fireDone(wsVar()); + repo.lastProjects->fireDone(emptyPageVar()); + EXPECT_EQ(stSpy.count(), 1); + EXPECT_FALSE(busySpy.last().at(0).toBool()); +} + +// setCheckedTms:新勾选 abort 旧异常批(以最后一次为准)。 +TEST(WorkbenchNavController, SetCheckedTmsAbortsPreviousBatch) { + StubAsyncRepo repo; + controller::WorkbenchNavController c(repo); + c.setCheckedTms({"tmA"}); + StubNavRequest* a = repo.exceptions.back(); + c.setCheckedTms({"tmB"}); // 覆盖 + EXPECT_TRUE(a->aborted); +} + +// setCheckedTms:全命中缓存 → 不发新请求、直接组装 emit。 +TEST(WorkbenchNavController, SetCheckedTmsUsesCacheWithoutRequest) { + StubAsyncRepo repo; + controller::WorkbenchNavController c(repo); + QSignalSpy exSpy(&c, &controller::WorkbenchNavController::exceptionTreeLoaded); + c.setCheckedTms({"tmA"}); // 首次未命中 → 发请求 + ASSERT_EQ(repo.exceptions.size(), 1u); + repo.exceptions.back()->fireDone(exVar()); // 写缓存 + emit + EXPECT_EQ(exSpy.count(), 1); + c.setCheckedTms({"tmA"}); // 第二次命中缓存 → 不发新请求 + EXPECT_EQ(repo.exceptions.size(), 1u); + EXPECT_EQ(exSpy.count(), 2); // 仍 emit +} + +// 回灌防护:abort 后旧句柄迟到 done 被身份比对丢弃。 +TEST(WorkbenchNavController, DropsLateStructureAfterProjectSwitch) { + StubAsyncRepo repo; + controller::WorkbenchNavController c(repo); + QSignalSpy stSpy(&c, &controller::WorkbenchNavController::structureLoaded); + c.switchProject("pA"); + StubNavRequest* a = repo.lastStructure; + c.switchProject("pB"); + StubNavRequest* b = repo.lastStructure; + EXPECT_TRUE(a->aborted); // 旧句柄被 abort + a->fireDone(nodesVar()); // 旧 → 丢弃 + EXPECT_EQ(stSpy.count(), 0); + b->fireDone(nodesVar()); // 新 → 正常 + EXPECT_EQ(stSpy.count(), 1); +} + +// selectObject 三并发:data/file/detail 各自完成 → 各发对应信号。 +TEST(WorkbenchNavController, SelectObjectConcurrentEmitsAllThree) { + StubAsyncRepo repo; + controller::WorkbenchNavController c(repo); + QSignalSpy dsSpy(&c, &controller::WorkbenchNavController::datasetsLoaded); + QSignalSpy flSpy(&c, &controller::WorkbenchNavController::filesLoaded); + QSignalSpy dtSpy(&c, &controller::WorkbenchNavController::objectDetailLoaded); + c.selectObject("obj1", 1); + repo.lastData->fireDone(dsPageVar()); + repo.lastFile->fireDone(dsPageVar()); + repo.lastDetail->fireDone(formVar()); + EXPECT_EQ(dsSpy.count(), 1); + EXPECT_EQ(flSpy.count(), 1); + EXPECT_EQ(dtSpy.count(), 1); +} + +// selectDataset 单请求 → datasetDetailLoaded。 +TEST(WorkbenchNavController, SelectDatasetEmitsDetail) { + StubAsyncRepo repo; + controller::WorkbenchNavController c(repo); + QSignalSpy spy(&c, &controller::WorkbenchNavController::datasetDetailLoaded); + c.selectDataset("ds1"); + repo.lastDataset->fireDone(formVar()); + EXPECT_EQ(spy.count(), 1); +} + +// selectObject 三并发部分失败:data 失败、file/detail 成功 → loadFailed×1,filesLoaded×1,objectDetailLoaded×1,datasetsLoaded×0。 +TEST(WorkbenchNavController, SelectObjectOneFailureEmitsPartialResults) { + StubAsyncRepo repo; + controller::WorkbenchNavController c(repo); + QSignalSpy dsSpy(&c, &controller::WorkbenchNavController::datasetsLoaded); + QSignalSpy flSpy(&c, &controller::WorkbenchNavController::filesLoaded); + QSignalSpy dtSpy(&c, &controller::WorkbenchNavController::objectDetailLoaded); + QSignalSpy failSpy(&c, &controller::WorkbenchNavController::loadFailed); + c.selectObject("obj2", 1); + // data 路失败,file/detail 路成功(三路独立,互不影响) + repo.lastData->fireFailed(); + repo.lastFile->fireDone(dsPageVar()); + repo.lastDetail->fireDone(formVar()); + EXPECT_EQ(dsSpy.count(), 0); // data 失败,无 datasetsLoaded + EXPECT_EQ(flSpy.count(), 1); // file 成功,有 filesLoaded + EXPECT_EQ(dtSpy.count(), 1); // detail 成功,有 objectDetailLoaded + EXPECT_EQ(failSpy.count(), 1); // 只有 data 路触发 loadFailed + EXPECT_EQ(failSpy.first().at(0).toString(), QStringLiteral("datasets")); +} + +namespace { +// 构造一棵树:6 个根(parentId="src" 不在集合内) + r1 两个子(c1a/c1b)。扁平 8 行。 +QVariant treePageVar() { + auto mk = [](const std::string& id, const std::string& parent) { + data::DsRow d; d.id = id; d.dsName = id; d.ddCode = "dd"; d.parentId = parent; return d; + }; + std::vector rows; + for (int i = 1; i <= 6; ++i) rows.push_back(mk("r" + std::to_string(i), "src")); + rows.push_back(mk("c1a", "r1")); + rows.push_back(mk("c1b", "r1")); + return QVariant::fromValue(data::DsPage{rows, static_cast(rows.size())}); +} +} // namespace + +// 数据页树形分页:按「第一层节点(根)」分页(每页 5 根),total=根总数,子树随根整棵带出; +// loadMoreData 同步续切下一页根。 +TEST(WorkbenchNavController, DataPaginatesByRootNodeNotFlatCount) { + qRegisterMetaType>(); + StubAsyncRepo repo; + controller::WorkbenchNavController c(repo); + QSignalSpy dsSpy(&c, &controller::WorkbenchNavController::datasetsLoaded); + c.selectObject("tm1", 2); + repo.lastData->fireDone(treePageVar()); // 一次取全 8 行 + + ASSERT_EQ(dsSpy.count(), 1); + const auto rows0 = qvariant_cast>(dsSpy.at(0).at(1)); + EXPECT_EQ(rows0.size(), 7u); // 首页 5 根(r1..r5) + r1 两子 = 7 行 + EXPECT_EQ(dsSpy.at(0).at(2).toInt(), 6); // total = 根总数 6(非扁平 8) + EXPECT_FALSE(dsSpy.at(0).at(3).toBool()); // append=false + + c.loadMoreData(); // 同步切下一页(无新请求) + ASSERT_EQ(dsSpy.count(), 2); + const auto rows1 = qvariant_cast>(dsSpy.at(1).at(1)); + EXPECT_EQ(rows1.size(), 1u); // 第二页第 6 个根 r6 + EXPECT_EQ(rows1[0].id, "r6"); + EXPECT_EQ(dsSpy.at(1).at(2).toInt(), 6); + EXPECT_TRUE(dsSpy.at(1).at(3).toBool()); // append=true + + c.loadMoreData(); // 已无更多根 → 不再 emit + EXPECT_EQ(dsSpy.count(), 2); +} + +// 失败路径:start 首级失败 → loadFailed + busy 复位。 +TEST(WorkbenchNavController, StartWorkspacesFailureEmitsLoadFailed) { + StubAsyncRepo repo; + controller::WorkbenchNavController c(repo); + QSignalSpy failSpy(&c, &controller::WorkbenchNavController::loadFailed); + QSignalSpy busySpy(&c, &controller::WorkbenchNavController::busyChanged); + c.start(); + repo.lastWorkspaces->fireFailed(); + EXPECT_EQ(failSpy.count(), 1); + EXPECT_FALSE(busySpy.last().at(0).toBool()); +} diff --git a/tests/core/test_model_data.cpp b/tests/core/test_model_data.cpp index 701ae4a..51e99d7 100644 --- a/tests/core/test_model_data.cpp +++ b/tests/core/test_model_data.cpp @@ -23,3 +23,12 @@ TEST(DataModel, AnomalyHolds) { EXPECT_EQ(a.markType, AnomalyMarkType::Polyline); EXPECT_EQ(a.localPts.size(), 2u); } + +#include +TEST(GridNaN, HasValueReflectsNaN) { + geopro::core::Grid g(2, 2); + g.valueAt(0, 0) = 3.0; + g.valueAt(1, 0) = std::nan(""); + EXPECT_TRUE(g.hasValue(0, 0)); + EXPECT_FALSE(g.hasValue(1, 0)); +} diff --git a/tests/data/test_async_repo_dispatch.cpp b/tests/data/test_async_repo_dispatch.cpp new file mode 100644 index 0000000..62d4d9a --- /dev/null +++ b/tests/data/test_async_repo_dispatch.cpp @@ -0,0 +1,103 @@ +#include +#include +#include +#include +#include "ApiClient.hpp" +#include "api/ApiDatasetRepository.hpp" +#include "api/DatasetLoadHandles.hpp" +#include "model/detail/DetailPayloads.hpp" + +using geopro::data::ApiDatasetRepository; +using geopro::data::DetailLoad; +using geopro::core::ContourPayload; +using geopro::core::ScatterPayload; + +namespace { +// ApiClient 构造 + getAsync/postJsonAsync 创建网络句柄需 QCoreApplication(QNAM 事件循环)。 +QCoreApplication* ensureApp() { + if (!QCoreApplication::instance()) { + static int argc = 0; + static char** argv = nullptr; + new QCoreApplication(argc, argv); + } + return QCoreApplication::instance(); +} +} // namespace + +// 已知 loaderKey 返回非空句柄;返回后立即 abort 取消在飞请求(不依赖网络可达)。 +TEST(AsyncRepoDispatch, KnownKeysReturnNonNullHandle) { + ensureApp(); + geopro::net::ApiClient api(QStringLiteral("http://tenant.geomative.cn/pop-api")); + ApiDatasetRepository repo(api); + + DetailLoad* scatter = repo.loadAsync("inversion.scatter", "ds1"); + ASSERT_NE(scatter, nullptr); + scatter->abort(); + + DetailLoad* grid = repo.loadAsync("inversion.grid", "ds1"); + ASSERT_NE(grid, nullptr); + grid->abort(); + + DetailLoad* measScatter = repo.loadAsync("ert_measurement.scatter", "ds1"); + ASSERT_NE(measScatter, nullptr); + measScatter->abort(); + + DetailLoad* measRows = repo.loadAsync("ert_measurement.rows", "ds1"); + ASSERT_NE(measRows, nullptr); + measRows->abort(); + + DetailLoad* grBar = repo.loadAsync("gr.bar", "ds1"); + ASSERT_NE(grBar, nullptr); + grBar->abort(); + + DetailLoad* grRows = repo.loadAsync("gr.rows", "ds1"); + ASSERT_NE(grRows, nullptr); + grRows->abort(); + + DetailLoad* trajRows = repo.loadAsync("traj.rows", "ds1"); + ASSERT_NE(trajRows, nullptr); + trajRows->abort(); + + DetailLoad* trajElev = repo.loadAsync("traj.elev", "ds1"); + ASSERT_NE(trajElev, nullptr); + trajElev->abort(); + + DetailLoad* trajMap = repo.loadAsync("traj.map", "ds1"); + ASSERT_NE(trajMap, nullptr); + trajMap->abort(); + + DetailLoad* gridRows = repo.loadAsync("grid.rows", "ds1", 1, 50); // 分页型:带页参 + ASSERT_NE(gridRows, nullptr); + gridRows->abort(); +} + +// 未知 loaderKey 抛 std::runtime_error。 +TEST(AsyncRepoDispatch, UnknownKeyThrows) { + ensureApp(); + geopro::net::ApiClient api(QStringLiteral("http://tenant.geomative.cn/pop-api")); + ApiDatasetRepository repo(api); + EXPECT_THROW(repo.loadAsync("bogus", "ds1"), std::runtime_error); +} + +// payload 经 QVariant 类型擦除往返还原(不触网)。 +TEST(AsyncRepoDispatch, ScatterPayloadRoundTrips) { + ScatterPayload in; + in.scatter.x = {1.0, 2.0, 3.0}; + in.scatter.v = {10.0, 20.0, 30.0}; + QVariant v = QVariant::fromValue(in); + ASSERT_TRUE(v.canConvert()); + ScatterPayload out = v.value(); + ASSERT_EQ(out.scatter.x.size(), 3u); + EXPECT_DOUBLE_EQ(out.scatter.v[2], 30.0); +} + +TEST(AsyncRepoDispatch, ContourPayloadRoundTrips) { + ContourPayload in; // Grid{1,1} 占位 + 空异常 + in.grid.vmin = 1.5; + in.grid.vmax = 9.5; + QVariant v = QVariant::fromValue(in); + ASSERT_TRUE(v.canConvert()); + ContourPayload out = v.value(); + EXPECT_DOUBLE_EQ(out.grid.vmin, 1.5); + EXPECT_DOUBLE_EQ(out.grid.vmax, 9.5); +} diff --git a/tests/data/test_dataset_chart_dto.cpp b/tests/data/test_dataset_chart_dto.cpp new file mode 100644 index 0000000..0c36ac1 --- /dev/null +++ b/tests/data/test_dataset_chart_dto.cpp @@ -0,0 +1,58 @@ +#include +#include +#include +#include +#include "dto/DatasetChartDto.hpp" +using namespace geopro::data::dto; + +static QJsonObject obj(const char* json) { + return QJsonDocument::fromJson(json).object(); +} + +TEST(DatasetChartDto, ParsesInversionGrid) { + auto d = obj(R"({"x":[0,1],"y":[0,1,2],"v":[[1,2],[3,4],[5,6]],"vmin":1,"vmax":6})"); + auto g = parseInversionGrid(d); + EXPECT_EQ(g.nx(), 2); EXPECT_EQ(g.ny(), 3); + EXPECT_DOUBLE_EQ(g.valueAt(1, 2), 6.0); + EXPECT_DOUBLE_EQ(g.vmax, 6.0); +} +TEST(DatasetChartDto, ParsesColorBar) { + // Real API colorBar format: MIXED hex (#RRGGBB) and CSS rgba() with alpha in 0–1 scale + // (verified live: lvl/colorGradation/getDetail returns e.g. ["1.51","rgba(0, 0, 170, 1)"]). + // Every stop must render OPAQUE (alpha=255); the rgba a=1 must map to 255, not 1. + const char* colorBarJson = + "{\"properties\":{\"colorBar\":[" + "[\"0.00\",\"#00008B\"]," + "[\"1.51\",\"rgba(0, 0, 170, 1)\"]," + "[\"60.77\",\"#FFFF00\"]," + "[\"138.20\",\"rgba(255, 128, 0, 1)\"]]}}"; + auto d = obj(colorBarJson); + auto cs = parseColorBar(d); + auto stops = cs.stops(); + ASSERT_EQ(stops.size(), 4u); + EXPECT_DOUBLE_EQ(stops[0].first, 0.0); + EXPECT_DOUBLE_EQ(stops[1].first, 1.51); + // hex stop → exact rgb, opaque + EXPECT_EQ(stops[0].second.r, 0); EXPECT_EQ(stops[0].second.g, 0); + EXPECT_EQ(stops[0].second.b, 0x8B); EXPECT_EQ(stops[0].second.a, 255); + // rgba(a=1) stop → exact rgb, OPAQUE (regression: was alpha=1 under Bit255 → near-transparent) + EXPECT_EQ(stops[1].second.r, 0); EXPECT_EQ(stops[1].second.g, 0); + EXPECT_EQ(stops[1].second.b, 170); EXPECT_EQ(stops[1].second.a, 255); + EXPECT_EQ(stops[3].second.r, 255); EXPECT_EQ(stops[3].second.g, 128); + EXPECT_EQ(stops[3].second.b, 0); EXPECT_EQ(stops[3].second.a, 255); + // every stop opaque + for (const auto& s : stops) EXPECT_EQ(s.second.a, 255) << "value=" << s.first; +} +TEST(DatasetChartDto, ParsesAnomalyPolyline) { + auto arr = QJsonDocument::fromJson( + R"([{"exceptionName":"A1","exceptionTypeName":"AnomalyZone","exceptionMarkType":2, + "legend":{"polylineColor":"#0D0101","polylineWidth":4,"polylineShape":"dash"}, + "location":{"coordinate":[{"x":1,"y":2},{"x":3,"y":4}]}}])").array(); + auto v = parseDatasetAnomalies(arr); + ASSERT_EQ(v.size(), 1u); + EXPECT_EQ(v[0].name, "A1"); + EXPECT_EQ(static_cast(v[0].markType), 2); + ASSERT_EQ(v[0].localPts.size(), 2u); + EXPECT_DOUBLE_EQ(v[0].localPts[1].x, 3.0); + EXPECT_TRUE(v[0].dashed); +} diff --git a/tests/data/test_dataset_load_handles.cpp b/tests/data/test_dataset_load_handles.cpp new file mode 100644 index 0000000..686df30 --- /dev/null +++ b/tests/data/test_dataset_load_handles.cpp @@ -0,0 +1,71 @@ +#include +#include +#include +#include +#include "api/DatasetLoadHandles.hpp" +#include "model/detail/DetailPayloads.hpp" +#include "net/FakeApiCall.hpp" + +using namespace geopro::data; +using geopro::net::ApiBatch; +using geopro::net::ApiResponse; +using geopro::net::test::FakeApiCall; +using geopro::core::ScatterPayload; + +namespace { +ApiResponse ok() { ApiResponse r; r.code = 200; r.httpStatus = 200; return r; } +ApiResponse bad() { ApiResponse r; r.code = 500; r.httpStatus = 200; r.msg = QStringLiteral("boom"); return r; } +auto isFailure = [](const ApiResponse& r) { return r.code != 200 || !r.rawError.isEmpty(); }; +} + +// ── 通用详情句柄 ApiDetailLoad(tab 引擎):done(QVariant) / failed / abort 闸门 ── + +TEST(DatasetLoadHandles, DetailLoadEmitsDoneWithPayloadOnSuccess) { + auto* a = new FakeApiCall; + auto* b = new FakeApiCall; + auto* batch = new ApiBatch({a, b}, isFailure); + auto* load = new ApiDetailLoad(batch, [](const QList& resps) { + ScatterPayload p; + p.scatter.v = std::vector(static_cast(resps.size()), 0.0); // 携带可校验状态 + return QVariant::fromValue(p); + }); + QSignalSpy doneSpy(load, &DetailLoad::done); + a->fire(ok()); + b->fire(ok()); + ASSERT_EQ(doneSpy.count(), 1); + QVariant payload = doneSpy.takeFirst().at(0).value(); + ASSERT_TRUE(payload.canConvert()); + EXPECT_EQ(payload.value().scatter.v.size(), 2u); // round-trip 还原字段 +} + +TEST(DatasetLoadHandles, DetailLoadEmitsFailedOnBatchFailure) { + auto* a = new FakeApiCall; + auto* batch = new ApiBatch({a}, isFailure); + auto* load = new ApiDetailLoad(batch, [](const QList&) { return QVariant{}; }); + QSignalSpy failSpy(load, &DetailLoad::failed); + a->fire(bad()); + EXPECT_EQ(failSpy.count(), 1); +} + +TEST(DatasetLoadHandles, DetailLoadEmitsFailedWhenParseThrows) { + auto* a = new FakeApiCall; + auto* batch = new ApiBatch({a}, isFailure); + auto* load = new ApiDetailLoad(batch, [](const QList&) -> QVariant { + throw std::runtime_error("parse boom"); + }); + QSignalSpy doneSpy(load, &DetailLoad::done); + QSignalSpy failSpy(load, &DetailLoad::failed); + a->fire(ok()); // batch 成功 → parse 抛 → failed(done 已移出 try) + EXPECT_EQ(doneSpy.count(), 0); + EXPECT_EQ(failSpy.count(), 1); +} + +TEST(DatasetLoadHandles, DetailLoadAbortSuppressesLateDone) { + auto* a = new FakeApiCall; + auto* batch = new ApiBatch({a}, isFailure); + auto* load = new ApiDetailLoad(batch, [](const QList&) { return QVariant{}; }); + QSignalSpy doneSpy(load, &DetailLoad::done); + load->abort(); + a->fire(ok()); // 迟到 + EXPECT_EQ(doneSpy.count(), 0); // 双闸门 +} diff --git a/tests/data/test_gr_dto.cpp b/tests/data/test_gr_dto.cpp new file mode 100644 index 0000000..190099f --- /dev/null +++ b/tests/data/test_gr_dto.cpp @@ -0,0 +1,123 @@ +#include + +#include +#include + +#include "dto/GrMeasurementDto.hpp" + +using namespace geopro::data::dto; +using geopro::core::TableColumnKind; + +namespace { + +// 取自真实夹具 tests/fixtures/dd/ert-gr-rows.json 的 data 数组(裁剪至 20 行,逐字一致)。 +// 与 test_measurement_dto.cpp 同法:内联夹具,避免引入 fixture 路径编译定义。 +const char* kGrRows = R"([ + { "electrodeId": 1, "testDate": "5/26/12", "testTime": "18:25:15", "p1Rg": 1385, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 2, "testDate": "5/26/12", "testTime": "18:25:16", "p1Rg": 3444, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 3, "testDate": "5/26/12", "testTime": "18:25:17", "p1Rg": 737, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 4, "testDate": "5/26/12", "testTime": "18:25:18", "p1Rg": 813, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 5, "testDate": "5/26/12", "testTime": "18:25:19", "p1Rg": 577, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 6, "testDate": "5/26/12", "testTime": "18:25:20", "p1Rg": 652, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 7, "testDate": "5/26/12", "testTime": "18:25:20", "p1Rg": 696, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 8, "testDate": "5/26/12", "testTime": "18:25:21", "p1Rg": 523, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 9, "testDate": "5/26/12", "testTime": "18:25:22", "p1Rg": 587, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 10, "testDate": "5/26/12", "testTime": "18:25:23", "p1Rg": 968, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 11, "testDate": "5/26/12", "testTime": "18:25:24", "p1Rg": 1701, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 12, "testDate": "5/26/12", "testTime": "18:25:25", "p1Rg": 1951, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 13, "testDate": "5/26/12", "testTime": "18:25:26", "p1Rg": 1405, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 14, "testDate": "5/26/12", "testTime": "18:25:27", "p1Rg": 1166, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 15, "testDate": "5/26/12", "testTime": "18:25:28", "p1Rg": 1222, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 16, "testDate": "5/26/12", "testTime": "18:25:29", "p1Rg": 1043, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 17, "testDate": "5/26/12", "testTime": "18:25:30", "p1Rg": 1272, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 18, "testDate": "5/26/12", "testTime": "18:25:30", "p1Rg": 2150, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 19, "testDate": "5/26/12", "testTime": "18:25:31", "p1Rg": 1806, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 20, "testDate": "5/26/12", "testTime": "18:25:32", "p1Rg": 971, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" } +])"; + +QJsonArray grRows() { return QJsonDocument::fromJson(kGrRows).array(); } + +} // namespace + +TEST(GrDto, ParsesBarSingleP1Series) { + const QJsonArray rows = grRows(); + ASSERT_EQ(rows.size(), 20); + + auto bar = parseGrBar(rows); + + // 类目 "#1".."#20"。 + ASSERT_EQ(bar.categories.size(), 20u); + EXPECT_EQ(bar.categories.front().toStdString(), "#1"); + EXPECT_EQ(bar.categories.back().toStdString(), "#20"); + + // 轴标题。 + EXPECT_EQ(bar.xTitle.toStdString(), std::string("电极点")); + EXPECT_EQ(bar.yTitle.toStdString(), std::string("电阻(单位:欧姆)")); + + // 单系列 P1(p2Rg 全 null → 无 P2),色 #5470c6。 + ASSERT_EQ(bar.series.size(), 1u); + const auto& s = bar.series.front(); + EXPECT_EQ(s.name.toStdString(), "P1"); + EXPECT_EQ(s.color.toStdString(), "#5470c6"); + ASSERT_EQ(s.values.size(), 20u); + EXPECT_DOUBLE_EQ(s.values.front(), 1385.0); // 首行 p1Rg + EXPECT_DOUBLE_EQ(s.values[1], 3444.0); + EXPECT_DOUBLE_EQ(s.values.back(), 971.0); // 第 20 行 p1Rg +} + +TEST(GrDto, AddsP2SeriesWhenAnyNonNull) { + // 合成:含一行 p2Rg 非空 → 应建 P2 系列(分组柱)。 + const char* json = R"([ + {"electrodeId":1,"p1Rg":100,"p1RgStatus":"正常","p2Rg":null,"p2RgStatus":""}, + {"electrodeId":2,"p1Rg":200,"p1RgStatus":"正常","p2Rg":55,"p2RgStatus":"正常"} + ])"; + const QJsonArray rows = QJsonDocument::fromJson(json).array(); + + auto bar = parseGrBar(rows); + ASSERT_EQ(bar.series.size(), 2u); + EXPECT_EQ(bar.series[0].name.toStdString(), "P1"); + EXPECT_EQ(bar.series[1].name.toStdString(), "P2"); + ASSERT_EQ(bar.series[1].values.size(), 2u); + EXPECT_DOUBLE_EQ(bar.series[1].values[1], 55.0); +} + +TEST(GrDto, ParsesEmptyArrayToValidEmptyPayloads) { + // 空/畸形响应:空 QJsonArray → 有效但空的载荷,不崩。 + const QJsonArray empty; + + auto bar = parseGrBar(empty); + EXPECT_EQ(bar.categories.size(), 0u); + EXPECT_EQ(bar.series.size(), 1u); // P1 恒有(系列存在但无值) + EXPECT_EQ(bar.series.front().values.size(), 0u); + EXPECT_EQ(bar.xTitle.toStdString(), std::string("电极点")); + + auto t = parseGrTable(empty); + EXPECT_EQ(t.columns.size(), 7u); // 7 列硬编码,恒有 + EXPECT_EQ(t.rows.size(), 0u); + EXPECT_EQ(t.total, 0); +} + +TEST(GrDto, ParsesTableSevenFixedColumns) { + const QJsonArray rows = grRows(); + auto t = parseGrTable(rows); + + // 7 固定列,无 Toggle 列(gr 不复用 measurement 的开关列)。 + ASSERT_EQ(t.columns.size(), 7u); + EXPECT_EQ(t.columns[0].title.toStdString(), "ID"); + EXPECT_EQ(t.columns[1].title.toStdString(), std::string("日期")); + EXPECT_EQ(t.columns[2].title.toStdString(), std::string("时间")); + EXPECT_EQ(t.columns[3].title.toStdString(), "P1 Rg(Ω)"); + EXPECT_EQ(t.columns[4].title.toStdString(), std::string("P1状态")); + EXPECT_EQ(t.columns[5].title.toStdString(), "P2 Rg(Ω)"); + EXPECT_EQ(t.columns[6].title.toStdString(), std::string("P2状态")); + for (const auto& c : t.columns) EXPECT_EQ(c.kind, TableColumnKind::Text); + + // 20 行;首行 ID=1, p1Rg=1385, P1状态=正常;p2Rg null → 空串。 + ASSERT_EQ(t.rows.size(), 20u); + ASSERT_EQ(t.rows[0].size(), 7u); + EXPECT_EQ(t.rows[0][0].toStdString(), "1"); + EXPECT_EQ(t.rows[0][3].toStdString(), "1385"); + EXPECT_EQ(t.rows[0][4].toStdString(), std::string("正常")); + EXPECT_TRUE(t.rows[0][5].isEmpty()); // p2Rg null + EXPECT_EQ(t.total, 20); +} diff --git a/tests/data/test_grid_dto.cpp b/tests/data/test_grid_dto.cpp new file mode 100644 index 0000000..68c0e61 --- /dev/null +++ b/tests/data/test_grid_dto.cpp @@ -0,0 +1,78 @@ +#include + +#include +#include + +#include "dto/GridDto.hpp" + +using namespace geopro::data::dto; +using geopro::core::TableColumnKind; + +namespace { + +// 取自真实夹具 tests/fixtures/dd/ert-grid-rows.json 的 data(rowList 5 行,gridHeaderDisplay +// 两列 x/y,total=62,逐字一致;端点 dd/ert/grid/rows)。内联避免引入 fixture 路径编译定义。 +const char* kGridData = R"({ + "gridHeaderDisplay": [ + { "columnCode": "x", "columnNameChn": "x", "columnNameEng": "x", "columnWidth": 10, "columnSort": 1 }, + { "columnCode": "y", "columnNameChn": "y", "columnNameEng": "y", "columnWidth": 10, "columnSort": 2 } + ], + "functionList": [], + "total": 62, + "rowList": [ + { "x": 2.904, "y": 25.126, "id": "1438944148742144" }, + { "x": 4.897, "y": 25.161, "id": "1438944148742145" }, + { "x": 6.892, "y": 25.247, "id": "1438944148742146" }, + { "x": 8.892, "y": 25.251, "id": "1438944148742147" }, + { "x": 10.891, "y": 25.226, "id": "1438944148742148" } + ] +})"; + +QJsonObject gridData() { return QJsonDocument::fromJson(kGridData).object(); } + +} // namespace + +TEST(GridDto, FirstPagePrependsSeqColumnAndReadsTotal) { + auto t = parseGridTable(gridData(), /*pageNo*/ 1, /*pageSize*/ 50); + + // 三列:序号 / x / y(序号前插,x/y 来自 gridHeaderDisplay 按 columnSort)。 + ASSERT_EQ(t.columns.size(), 3u); + EXPECT_EQ(t.columns[0].title.toStdString(), std::string("序号")); + EXPECT_EQ(t.columns[0].code.toStdString(), "__seq"); + EXPECT_EQ(t.columns[1].title.toStdString(), "x"); + EXPECT_EQ(t.columns[2].title.toStdString(), "y"); + for (const auto& c : t.columns) EXPECT_EQ(c.kind, TableColumnKind::Text); // 无特殊列 + + // 5 行;首行 序号=1 / x=2.904 / y=25.126。 + ASSERT_EQ(t.rows.size(), 5u); + ASSERT_EQ(t.rows[0].size(), 3u); + EXPECT_EQ(t.rows[0][0].toStdString(), "1"); + EXPECT_EQ(t.rows[0][1].toStdString(), "2.904"); + EXPECT_EQ(t.rows[0][2].toStdString(), "25.126"); + EXPECT_EQ(t.rows[4][0].toStdString(), "5"); // 第 5 行序号=5 + + // 总数取 data.total=62(非本批 5 行);分页态回填。 + EXPECT_EQ(t.total, 62); + EXPECT_EQ(t.pageNo, 1); + EXPECT_EQ(t.pageSize, 50); +} + +TEST(GridDto, SeqColumnOffsetsByPage) { + // 第 2 页、每页 50:本页首行全局序号 = (2-1)*50 + 1 = 51。 + auto t = parseGridTable(gridData(), /*pageNo*/ 2, /*pageSize*/ 50); + ASSERT_EQ(t.rows.size(), 5u); + EXPECT_EQ(t.rows[0][0].toStdString(), "51"); + EXPECT_EQ(t.rows[4][0].toStdString(), "55"); + EXPECT_EQ(t.pageNo, 2); + EXPECT_EQ(t.pageSize, 50); +} + +TEST(GridDto, EmptyDataYieldsSeqOnlyColumnNoRows) { + const QJsonObject empty; + auto t = parseGridTable(empty, 1, 50); + // 空数据:仅有前插的「序号」列,无行;total=0。 + ASSERT_EQ(t.columns.size(), 1u); + EXPECT_EQ(t.columns[0].code.toStdString(), "__seq"); + EXPECT_EQ(t.rows.size(), 0u); + EXPECT_EQ(t.total, 0); +} diff --git a/tests/data/test_measurement_dto.cpp b/tests/data/test_measurement_dto.cpp new file mode 100644 index 0000000..25f3b0d --- /dev/null +++ b/tests/data/test_measurement_dto.cpp @@ -0,0 +1,254 @@ +#include +#include +#include +#include +#include "dto/MeasurementDto.hpp" +using namespace geopro::data::dto; + +static QJsonObject obj(const char* json) { + return QJsonDocument::fromJson(json).object(); +} + +// scatter/graph 的 data:electrodeList + scatterGraphData.rows(17 列定位数组,取自真实夹具前 3 行)。 +static const char* kScatterData = R"({ + "electrodeList": [ + {"electrodeNo":1,"horizontalDistance":0,"slopeDistance":0,"pseudoDepth":0}, + {"electrodeNo":2,"horizontalDistance":1.08,"slopeDistance":1.2459494894830485,"pseudoDepth":0}, + {"electrodeNo":3,"horizontalDistance":3.15,"slopeDistance":3.3194843662393243,"pseudoDepth":0} + ], + "scatterGraphData": { + "hasCoordinate": true, + "rows": [ + [2.115013167852418,2.2827169278611863,null,-1.2,242.952988,2.385,4.116,0,"1453611521843200",24.288,1,4,2,3,12.5664,1,242.952988], + [5.124875148343908,5.300065674086864,null,-2.4,-1066.592407,1.873,13.377,0,"1453611521843201",23.12,1,7,3,5,25.1327,2,-1066.592407], + [7.994418521491841,8.175900836015376,null,-3.6,100.239388,2.001,9.976,0,"1453611521843202",21.987,1,10,4,7,37.6991,3,100.239388] + ], + "__rowsTotal": 576 + } +})"; + +// colorGradation/getDetail{type:3} 的 data:与反演同构混合格式 colorBar(hex + rgba alpha 0–1)。 +static const char* kColorBarData = R"json({ + "properties": { + "lvlSchemeType": "normal", + "colorBar": [ + ["0.00", "#00008B"], + ["1.51", "rgba(0, 0, 170, 1)"], + ["208.40", "#FF0000"], + ["1323.20", "rgba(48, 0, 48, 1)"] + ] + } +})json"; + +// measurement/rows 的 data:filedList(列定义)+ rowList(含 vmap 摊平),取自真实夹具前 2 行。 +static const char* kRowsData = R"({ + "filedList": [ + {"fieldCode":"a","name":"A"}, + {"fieldCode":"b","name":"B"}, + {"fieldCode":"k","name":"K"}, + {"fieldCode":"R","name":"电阻"}, + {"fieldCode":"R0","name":"视电阻率"} + ], + "rowList": [ + {"id":"1453611521843200","rowNo":1,"displayStatus":0,"a":1,"b":4,"k":12.5664, + "vmap":{"R":19.333542,"V":10223.44531,"I":528.793213,"R0_RD":0,"SP":127.640175,"R0":242.952988}}, + {"id":"1453611521843201","rowNo":2,"displayStatus":0,"a":1,"b":7,"k":25.1327, + "vmap":{"R":2.974368,"V":1822.526123,"I":612.743958,"R0_RD":0,"SP":92.380089,"R0":74.753899}} + ], + "__rowListTotal": 576 +})"; + +TEST(MeasurementDto, ParsesScatterPositionalRows) { + auto p = parseMeasurementScatter(obj(kScatterData), obj(kColorBarData)); + const auto& s = p.scatter; + + // 3 个散点;连续上色(cauto,与反演原数据同路径,已无 discreteColor 字段) + // + 图例与反演统一为底部横条。 + EXPECT_FALSE(p.verticalLegend); // measurement 图例改用底部横条(与反演一致) + ASSERT_EQ(s.x.size(), 3u); + ASSERT_EQ(s.y.size(), 3u); + ASSERT_EQ(s.v.size(), 3u); + + // x = col1 斜距;y = col3 伪深度(负);色值 = col16。 + EXPECT_DOUBLE_EQ(s.x[0], 2.2827169278611863); + EXPECT_DOUBLE_EQ(s.y[0], -1.2); + EXPECT_LT(s.y[0], 0.0); // 伪深度向下为负 + EXPECT_DOUBLE_EQ(s.v[0], 242.952988); + EXPECT_DOUBLE_EQ(s.x[2], 8.175900836015376); + EXPECT_DOUBLE_EQ(s.y[2], -3.6); + + // cauto 关键事实:负异常值(col16)原样保留——视图 setDataRange 据此得 cmin<0, + // 使中段视电阻率(≈25–250)归一化到色阶中部(深品红/紫),与原版 Plotly 一致。 + EXPECT_DOUBLE_EQ(s.v[1], -1066.592407); + EXPECT_LT(s.v[1], 0.0); // 负异常值未被过滤 + const double vMin = std::min({s.v[0], s.v[1], s.v[2]}); + EXPECT_DOUBLE_EQ(vMin, -1066.592407); // 数据范围下界为负 → cmin<0 + + // A/B/M/N = col10-13。 + ASSERT_EQ(s.a.size(), 3u); + EXPECT_DOUBLE_EQ(s.a[0], 1.0); + EXPECT_DOUBLE_EQ(s.b[0], 4.0); + EXPECT_DOUBLE_EQ(s.m[0], 2.0); + EXPECT_DOUBLE_EQ(s.n[0], 3.0); + + // 电极 X = electrodeList.slopeDistance。 + ASSERT_EQ(s.electrodeX.size(), 3u); + EXPECT_DOUBLE_EQ(s.electrodeX[0], 0.0); + EXPECT_DOUBLE_EQ(s.electrodeX[1], 1.2459494894830485); + + // 电极编号 = electrodeList.electrodeNo(1-based),与 electrodeX 一一对应(供 hover num)。 + ASSERT_EQ(s.electrodeNo.size(), 3u); + EXPECT_DOUBLE_EQ(s.electrodeNo[0], 1.0); + EXPECT_DOUBLE_EQ(s.electrodeNo[1], 2.0); + EXPECT_DOUBLE_EQ(s.electrodeNo[2], 3.0); +} + +// scatter/graph 含 scatterGraphConf 时:工具条下拉项 + 默认选中 + x/y 本地重绘备选列。 +static const char* kScatterDataWithConf = R"({ + "electrodeList": [ + {"electrodeNo":1,"horizontalDistance":0,"slopeDistance":0,"pseudoDepth":0} + ], + "scatterGraphConf": { + "x": [ + {"fieldCode":"horizontalDistance","name":"平距"}, + {"fieldCode":"slopeDistance","name":"斜距"} + ], + "y": [ + {"fieldCode":"Layer No","name":"层数"}, + {"fieldCode":"pseudoDepth","name":"伪深度"}, + {"fieldCode":"elevationPseudoDepth","name":"伪深度+高程"} + ], + "v": [ + {"fieldCode":"I","name":"电流"}, + {"fieldCode":"R0","name":"视电阻率"}, + {"fieldCode":"SP","name":"自然电位"} + ], + "vcalculationMethod": [ + {"fieldCode":null,"name":"线性"}, + {"fieldCode":null,"name":"对数"}, + {"fieldCode":null,"name":"导数"} + ] + }, + "scatterGraphData": { + "rows": [ + [2.115013167852418,2.2827169278611863,null,-1.2,242.952988,2.385,4.116,0,"id0",24.288,1,4,2,3,12.5664,1,242.952988] + ] + } +})"; + +TEST(MeasurementDto, ParsesScatterToolbarConfAndDefaults) { + auto p = parseMeasurementScatter(obj(kScatterDataWithConf), obj(kColorBarData)); + const auto& tb = p.toolbar; + + EXPECT_FALSE(tb.empty()); + ASSERT_EQ(tb.x.size(), 2u); + ASSERT_EQ(tb.y.size(), 3u); + ASSERT_EQ(tb.v.size(), 3u); + ASSERT_EQ(tb.method.size(), 3u); + + // 选项名/码(顺序保留)。 + EXPECT_EQ(tb.x[0].code.toStdString(), "horizontalDistance"); + EXPECT_EQ(tb.x[0].name.toStdString(), std::string("平距")); + EXPECT_EQ(tb.x[1].code.toStdString(), "slopeDistance"); + EXPECT_EQ(tb.method[0].name.toStdString(), std::string("线性")); + EXPECT_TRUE(tb.method[0].code.isEmpty()); // method 的 fieldCode 为 null + + // 默认选中:斜距 / 伪深度 / 视电阻率 / 线性。 + EXPECT_EQ(tb.defaultX.toStdString(), "slopeDistance"); + EXPECT_EQ(tb.defaultY.toStdString(), "pseudoDepth"); + EXPECT_EQ(tb.defaultV.toStdString(), "R0"); + EXPECT_EQ(tb.defaultMethod.toStdString(), std::string("线性")); +} + +TEST(MeasurementDto, CarriesAltColumnsForLocalReplot) { + auto p = parseMeasurementScatter(obj(kScatterDataWithConf), obj(kColorBarData)); + + // x 备选:平距(col0) / 斜距(col1);y 备选:伪深度(col3) / 伪深度+高程(col9)。 + ASSERT_EQ(p.altXHorizontal.size(), 1u); + ASSERT_EQ(p.altXSlope.size(), 1u); + ASSERT_EQ(p.altYPseudo.size(), 1u); + ASSERT_EQ(p.altYElevationPseudo.size(), 1u); + EXPECT_DOUBLE_EQ(p.altXHorizontal[0], 2.115013167852418); + EXPECT_DOUBLE_EQ(p.altXSlope[0], 2.2827169278611863); + EXPECT_DOUBLE_EQ(p.altYPseudo[0], -1.2); + EXPECT_DOUBLE_EQ(p.altYElevationPseudo[0], 24.288); +} + +TEST(MeasurementDto, ToolbarEmptyWhenNoConf) { + // 无 scatterGraphConf(反演原数据形状):工具条空 → 视图渲染反演工具条。 + auto p = parseMeasurementScatter(obj(kScatterData), obj(kColorBarData)); + EXPECT_TRUE(p.toolbar.empty()); +} + +TEST(MeasurementDto, ParsesScatterColorBarOpaque) { + auto p = parseMeasurementScatter(obj(kScatterData), obj(kColorBarData)); + auto stops = p.scale.stops(); + ASSERT_EQ(stops.size(), 4u); + // 首/末值。 + EXPECT_DOUBLE_EQ(stops.front().first, 0.0); + EXPECT_DOUBLE_EQ(stops.back().first, 1323.20); + // 首色 #00008B(深蓝),末色 rgba(48,0,48,1)(alpha=1 → 255,不透明)。 + EXPECT_EQ(stops.front().second.r, 0); + EXPECT_EQ(stops.front().second.g, 0); + EXPECT_EQ(stops.front().second.b, 0x8B); + EXPECT_EQ(stops.front().second.a, 255); + EXPECT_EQ(stops.back().second.r, 48); + EXPECT_EQ(stops.back().second.g, 0); + EXPECT_EQ(stops.back().second.b, 48); + EXPECT_EQ(stops.back().second.a, 255); // 回归:alpha=1 须映射为 255 不透明 +} + +TEST(MeasurementDto, ParsesTableColumnsAndVmapFlattened) { + auto t = parseMeasurementTable(obj(kRowsData)); + + // 列来自 filedList(5 列)+ 末尾追加“隐藏/显示”开关列 = 6 列。 + ASSERT_EQ(t.columns.size(), 6u); + EXPECT_EQ(t.columns[0].code.toStdString(), "a"); + EXPECT_EQ(t.columns[0].title.toStdString(), "A"); + EXPECT_EQ(t.columns[4].code.toStdString(), "R0"); + EXPECT_EQ(t.columns[4].title.toStdString(), std::string("视电阻率")); + + // 2 行;total 来自 __rowListTotal。 + ASSERT_EQ(t.rows.size(), 2u); + EXPECT_EQ(t.total, 576); + + // 顶层列(a/b/k)从行对象取;vmap 列(R/R0)从 vmap 摊平。 + ASSERT_EQ(t.rows[0].size(), 6u); + EXPECT_EQ(t.rows[0][0].toStdString(), "1"); // a(整数无小数点) + EXPECT_EQ(t.rows[0][1].toStdString(), "4"); // b + EXPECT_EQ(t.rows[0][3].toStdString(), "19.333542"); // R from vmap + EXPECT_EQ(t.rows[0][4].toStdString(), "242.952988"); // R0 from vmap + EXPECT_EQ(t.rows[1][4].toStdString(), "74.753899"); +} + +TEST(MeasurementDto, AppendsHideShowToggleColumn) { + auto t = parseMeasurementTable(obj(kRowsData)); + + // 末列为“隐藏/显示”,kind=Toggle,code=displayStatus。 + ASSERT_EQ(t.columns.size(), 6u); + const auto& toggle = t.columns.back(); + EXPECT_EQ(toggle.title.toStdString(), std::string("隐藏/显示")); + EXPECT_EQ(toggle.code.toStdString(), "displayStatus"); + EXPECT_EQ(toggle.kind, geopro::core::TableColumnKind::Toggle); + + // displayStatus==0 ⇒ ON/可见("1"),全部 ON。 + ASSERT_EQ(t.rows.size(), 2u); + EXPECT_EQ(t.rows[0].back().toStdString(), "1"); + EXPECT_EQ(t.rows[1].back().toStdString(), "1"); +} + +TEST(MeasurementDto, ToggleOffWhenDisplayStatusNonZero) { + // displayStatus!=0 ⇒ OFF("0")。 + auto t = parseMeasurementTable(obj(R"({ + "filedList": [{"fieldCode":"a","name":"A"}], + "rowList": [ + {"id":"x","a":1,"displayStatus":0}, + {"id":"y","a":2,"displayStatus":1} + ] + })")); + ASSERT_EQ(t.columns.size(), 2u); + EXPECT_EQ(t.columns.back().kind, geopro::core::TableColumnKind::Toggle); + ASSERT_EQ(t.rows.size(), 2u); + EXPECT_EQ(t.rows[0].back().toStdString(), "1"); // displayStatus 0 → ON + EXPECT_EQ(t.rows[1].back().toStdString(), "0"); // displayStatus 1 → OFF +} diff --git a/tests/data/test_nav_dto.cpp b/tests/data/test_nav_dto.cpp index eb4a42f..b42f9cf 100644 --- a/tests/data/test_nav_dto.cpp +++ b/tests/data/test_nav_dto.cpp @@ -176,6 +176,24 @@ TEST(NavDto, ParseDsRowsDataAndFile) { EXPECT_EQ(page.rows[0].dsName, "a"); } +// 数据集树父节点:sourceShowParentId 优先(=显示树父),缺失回退 parentId,皆无则空(=树根)。 +TEST(NavDto, ParseDsRowsParentIdForTree) { + const auto d = dto::parseDsRows(arrOf(R"([ + {"id":"raw","dsName":"E3原始","name":"ERT原始数据","ddCode":"dd_ert_measurement_data", + "parentId":"srcFile","sourceShowParentId":"srcFile"}, + {"id":"inv","dsName":"E3反演","name":"电阻率数据","ddCode":"dd_inversion_data", + "parentId":"raw","sourceShowParentId":"raw"}, + {"id":"fallback","dsName":"仅parentId","name":"t","ddCode":"dd", + "parentId":"raw"}, + {"id":"root","dsName":"无父","name":"t","ddCode":"dd"} + ])")); + ASSERT_EQ(d.size(), 4u); + EXPECT_EQ(d[0].parentId, "srcFile"); // 原始数据→源文件节点(不在本批→树根) + EXPECT_EQ(d[1].parentId, "raw"); // 派生反演→挂原始数据下 + EXPECT_EQ(d[2].parentId, "raw"); // 无 sourceShowParentId → 回退 parentId + EXPECT_TRUE(d[3].parentId.empty()); // 二者皆无 → 空(树根) +} + TEST(NavDto, ParseProjectItemFullFields) { const auto v = dto::parseProjectList(arrOf(R"([ {"id":"p1","projectName":"演示","projectCode":"001","status":2, diff --git a/tests/data/test_nav_request.cpp b/tests/data/test_nav_request.cpp new file mode 100644 index 0000000..1beb902 --- /dev/null +++ b/tests/data/test_nav_request.cpp @@ -0,0 +1,45 @@ +#include +#include +#include +#include "api/NavRequest.hpp" +#include "api/NavLoads.hpp" +#include "net/FakeApiCall.hpp" + +using namespace geopro::data; +using geopro::net::ApiResponse; +using geopro::net::test::FakeApiCall; + +namespace { +ApiResponse ok() { ApiResponse r; r.code = 200; r.httpStatus = 200; return r; } +ApiResponse bad() { ApiResponse r; r.code = 500; r.httpStatus = 200; r.msg = QStringLiteral("boom"); return r; } +auto isFailure = [](const ApiResponse& r) { return r.code != 200 || !r.rawError.isEmpty(); }; +} // namespace + +TEST(NavRequest, EmitsDoneWithParsedPayload) { + auto* call = new FakeApiCall; + auto* req = new ApiNavRequest(call, + [](const ApiResponse&) { return QVariant::fromValue(DsPage{{}, 42}); }, isFailure); + QSignalSpy doneSpy(req, &NavRequest::done); + call->fire(ok()); + ASSERT_EQ(doneSpy.count(), 1); + const auto page = qvariant_cast(doneSpy.takeFirst().at(0)); + EXPECT_EQ(page.total, 42); +} + +TEST(NavRequest, EmitsFailedOnBusinessError) { + auto* call = new FakeApiCall; + auto* req = new ApiNavRequest(call, [](const ApiResponse&) { return QVariant(); }, isFailure); + QSignalSpy failSpy(req, &NavRequest::failed); + call->fire(bad()); + EXPECT_EQ(failSpy.count(), 1); +} + +TEST(NavRequest, AbortSuppressesLateDone) { + auto* call = new FakeApiCall; + auto* req = new ApiNavRequest(call, [](const ApiResponse&) { return QVariant(); }, isFailure); + QSignalSpy doneSpy(req, &NavRequest::done); + req->abort(); + EXPECT_TRUE(call->aborted); + call->fire(ok()); + EXPECT_EQ(doneSpy.count(), 0); +} diff --git a/tests/data/test_trajectory_dto.cpp b/tests/data/test_trajectory_dto.cpp new file mode 100644 index 0000000..5ed260d --- /dev/null +++ b/tests/data/test_trajectory_dto.cpp @@ -0,0 +1,179 @@ +#include + +#include +#include + +#include "dto/TrajectoryDto.hpp" + +using namespace geopro::data::dto; +using geopro::core::TableColumnKind; + +namespace { + +// 取自真实夹具 tests/fixtures/dd/ert-trajectory-rows.json 的 data(rowList 裁剪至 20 行, +// gridHeaderDisplay 全量 14 列,逐字一致)。内联避免引入 fixture 路径编译定义。 +const char* kTrajData = R"({ + "gridHeaderDisplay": [ + { "columnCode": "electrodeNo", "columnNameChn": "电极号", "columnSort": 1, "columnWidth": 10 }, + { "columnCode": "horizontalDistance", "columnNameChn": "平距", "columnSort": 2, "columnWidth": 10 }, + { "columnCode": "slopeDistance", "columnNameChn": "斜距", "columnSort": 3, "columnWidth": 10 }, + { "columnCode": "projectX", "columnNameChn": "经度/东坐标", "columnSort": 4, "columnWidth": 10 }, + { "columnCode": "projectY", "columnNameChn": "纬度/北坐标", "columnSort": 5, "columnWidth": 10 }, + { "columnCode": "elevation", "columnNameChn": "高程", "columnSort": 6, "columnWidth": 10 }, + { "columnCode": "lineNo", "columnNameChn": "线号", "columnSort": 7, "columnWidth": 10 }, + { "columnCode": "channelNo", "columnNameChn": "道号", "columnSort": 8, "columnWidth": 10 }, + { "columnCode": "measuredDistance", "columnNameChn": "里程", "columnSort": 9, "columnWidth": 10 }, + { "columnCode": "depth", "columnNameChn": "深度", "columnSort": 10, "columnWidth": 10 }, + { "columnCode": "depthOfWater", "columnNameChn": "水深", "columnSort": 11, "columnWidth": 10 }, + { "columnCode": "date", "columnNameChn": "日期", "columnSort": 12, "columnWidth": 10 }, + { "columnCode": "time", "columnNameChn": "时间", "columnSort": 13, "columnWidth": 10 }, + { "columnCode": "resultStatus", "columnNameChn": "解状态", "columnSort": 14, "columnWidth": 10 } + ], + "__rowTotal": 40, + "rowList": [ + { "electrodeNo": 1, "horizontalDistance": 0, "slopeDistance": 0, "projectX": 516838.884, "projectY": 2494251.836, "elevation": 26.172, "lineNo": "ert1", "channelNo": "ERT1-1", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 2, "horizontalDistance": 1.08, "slopeDistance": 1.24, "projectX": 516840.099, "projectY": 2494252.112, "elevation": 25.551, "lineNo": "ert1", "channelNo": "ERT1-2", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 3, "horizontalDistance": 3.14, "slopeDistance": 3.31, "projectX": 516842.07, "projectY": 2494252.756, "elevation": 25.425, "lineNo": "ert1", "channelNo": "ERT1-3", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 4, "horizontalDistance": 4.94, "slopeDistance": 5.12, "projectX": 516843.874, "projectY": 2494252.792, "elevation": 25.625, "lineNo": "ert1", "channelNo": "ERT1-4", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 5, "horizontalDistance": 7.09, "slopeDistance": 7.28, "projectX": 516846.009, "projectY": 2494253.098, "elevation": 25.615, "lineNo": "ert1", "channelNo": "ERT1-5", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 6, "horizontalDistance": 8.91, "slopeDistance": 9.09, "projectX": 516847.815, "projectY": 2494253.249, "elevation": 25.627, "lineNo": "ert1", "channelNo": "ERT1-6", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 7, "horizontalDistance": 11.04, "slopeDistance": 11.22, "projectX": 516849.899, "projectY": 2494253.713, "elevation": 25.549, "lineNo": "ert1", "channelNo": "ERT1-7", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 8, "horizontalDistance": 13.09, "slopeDistance": 13.27, "projectX": 516851.911, "projectY": 2494254.1, "elevation": 25.49, "lineNo": "ert1", "channelNo": "ERT1-8", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 9, "horizontalDistance": 15.03, "slopeDistance": 15.26, "projectX": 516853.844, "projectY": 2494254.561, "elevation": 25.922, "lineNo": "ert1", "channelNo": "ERT1-9", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 10, "horizontalDistance": 16.95, "slopeDistance": 17.26, "projectX": 516855.736, "projectY": 2494255.216, "elevation": 25.363, "lineNo": "ert1", "channelNo": "ERT1-10", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 11, "horizontalDistance": 18.92, "slopeDistance": 19.24, "projectX": 516857.548, "projectY": 2494255.999, "elevation": 25.354, "lineNo": "ert1", "channelNo": "ERT1-11", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 12, "horizontalDistance": 20.97, "slopeDistance": 21.28, "projectX": 516859.35, "projectY": 2494256.96, "elevation": 25.39, "lineNo": "ert1", "channelNo": "ERT1-12", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 13, "horizontalDistance": 22.92, "slopeDistance": 23.24, "projectX": 516861.033, "projectY": 2494257.96, "elevation": 25.444, "lineNo": "ert1", "channelNo": "ERT1-13", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 14, "horizontalDistance": 24.81, "slopeDistance": 25.12, "projectX": 516862.667, "projectY": 2494258.906, "elevation": 25.48, "lineNo": "ert1", "channelNo": "ERT1-14", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 15, "horizontalDistance": 26.92, "slopeDistance": 27.24, "projectX": 516864.416, "projectY": 2494260.092, "elevation": 25.384, "lineNo": "ert1", "channelNo": "ERT1-15", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 16, "horizontalDistance": 28.68, "slopeDistance": 29.00, "projectX": 516865.639, "projectY": 2494261.366, "elevation": 25.5, "lineNo": "ert1", "channelNo": "ERT1-16", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 17, "horizontalDistance": 30.88, "slopeDistance": 31.19, "projectX": 516867.233, "projectY": 2494262.869, "elevation": 25.54, "lineNo": "ert1", "channelNo": "ERT1-17", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 18, "horizontalDistance": 32.76, "slopeDistance": 33.08, "projectX": 516868.457, "projectY": 2494264.304, "elevation": 25.505, "lineNo": "ert1", "channelNo": "ERT1-18", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 19, "horizontalDistance": 34.71, "slopeDistance": 35.03, "projectX": 516869.61, "projectY": 2494265.873, "elevation": 25.576, "lineNo": "ert1", "channelNo": "ERT1-19", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "electrodeNo": 20, "horizontalDistance": 36.64, "slopeDistance": 36.96, "projectX": 516870.684, "projectY": 2494267.48, "elevation": 25.567, "lineNo": "ert1", "channelNo": "ERT1-20", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" } + ] +})"; + +QJsonObject trajData() { return QJsonDocument::fromJson(kTrajData).object(); } + +// 取自真实夹具 tests/fixtures/dd/ert-trajectory-line.json 的 data(electrodelList 20 个电极, +// 逐字一致;服务端直接返回 EPSG:4326 经纬 x=lon/y=lat)。内联避免引入 fixture 路径编译定义。 +const char* kTrajLineData = R"({ + "id": null, + "electrodelList": [ + { "electrodeNo": 1, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16370297649078, "y": 22.54583186479361, "z": 0 }] } }, + { "electrodeNo": 2, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16371479128762, "y": 22.54583434513919, "z": 0 }] } }, + { "electrodeNo": 3, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16373395960512, "y": 22.545840141157978, "z": 0 }] } }, + { "electrodeNo": 4, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16375149792411, "y": 22.545840448398355, "z": 0 }] } }, + { "electrodeNo": 5, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16377225700053, "y": 22.545843190544776, "z": 0 }] } }, + { "electrodeNo": 6, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16378981598876, "y": 22.54584453624687, "z": 0 }] } }, + { "electrodeNo": 7, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16381008094395, "y": 22.54584870568223, "z": 0 }] } }, + { "electrodeNo": 8, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16382964511763, "y": 22.545852180493952, "z": 0 }] } }, + { "electrodeNo": 9, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16384844206722, "y": 22.54585632432848, "z": 0 }] } }, + { "electrodeNo": 10, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16386684249656, "y": 22.545862220446992, "z": 0 }] } }, + { "electrodeNo": 11, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16388446655657, "y": 22.545869273235397, "z": 0 }] } }, + { "electrodeNo": 12, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16390199529941, "y": 22.545877933516117, "z": 0 }] } }, + { "electrodeNo": 13, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16391836757842, "y": 22.545886947155804, "z": 0 }] } }, + { "electrodeNo": 14, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16393426292046, "y": 22.545895473642467, "z": 0 }] } }, + { "electrodeNo": 15, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16395127881961, "y": 22.545906166262615, "z": 0 }] } }, + { "electrodeNo": 16, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16396318204642, "y": 22.545917658760136, "z": 0 }] } }, + { "electrodeNo": 17, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16397869446884, "y": 22.545931215521435, "z": 0 }] } }, + { "electrodeNo": 18, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16399060913946, "y": 22.545944161886897, "z": 0 }] } }, + { "electrodeNo": 19, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16400183500123, "y": 22.54595831901666, "z": 0 }] } }, + { "electrodeNo": 20, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16401229325622, "y": 22.545972820080397, "z": 0 }] } } + ] +})"; + +QJsonObject trajLineData() { return QJsonDocument::fromJson(kTrajLineData).object(); } + +} // namespace + +TEST(TrajectoryDto, ParsesElevationLineSeries) { + const QJsonObject data = trajData(); + auto line = parseTrajectoryElevation(data); + + // 类目 "#1".."#20"(来自 electrodeNo)。 + ASSERT_EQ(line.categories.size(), 20u); + EXPECT_EQ(line.categories.front().toStdString(), "#1"); + EXPECT_EQ(line.categories.back().toStdString(), "#20"); + + // y = elevation,首值 26.172。 + ASSERT_EQ(line.y.size(), 20u); + EXPECT_DOUBLE_EQ(line.y.front(), 26.172); + EXPECT_DOUBLE_EQ(line.y[1], 25.551); + + // 轴标题 / 系列 / 色 / 平滑。 + EXPECT_EQ(line.xTitle.toStdString(), std::string("电极号")); + EXPECT_EQ(line.yTitle.toStdString(), std::string("高程")); + EXPECT_EQ(line.seriesName.toStdString(), std::string("高程")); + EXPECT_EQ(line.color.toStdString(), "#5470c6"); + EXPECT_TRUE(line.smooth); +} + +TEST(TrajectoryDto, ParsesTableFourteenColumnsOrderedBySort) { + const QJsonObject data = trajData(); + auto t = parseTrajectoryTable(data); + + // 14 列,按 columnSort 升序,title=columnNameChn,首列 电极号。 + ASSERT_EQ(t.columns.size(), 14u); + EXPECT_EQ(t.columns[0].title.toStdString(), std::string("电极号")); + EXPECT_EQ(t.columns[0].code.toStdString(), "electrodeNo"); + EXPECT_EQ(t.columns[1].title.toStdString(), std::string("平距")); + EXPECT_EQ(t.columns[2].title.toStdString(), std::string("斜距")); + EXPECT_EQ(t.columns[5].title.toStdString(), std::string("高程")); + EXPECT_EQ(t.columns[13].title.toStdString(), std::string("解状态")); + // 无 Toggle 列(trajectory 走 gridHeaderDisplay,不追加开关列)。 + for (const auto& c : t.columns) EXPECT_EQ(c.kind, TableColumnKind::Text); + + // 20 行;row0:电极号=1 / projectX=516838.884 / elevation=26.172 / resultStatus=固定解。 + ASSERT_EQ(t.rows.size(), 20u); + ASSERT_EQ(t.rows[0].size(), 14u); + EXPECT_EQ(t.rows[0][0].toStdString(), "1"); // electrodeNo + EXPECT_EQ(t.rows[0][3].toStdString(), "516838.884"); // projectX + EXPECT_EQ(t.rows[0][5].toStdString(), "26.172"); // elevation + EXPECT_EQ(t.rows[0][13].toStdString(), std::string("固定解")); // resultStatus + + // null 单元 → 空串(里程/深度/水深/日期/时间)。 + EXPECT_TRUE(t.rows[0][8].isEmpty()); // measuredDistance null + EXPECT_TRUE(t.rows[0][9].isEmpty()); // depth null + EXPECT_TRUE(t.rows[0][11].isEmpty()); // date null + + // total 取 __rowTotal=40(非本批 20)。 + EXPECT_EQ(t.total, 40); +} + +TEST(TrajectoryDto, ParsesEmptyToValidEmptyPayloads) { + const QJsonObject empty; + + auto line = parseTrajectoryElevation(empty); + EXPECT_EQ(line.categories.size(), 0u); + EXPECT_EQ(line.y.size(), 0u); + EXPECT_EQ(line.xTitle.toStdString(), std::string("电极号")); + EXPECT_TRUE(line.smooth); + + auto t = parseTrajectoryTable(empty); + EXPECT_EQ(t.columns.size(), 0u); + EXPECT_EQ(t.rows.size(), 0u); + EXPECT_EQ(t.total, 0); + + auto m = parseTrajectoryMap(empty); + EXPECT_EQ(m.points.size(), 0u); +} + +TEST(TrajectoryDto, ParsesMapPointsFromLineEndpoint) { + const QJsonObject data = trajLineData(); + auto m = parseTrajectoryMap(data); + + // 20 个电极点(electrodelList)。 + ASSERT_EQ(m.points.size(), 20u); + + // point0:electrodeNo=1,lon≈114.16370,lat≈22.54583(x=lon、y=lat,EPSG:4326 直返)。 + EXPECT_EQ(m.points[0].electrodeNo, 1); + EXPECT_NEAR(m.points[0].lon, 114.16370297649078, 1e-9); + EXPECT_NEAR(m.points[0].lat, 22.54583186479361, 1e-9); + + // point19:electrodeNo=20。 + EXPECT_EQ(m.points[19].electrodeNo, 20); + EXPECT_NEAR(m.points[19].lon, 114.16401229325622, 1e-9); + EXPECT_NEAR(m.points[19].lat, 22.545972820080397, 1e-9); +} diff --git a/tests/fixtures/dd/ert-gr-rows.json b/tests/fixtures/dd/ert-gr-rows.json new file mode 100644 index 0000000..cff1c41 --- /dev/null +++ b/tests/fixtures/dd/ert-gr-rows.json @@ -0,0 +1,27 @@ +{ + "code": 200, + "msg": "成功", + "__total": 40, + "data": [ + { "electrodeId": 1, "testDate": "5/26/12", "testTime": "18:25:15", "p1Rg": 1385, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 2, "testDate": "5/26/12", "testTime": "18:25:16", "p1Rg": 3444, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 3, "testDate": "5/26/12", "testTime": "18:25:17", "p1Rg": 737, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 4, "testDate": "5/26/12", "testTime": "18:25:18", "p1Rg": 813, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 5, "testDate": "5/26/12", "testTime": "18:25:19", "p1Rg": 577, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 6, "testDate": "5/26/12", "testTime": "18:25:20", "p1Rg": 652, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 7, "testDate": "5/26/12", "testTime": "18:25:20", "p1Rg": 696, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 8, "testDate": "5/26/12", "testTime": "18:25:21", "p1Rg": 523, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 9, "testDate": "5/26/12", "testTime": "18:25:22", "p1Rg": 587, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 10, "testDate": "5/26/12", "testTime": "18:25:23", "p1Rg": 968, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 11, "testDate": "5/26/12", "testTime": "18:25:24", "p1Rg": 1701, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 12, "testDate": "5/26/12", "testTime": "18:25:25", "p1Rg": 1951, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 13, "testDate": "5/26/12", "testTime": "18:25:26", "p1Rg": 1405, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 14, "testDate": "5/26/12", "testTime": "18:25:27", "p1Rg": 1166, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 15, "testDate": "5/26/12", "testTime": "18:25:28", "p1Rg": 1222, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 16, "testDate": "5/26/12", "testTime": "18:25:29", "p1Rg": 1043, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 17, "testDate": "5/26/12", "testTime": "18:25:30", "p1Rg": 1272, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 18, "testDate": "5/26/12", "testTime": "18:25:30", "p1Rg": 2150, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 19, "testDate": "5/26/12", "testTime": "18:25:31", "p1Rg": 1806, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }, + { "electrodeId": 20, "testDate": "5/26/12", "testTime": "18:25:32", "p1Rg": 971, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" } + ] +} diff --git a/tests/fixtures/dd/ert-grid-rows.json b/tests/fixtures/dd/ert-grid-rows.json new file mode 100644 index 0000000..8cc07ad --- /dev/null +++ b/tests/fixtures/dd/ert-grid-rows.json @@ -0,0 +1,19 @@ +{ + "code": 200, + "msg": "成功", + "data": { + "gridHeaderDisplay": [ + { "columnCode": "x", "columnNameChn": "x", "columnNameEng": "x", "columnWidth": 10, "columnSort": 1 }, + { "columnCode": "y", "columnNameChn": "y", "columnNameEng": "y", "columnWidth": 10, "columnSort": 2 } + ], + "functionList": [], + "total": 62, + "rowList": [ + { "x": 2.904, "y": 25.126, "id": "1438944148742144" }, + { "x": 4.897, "y": 25.161, "id": "1438944148742145" }, + { "x": 6.892, "y": 25.247, "id": "1438944148742146" }, + { "x": 8.892, "y": 25.251, "id": "1438944148742147" }, + { "x": 10.891, "y": 25.226, "id": "1438944148742148" } + ] + } +} diff --git a/tests/fixtures/dd/ert-measurement-colorbar.json b/tests/fixtures/dd/ert-measurement-colorbar.json new file mode 100644 index 0000000..7b1b490 --- /dev/null +++ b/tests/fixtures/dd/ert-measurement-colorbar.json @@ -0,0 +1,39 @@ +{ + "code": 200, + "msg": "成功", + "data": { + "id": null, + "projectId": "1438889436225536", + "templateId": "1439864962138112", + "dsObjectId": null, + "tmObjectId": null, + "graphicArea": 0, + "properties": { + "lvlSchemeType": "normal", + "colorBar": [ + ["0.00", "#00008B"], + ["1.51", "rgba(0, 0, 170, 1)"], + ["2.27", "rgba(0, 0, 211, 1)"], + ["3.43", "#0000FF"], + ["5.17", "rgba(0, 128, 255, 1)"], + ["7.79", "#00FFFF"], + ["11.75", "rgba(0, 192, 128, 1)"], + ["17.72", "#00FF00"], + ["26.73", "rgba(0, 128, 0, 1)"], + ["40.30", "rgba(128, 192, 0, 1)"], + ["60.77", "#FFFF00"], + ["91.64", "rgba(191, 128, 0, 1)"], + ["138.20", "rgba(255, 128, 0, 1)"], + ["208.40", "#FF0000"], + ["314.20", "rgba(211, 0, 0, 1)"], + ["473.80", "rgba(132, 0, 64, 1)"], + ["714.50", "rgba(96, 0, 96, 1)"], + ["1323.20", "rgba(48, 0, 48, 1)"] + ], + "equalAreaLayerCount": 10, + "labelConfig": { "showLabels": true, "color": "#000000" }, + "logLinesCount": 8, + "lineConfig": { "showLines": true, "color": "#000000", "lineType": "dashed" } + } + } +} diff --git a/tests/fixtures/dd/ert-measurement-rows.json b/tests/fixtures/dd/ert-measurement-rows.json new file mode 100644 index 0000000..98eb0d9 --- /dev/null +++ b/tests/fixtures/dd/ert-measurement-rows.json @@ -0,0 +1,575 @@ +{ + "code": 200, + "msg": "成功", + "data": { + "filedList": [ + { + "fieldCode": "a", + "name": "A", + "sheetHeaderNameList": [ + "A" + ] + }, + { + "fieldCode": "b", + "name": "B", + "sheetHeaderNameList": [ + "B" + ] + }, + { + "fieldCode": "m", + "name": "M", + "sheetHeaderNameList": [ + "M" + ] + }, + { + "fieldCode": "n", + "name": "N", + "sheetHeaderNameList": [ + "N" + ] + }, + { + "fieldCode": "k", + "name": "K", + "sheetHeaderNameList": [ + "K" + ] + }, + { + "fieldCode": "stacking", + "name": "Stacking", + "sheetHeaderNameList": [ + "Stacking" + ] + }, + { + "fieldCode": "I", + "name": "电流", + "sheetHeaderNameList": [ + "I(mA)" + ] + }, + { + "fieldCode": "V", + "name": "电压", + "sheetHeaderNameList": [ + "V(mV)" + ] + }, + { + "fieldCode": "R", + "name": "电阻", + "sheetHeaderNameList": [ + "R(Ohm)" + ] + }, + { + "fieldCode": "R0", + "name": "视电阻率", + "sheetHeaderNameList": [ + "R0" + ] + }, + { + "fieldCode": "SP", + "name": "自然电位", + "sheetHeaderNameList": [ + "SP" + ] + }, + { + "fieldCode": "R0_RD", + "name": "电阻误差", + "sheetHeaderNameList": [ + "R0_RD" + ] + } + ], + "rowList": [ + { + "id": "1453611521843200", + "rowNo": 1, + "displayStatus": 0, + "horizontalDistance": 2.115013167852418, + "slopeDistance": 2.2827169278611863, + "layerNo": null, + "pseudoDepth": -1.2, + "elevationPseudoDepth": 24.288, + "n": 3, + "a": 1, + "b": 4, + "m": 2, + "k": 12.5664, + "vmap": { + "R": 19.333542, + "V": 10223.44531, + "I": 528.793213, + "R0_RD": 0, + "SP": 127.640175, + "R0": 242.952988 + }, + "stacking": 1 + }, + { + "id": "1453611521843201", + "rowNo": 2, + "displayStatus": 0, + "horizontalDistance": 5.124875148343908, + "slopeDistance": 5.300065674086864, + "layerNo": null, + "pseudoDepth": -2.4, + "elevationPseudoDepth": 23.12, + "n": 5, + "a": 1, + "b": 7, + "m": 3, + "k": 25.1327, + "vmap": { + "R": 2.974368, + "V": 1822.526123, + "I": 612.743958, + "R0_RD": 0, + "SP": 92.380089, + "R0": 74.753899 + }, + "stacking": 1 + }, + { + "id": "1453611521843202", + "rowNo": 3, + "displayStatus": 0, + "horizontalDistance": 7.994418521491841, + "slopeDistance": 8.175900836015376, + "layerNo": null, + "pseudoDepth": -3.6, + "elevationPseudoDepth": 21.987, + "n": 7, + "a": 1, + "b": 10, + "m": 4, + "k": 37.6991, + "vmap": { + "R": 2.658933, + "V": 886.042603, + "I": 333.232361, + "R0_RD": 0, + "SP": -186.630417, + "R0": 100.239388 + }, + "stacking": 1 + }, + { + "id": "1453611521843203", + "rowNo": 4, + "displayStatus": 0, + "horizontalDistance": 8.00601333013422, + "slopeDistance": 8.186794593291953, + "layerNo": null, + "pseudoDepth": -3.6, + "elevationPseudoDepth": 22.020999999999997, + "n": 6, + "a": 1, + "b": 10, + "m": 5, + "k": 125.663696, + "vmap": { + "R": 0.561675, + "V": 187.168427, + "I": 333.232361, + "R0_RD": 0, + "SP": -129.25145, + "R0": 70.582214 + }, + "stacking": 1 + }, + { + "id": "1453611521843204", + "rowNo": 5, + "displayStatus": 0, + "horizontalDistance": 11.066663821597649, + "slopeDistance": 11.272344909478555, + "layerNo": null, + "pseudoDepth": -4.8, + "elevationPseudoDepth": 20.9685, + "n": 9, + "a": 1, + "b": 13, + "m": 5, + "k": 50.265499, + "vmap": { + "R": 1.496101, + "V": 528.129211, + "I": 353.003784, + "R0_RD": 0, + "SP": -22.242311, + "R0": 75.202255 + }, + "stacking": 1 + }, + { + "id": "1453611521843205", + "rowNo": 6, + "displayStatus": 0, + "horizontalDistance": 11.00295159513125, + "slopeDistance": 11.184890199081117, + "layerNo": null, + "pseudoDepth": -4.8, + "elevationPseudoDepth": 20.758499999999998, + "n": 8, + "a": 1, + "b": 13, + "m": 6, + "k": 109.955704, + "vmap": { + "R": 0.941678, + "V": 332.415863, + "I": 353.003784, + "R0_RD": 0, + "SP": 288.221405, + "R0": 103.542862 + }, + "stacking": 1 + }, + { + "id": "1453611521859584", + "rowNo": 7, + "displayStatus": 0, + "horizontalDistance": 13.92101964767117, + "slopeDistance": 14.166540305149894, + "layerNo": null, + "pseudoDepth": -6, + "elevationPseudoDepth": 19.490499999999997, + "n": 11, + "a": 1, + "b": 16, + "m": 6, + "k": 62.831902, + "vmap": { + "R": 1.357018, + "V": 693.766174, + "I": 511.243073, + "R0_RD": 0, + "SP": 289.488678, + "R0": 85.264038 + }, + "stacking": 1 + }, + { + "id": "1453611521859585", + "rowNo": 8, + "displayStatus": 0, + "horizontalDistance": 14.000862630669717, + "slopeDistance": 14.247085672002468, + "layerNo": null, + "pseudoDepth": -6, + "elevationPseudoDepth": 19.456, + "n": 10, + "a": 1, + "b": 16, + "m": 7, + "k": 113.097298, + "vmap": { + "R": 0.557539, + "V": 285.037994, + "I": 511.243073, + "R0_RD": 0, + "SP": -35.634811, + "R0": 63.056168 + }, + "stacking": 1 + }, + { + "id": "1453611521859586", + "rowNo": 9, + "displayStatus": 0, + "horizontalDistance": 14.063602086594681, + "slopeDistance": 14.27044051526772, + "layerNo": null, + "pseudoDepth": -6, + "elevationPseudoDepth": 19.706, + "n": 9, + "a": 1, + "b": 16, + "m": 8, + "k": 351.858398, + "vmap": { + "R": 0.125825, + "V": 64.3274, + "I": 511.243073, + "R0_RD": 0, + "SP": -187.302017, + "R0": 44.272751 + }, + "stacking": 1 + }, + { + "id": "1453611521859587", + "rowNo": 10, + "displayStatus": 0, + "horizontalDistance": 16.987234838304097, + "slopeDistance": 17.233999253766825, + "layerNo": null, + "pseudoDepth": -7.2, + "elevationPseudoDepth": 18.296499999999998, + "n": 13, + "a": 1, + "b": 19, + "m": 7, + "k": 75.398201, + "vmap": { + "R": 0.889478, + "V": 318.904541, + "I": 358.53006, + "R0_RD": 0, + "SP": -63.969704, + "R0": 67.065033 + }, + "stacking": 1 + }, + { + "id": "1453611521859588", + "rowNo": 11, + "displayStatus": 0, + "horizontalDistance": 17.03278556632032, + "slopeDistance": 17.279602362635487, + "layerNo": null, + "pseudoDepth": -7.2, + "elevationPseudoDepth": 18.24, + "n": 12, + "a": 1, + "b": 19, + "m": 8, + "k": 120.951302, + "vmap": { + "R": 0.585498, + "V": 209.918564, + "I": 358.53006, + "R0_RD": 0, + "SP": -268.122803, + "R0": 70.816719 + }, + "stacking": 1 + }, + { + "id": "1453611521859589", + "rowNo": 12, + "displayStatus": 0, + "horizontalDistance": 16.9816701391346, + "slopeDistance": 17.2520906213365, + "layerNo": null, + "pseudoDepth": -7.2, + "elevationPseudoDepth": 18.438, + "n": 11, + "a": 1, + "b": 19, + "m": 9, + "k": 251.327393, + "vmap": { + "R": 0.166266, + "V": 59.611279, + "I": 358.53006, + "R0_RD": 0, + "SP": 187.193558, + "R0": 41.787144 + }, + "stacking": 1 + }, + { + "id": "1453611521859590", + "rowNo": 13, + "displayStatus": 0, + "horizontalDistance": 20.01061875986487, + "slopeDistance": 20.259070486101244, + "layerNo": null, + "pseudoDepth": -8.4, + "elevationPseudoDepth": 17.037, + "n": 15, + "a": 1, + "b": 22, + "m": 8, + "k": 87.9646, + "vmap": { + "R": 0.778783, + "V": 222.046936, + "I": 285.120575, + "R0_RD": 0, + "SP": -188.105652, + "R0": 68.505302 + }, + "stacking": 1 + }, + { + "id": "1453611521859591", + "rowNo": 14, + "displayStatus": 0, + "horizontalDistance": 19.924955341057455, + "slopeDistance": 20.196078557097973, + "layerNo": null, + "pseudoDepth": -8.4, + "elevationPseudoDepth": 17.301000000000002, + "n": 14, + "a": 1, + "b": 22, + "m": 9, + "k": 130.690308, + "vmap": { + "R": 0.586209, + "V": 167.140335, + "I": 285.120575, + "R0_RD": 0, + "SP": -30.588928, + "R0": 76.61187 + }, + "stacking": 1 + }, + { + "id": "1453611521859592", + "rowNo": 15, + "displayStatus": 0, + "horizontalDistance": 19.94235941950773, + "slopeDistance": 20.25312046337076, + "layerNo": null, + "pseudoDepth": -8.4, + "elevationPseudoDepth": 17.003500000000003, + "n": 13, + "a": 1, + "b": 22, + "m": 10, + "k": 226.194595, + "vmap": { + "R": 0.258264, + "V": 73.636406, + "I": 285.120575, + "R0_RD": 0, + "SP": -38.283382, + "R0": 58.417942 + }, + "stacking": 1 + }, + { + "id": "1453611521859593", + "rowNo": 16, + "displayStatus": 0, + "horizontalDistance": 19.95085361886024, + "slopeDistance": 20.261252468704264, + "layerNo": null, + "pseudoDepth": -8.4, + "elevationPseudoDepth": 16.972, + "n": 12, + "a": 1, + "b": 22, + "m": 11, + "k": 691.150391, + "vmap": { + "R": 0.219839, + "V": 62.776394, + "I": 285.55603, + "R0_RD": 0, + "SP": -264.453094, + "R0": 151.941925 + }, + "stacking": 1 + }, + { + "id": "1453611521859594", + "rowNo": 17, + "displayStatus": 0, + "horizontalDistance": 22.95679663366693, + "slopeDistance": 23.231100221926333, + "layerNo": null, + "pseudoDepth": -9.6, + "elevationPseudoDepth": 16.131, + "n": 17, + "a": 1, + "b": 25, + "m": 9, + "k": 100.530998, + "vmap": { + "R": 0.865843, + "V": 235.531586, + "I": 272.025665, + "R0_RD": 0, + "SP": 252.63089, + "R0": 87.04409 + }, + "stacking": 1 + }, + { + "id": "1453611521859595", + "rowNo": 18, + "displayStatus": 0, + "horizontalDistance": 22.822828222784292, + "slopeDistance": 23.136758664349642, + "layerNo": null, + "pseudoDepth": -9.6, + "elevationPseudoDepth": 15.8315, + "n": 16, + "a": 1, + "b": 25, + "m": 10, + "k": 141.371704, + "vmap": { + "R": 0.636254, + "V": 173.077347, + "I": 272.025665, + "R0_RD": 0, + "SP": 17.840714, + "R0": 89.94828 + }, + "stacking": 1 + }, + { + "id": "1453611521867776", + "rowNo": 19, + "displayStatus": 0, + "horizontalDistance": 22.928686812404784, + "slopeDistance": 23.24072059217002, + "layerNo": null, + "pseudoDepth": -9.6, + "elevationPseudoDepth": 15.769, + "n": 15, + "a": 1, + "b": 25, + "m": 11, + "k": 219.911407, + "vmap": { + "R": 0.112888, + "V": 30.708496, + "I": 272.025665, + "R0_RD": 0, + "SP": -189.315582, + "R0": 24.825411 + }, + "stacking": 1 + }, + { + "id": "1453611521867777", + "rowNo": 20, + "displayStatus": 0, + "horizontalDistance": 22.8941388207831, + "slopeDistance": 23.205240404465737, + "layerNo": null, + "pseudoDepth": -9.6, + "elevationPseudoDepth": 15.835000000000003, + "n": 14, + "a": 1, + "b": 25, + "m": 12, + "k": 449.247803, + "vmap": { + "R": 0.086995, + "V": 23.698902, + "I": 272.416779, + "R0_RD": 0, + "SP": 45.706585, + "R0": 39.082321 + }, + "stacking": 1 + } + ], + "__rowListTotal": 576 + } +} \ No newline at end of file diff --git a/tests/fixtures/dd/ert-measurement-scatter.json b/tests/fixtures/dd/ert-measurement-scatter.json new file mode 100644 index 0000000..91fedd6 --- /dev/null +++ b/tests/fixtures/dd/ert-measurement-scatter.json @@ -0,0 +1,830 @@ +{ + "code": 200, + "msg": "成功", + "data": { + "electrodeList": [ + { + "electrodeNo": 1, + "horizontalDistance": 0, + "slopeDistance": 0, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 26.172 + }, + { + "electrodeNo": 2, + "horizontalDistance": 1.0801616223246715, + "slopeDistance": 1.2459494894830485, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.551 + }, + { + "electrodeNo": 3, + "horizontalDistance": 3.1498647133801647, + "slopeDistance": 3.3194843662393243, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.425 + }, + { + "electrodeNo": 4, + "horizontalDistance": 4.943098993517602, + "slopeDistance": 5.123837209632222, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.625 + }, + { + "electrodeNo": 5, + "horizontalDistance": 7.0998855833076515, + "slopeDistance": 7.280646981934403, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.615 + }, + { + "electrodeNo": 6, + "horizontalDistance": 8.912141076960786, + "slopeDistance": 9.092942204649502, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.627 + }, + { + "electrodeNo": 7, + "horizontalDistance": 11.045738049466081, + "slopeDistance": 11.227964462398532, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.549 + }, + { + "electrodeNo": 8, + "horizontalDistance": 13.093762113301716, + "slopeDistance": 13.276838193512734, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.49 + }, + { + "electrodeNo": 9, + "horizontalDistance": 15.033442059887646, + "slopeDistance": 15.264042837022707, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.922 + }, + { + "electrodeNo": 10, + "horizontalDistance": 16.955987211873353, + "slopeDistance": 17.266206881606404, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.363 + }, + { + "electrodeNo": 11, + "horizontalDistance": 18.929898218381553, + "slopeDistance": 19.24013840565029, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.354 + }, + { + "electrodeNo": 12, + "horizontalDistance": 20.971809019338927, + "slopeDistance": 21.282366531758235, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.39 + }, + { + "electrodeNo": 13, + "horizontalDistance": 22.92873162714211, + "slopeDistance": 23.240034045135115, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.444 + }, + { + "electrodeNo": 14, + "horizontalDistance": 24.816468622227266, + "slopeDistance": 25.12811427717324, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.48 + }, + { + "electrodeNo": 15, + "horizontalDistance": 26.92747540642802, + "slopeDistance": 27.241302778689754, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.384 + }, + { + "electrodeNo": 16, + "horizontalDistance": 28.68966923369523, + "slopeDistance": 29.007310447092884, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.5 + }, + { + "electrodeNo": 17, + "horizontalDistance": 30.880151207446215, + "slopeDistance": 31.19815760682996, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.54 + }, + { + "electrodeNo": 18, + "horizontalDistance": 32.7659271758147, + "slopeDistance": 33.084258347213705, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.505 + }, + { + "electrodeNo": 19, + "horizontalDistance": 34.71171810948364, + "slopeDistance": 35.03134421012479, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.576 + }, + { + "electrodeNo": 20, + "horizontalDistance": 36.644544469129066, + "slopeDistance": 36.96419152342716, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.567 + }, + { + "electrodeNo": 21, + "horizontalDistance": 38.31639860471849, + "slopeDistance": 38.640204724654254, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.685 + }, + { + "electrodeNo": 22, + "horizontalDistance": 40.713354415497676, + "slopeDistance": 41.050580869331284, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.431 + }, + { + "electrodeNo": 23, + "horizontalDistance": 42.28359724152586, + "slopeDistance": 42.626616253671685, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.296 + }, + { + "electrodeNo": 24, + "horizontalDistance": 44.00336585439301, + "slopeDistance": 44.351366984010134, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.427 + }, + { + "electrodeNo": 25, + "horizontalDistance": 45.95916673864363, + "slopeDistance": 46.30721797496919, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.413 + }, + { + "electrodeNo": 26, + "horizontalDistance": 48.03509295173176, + "slopeDistance": 48.385051135303875, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.502 + }, + { + "electrodeNo": 27, + "horizontalDistance": 50.00439250362521, + "slopeDistance": 50.35438140854088, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.513 + }, + { + "electrodeNo": 28, + "horizontalDistance": 52.15587463692657, + "slopeDistance": 52.50823292847325, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.614 + }, + { + "electrodeNo": 29, + "horizontalDistance": 54.31927360220433, + "slopeDistance": 54.671698685762905, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.597 + }, + { + "electrodeNo": 30, + "horizontalDistance": 56.070653463392375, + "slopeDistance": 56.42330235616756, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.625 + }, + { + "electrodeNo": 31, + "horizontalDistance": 58.3971778197452, + "slopeDistance": 58.820309778079924, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 26.202 + }, + { + "electrodeNo": 32, + "horizontalDistance": 60.267675140394914, + "slopeDistance": 60.69681190346048, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 26.052 + }, + { + "electrodeNo": 33, + "horizontalDistance": 62.3517973628095, + "slopeDistance": 62.78119537057067, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 26.085 + }, + { + "electrodeNo": 34, + "horizontalDistance": 64.26099256115273, + "slopeDistance": 64.69219388061114, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 26.002 + }, + { + "electrodeNo": 35, + "horizontalDistance": 66.23694132533387, + "slopeDistance": 66.66845259815523, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 25.967 + }, + { + "electrodeNo": 36, + "horizontalDistance": 68.50618870757245, + "slopeDistance": 68.9401279010532, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 26.072 + }, + { + "electrodeNo": 37, + "horizontalDistance": 70.12425130226687, + "slopeDistance": 70.55912498577858, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 26.127 + }, + { + "electrodeNo": 38, + "horizontalDistance": 72.45685720369549, + "slopeDistance": 72.90445786010146, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 26.371 + }, + { + "electrodeNo": 39, + "horizontalDistance": 74.330372956236, + "slopeDistance": 74.85313876651416, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 26.907 + }, + { + "electrodeNo": 40, + "horizontalDistance": 75.76722057945078, + "slopeDistance": 76.30859577114809, + "layerNo": 0, + "pseudoDepth": 0, + "elevationPseudoDepth": 27.139 + } + ], + "scatterGraphConf": { + "canFilter": null, + "x": [ + { + "fieldCode": "horizontalDistance", + "name": "平距", + "sheetHeaderNameList": null + }, + { + "fieldCode": "slopeDistance", + "name": "斜距", + "sheetHeaderNameList": null + } + ], + "v": [ + { + "fieldCode": "I", + "name": "电流", + "sheetHeaderNameList": [ + "I(mA)" + ] + }, + { + "fieldCode": "V", + "name": "电压", + "sheetHeaderNameList": [ + "V(mV)" + ] + }, + { + "fieldCode": "R", + "name": "电阻", + "sheetHeaderNameList": [ + "R(Ohm)" + ] + }, + { + "fieldCode": "R0", + "name": "视电阻率", + "sheetHeaderNameList": [ + "R0" + ] + }, + { + "fieldCode": "SP", + "name": "自然电位", + "sheetHeaderNameList": [ + "SP" + ] + }, + { + "fieldCode": "R0_RD", + "name": "电阻误差", + "sheetHeaderNameList": [ + "R0_RD" + ] + } + ], + "y": [ + { + "fieldCode": "Layer No", + "name": "层数", + "sheetHeaderNameList": null + }, + { + "fieldCode": "pseudoDepth", + "name": "伪深度", + "sheetHeaderNameList": null + }, + { + "fieldCode": "elevationPseudoDepth", + "name": "伪深度+高程", + "sheetHeaderNameList": null + } + ], + "vcalculationMethod": [ + { + "fieldCode": null, + "name": "线性", + "sheetHeaderNameList": null + }, + { + "fieldCode": null, + "name": "对数", + "sheetHeaderNameList": null + }, + { + "fieldCode": null, + "name": "导数", + "sheetHeaderNameList": null + } + ] + }, + "scatterGraphData": { + "hasCoordinate": true, + "hasElevation": true, + "min": [ + 2.115013167852418, + 2.2827169278611863, + 0, + -15.6, + -1066.592407, + -1.1950568103363985, + -7538.067239559778, + 0, + "" + ], + "max": [ + 73.39361507996574, + 73.87879831330781, + 0, + -1.2, + 1232.438965, + "NaN", + 15669.56031213764, + 0, + "" + ], + "rows": [ + [ + 2.115013167852418, + 2.2827169278611863, + null, + -1.2, + 242.952988, + 2.385522244678844, + 4.116022643854044, + 0, + "1453611521843200", + 24.288, + 1, + 4, + 2, + 3, + 12.5664, + 1, + 242.952988 + ], + [ + 5.124875148343908, + 5.300065674086864, + null, + -2.4, + 74.753899, + 1.8736338494382652, + 13.377228657999497, + 0, + "1453611521843201", + 23.12, + 1, + 7, + 3, + 5, + 25.1327, + 2, + 74.753899 + ], + [ + 7.994418521491841, + 8.175900836015376, + null, + -3.6, + 100.239388, + 2.001038406459415, + 9.976118369757005, + 0, + "1453611521843202", + 21.987, + 1, + 10, + 4, + 7, + 37.6991, + 3, + 100.239388 + ], + [ + 8.00601333013422, + 8.186794593291953, + null, + -3.6, + 70.582214, + 1.8486952770460858, + 14.16787521003521, + 0, + "1453611521843203", + 22.020999999999997, + 1, + 10, + 5, + 6, + 125.663696, + 4, + 70.582214 + ], + [ + 11.066663821597649, + 11.272344909478555, + null, + -4.8, + 75.202255, + 1.8762308634556513, + 13.297473593045847, + 0, + "1453611521843204", + 20.9685, + 1, + 13, + 5, + 9, + 50.265499, + 5, + 75.202255 + ], + [ + 11.00295159513125, + 11.184890199081117, + null, + -4.8, + 103.542862, + 2.015120165027033, + 9.65783619154742, + 0, + "1453611521843205", + 20.758499999999998, + 1, + 13, + 6, + 8, + 109.955704, + 6, + 103.542862 + ], + [ + 13.92101964767117, + 14.166540305149894, + null, + -6, + 85.264038, + 1.9307658964508176, + 11.728273999877885, + 0, + "1453611521859584", + 19.490499999999997, + 1, + 16, + 6, + 11, + 62.831902, + 7, + 85.264038 + ], + [ + 14.000862630669717, + 14.247085672002468, + null, + -6, + 63.056168, + 1.7997275746100803, + 15.858876803297024, + 0, + "1453611521859585", + 19.456, + 1, + 16, + 7, + 10, + 113.097298, + 8, + 63.056168 + ], + [ + 14.063602086594681, + 14.27044051526772, + null, + -6, + 44.272751, + 1.6461365088096525, + 22.587256888554318, + 0, + "1453611521859586", + 19.706, + 1, + 16, + 8, + 9, + 351.858398, + 9, + 44.272751 + ], + [ + 16.987234838304097, + 17.233999253766825, + null, + -7.2, + 67.065033, + 1.8264961426225779, + 14.91089999165437, + 0, + "1453611521859587", + 18.296499999999998, + 1, + 19, + 7, + 13, + 75.398201, + 10, + 67.065033 + ], + [ + 17.03278556632032, + 17.279602362635487, + null, + -7.2, + 70.816719, + 1.85013580164825, + 14.120959204562979, + 0, + "1453611521859588", + 18.24, + 1, + 19, + 8, + 12, + 120.951302, + 11, + 70.816719 + ], + [ + 16.9816701391346, + 17.2520906213365, + null, + -7.2, + 41.787144, + 1.6210426897024701, + 23.930805129922256, + 0, + "1453611521859589", + 18.438, + 1, + 19, + 9, + 11, + 251.327393, + 12, + 41.787144 + ], + [ + 20.01061875986487, + 20.259070486101244, + null, + -8.4, + 68.505302, + 1.8357241852184725, + 14.597410285119246, + 0, + "1453611521859590", + 17.037, + 1, + 22, + 8, + 15, + 87.9646, + 13, + 68.505302 + ], + [ + 19.924955341057455, + 20.196078557097973, + null, + -8.4, + 76.61187, + 1.8842960630545889, + 13.05280761323278, + 0, + "1453611521859591", + 17.301000000000002, + 1, + 22, + 9, + 14, + 130.690308, + 14, + 76.61187 + ], + [ + 19.94235941950773, + 20.25312046337076, + null, + -8.4, + 58.417942, + 1.7665462531889349, + 17.118028567319268, + 0, + "1453611521859592", + 17.003500000000003, + 1, + 22, + 10, + 13, + 226.194595, + 15, + 58.417942 + ], + [ + 19.95085361886024, + 20.261252468704264, + null, + -8.4, + 151.941925, + 2.1816776243164018, + 6.581461963180999, + 0, + "1453611521859593", + 16.972, + 1, + 22, + 11, + 12, + 691.150391, + 16, + 151.941925 + ], + [ + 22.95679663366693, + 23.231100221926333, + null, + -9.6, + 87.04409, + 1.9397392893244523, + 11.488430748141546, + 0, + "1453611521859594", + 16.131, + 1, + 25, + 9, + 17, + 100.530998, + 17, + 87.04409 + ], + [ + 22.822828222784292, + 23.136758664349642, + null, + -9.6, + 89.94828, + 1.9539928631384818, + 11.117499967759251, + 0, + "1453611521859595", + 15.8315, + 1, + 25, + 10, + 16, + 141.371704, + 18, + 89.94828 + ], + [ + 22.928686812404784, + 23.24072059217002, + null, + -9.6, + 24.825411, + 1.3948964472401078, + 40.28130692378064, + 0, + "1453611521867776", + 15.769, + 1, + 25, + 11, + 15, + 219.911407, + 19, + 24.825411 + ], + [ + 22.8941388207831, + 23.205240404465737, + null, + -9.6, + 39.082321, + 1.5919803474612875, + 25.587016697396244, + 0, + "1453611521867777", + 15.835000000000003, + 1, + 25, + 12, + 14, + 449.247803, + 20, + 39.082321 + ] + ], + "__rowsTotal": 576 + } + } +} \ No newline at end of file diff --git a/tests/fixtures/dd/ert-trajectory-line.json b/tests/fixtures/dd/ert-trajectory-line.json new file mode 100644 index 0000000..1b2db91 --- /dev/null +++ b/tests/fixtures/dd/ert-trajectory-line.json @@ -0,0 +1,29 @@ +{ + "code": 200, + "msg": "成功", + "data": { + "id": null, + "electrodelList": [ + { "electrodeNo": 1, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16370297649078, "y": 22.54583186479361, "z": 0 }] } }, + { "electrodeNo": 2, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16371479128762, "y": 22.54583434513919, "z": 0 }] } }, + { "electrodeNo": 3, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16373395960512, "y": 22.545840141157978, "z": 0 }] } }, + { "electrodeNo": 4, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16375149792411, "y": 22.545840448398355, "z": 0 }] } }, + { "electrodeNo": 5, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16377225700053, "y": 22.545843190544776, "z": 0 }] } }, + { "electrodeNo": 6, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16378981598876, "y": 22.54584453624687, "z": 0 }] } }, + { "electrodeNo": 7, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16381008094395, "y": 22.54584870568223, "z": 0 }] } }, + { "electrodeNo": 8, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16382964511763, "y": 22.545852180493952, "z": 0 }] } }, + { "electrodeNo": 9, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16384844206722, "y": 22.54585632432848, "z": 0 }] } }, + { "electrodeNo": 10, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16386684249656, "y": 22.545862220446992, "z": 0 }] } }, + { "electrodeNo": 11, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16388446655657, "y": 22.545869273235397, "z": 0 }] } }, + { "electrodeNo": 12, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16390199529941, "y": 22.545877933516117, "z": 0 }] } }, + { "electrodeNo": 13, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16391836757842, "y": 22.545886947155804, "z": 0 }] } }, + { "electrodeNo": 14, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16393426292046, "y": 22.545895473642467, "z": 0 }] } }, + { "electrodeNo": 15, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16395127881961, "y": 22.545906166262615, "z": 0 }] } }, + { "electrodeNo": 16, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16396318204642, "y": 22.545917658760136, "z": 0 }] } }, + { "electrodeNo": 17, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16397869446884, "y": 22.545931215521435, "z": 0 }] } }, + { "electrodeNo": 18, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16399060913946, "y": 22.545944161886897, "z": 0 }] } }, + { "electrodeNo": 19, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16400183500123, "y": 22.54595831901666, "z": 0 }] } }, + { "electrodeNo": 20, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16401229325622, "y": 22.545972820080397, "z": 0 }] } } + ] + } +} diff --git a/tests/fixtures/dd/ert-trajectory-rows.json b/tests/fixtures/dd/ert-trajectory-rows.json new file mode 100644 index 0000000..fdd22b1 --- /dev/null +++ b/tests/fixtures/dd/ert-trajectory-rows.json @@ -0,0 +1,46 @@ +{ + "code": 200, + "msg": "成功", + "data": { + "gridHeaderDisplay": [ + { "columnCode": "electrodeNo", "columnNameChn": "电极号", "columnNameEng": "Electrode No", "columnWidth": 10, "columnSort": 1 }, + { "columnCode": "horizontalDistance", "columnNameChn": "平距", "columnNameEng": "Horizontal Distance", "columnWidth": 10, "columnSort": 2 }, + { "columnCode": "slopeDistance", "columnNameChn": "斜距", "columnNameEng": "Slope Distance", "columnWidth": 10, "columnSort": 3 }, + { "columnCode": "projectX", "columnNameChn": "经度/东坐标", "columnNameEng": "Longitude/EastCoordinate", "columnWidth": 10, "columnSort": 4 }, + { "columnCode": "projectY", "columnNameChn": "纬度/北坐标", "columnNameEng": "Latitude/NorthCoordinate", "columnWidth": 10, "columnSort": 5 }, + { "columnCode": "elevation", "columnNameChn": "高程", "columnNameEng": "Elevation", "columnWidth": 10, "columnSort": 6 }, + { "columnCode": "lineNo", "columnNameChn": "线号", "columnNameEng": "Line No", "columnWidth": 10, "columnSort": 7 }, + { "columnCode": "channelNo", "columnNameChn": "道号", "columnNameEng": "Channel No", "columnWidth": 10, "columnSort": 8 }, + { "columnCode": "measuredDistance", "columnNameChn": "里程", "columnNameEng": "Measured Distance", "columnWidth": 10, "columnSort": 9 }, + { "columnCode": "depth", "columnNameChn": "深度", "columnNameEng": "Depth", "columnWidth": 10, "columnSort": 10 }, + { "columnCode": "depthOfWater", "columnNameChn": "水深", "columnNameEng": "Depth of Water", "columnWidth": 10, "columnSort": 11 }, + { "columnCode": "date", "columnNameChn": "日期", "columnNameEng": "Date", "columnWidth": 10, "columnSort": 12 }, + { "columnCode": "time", "columnNameChn": "时间", "columnNameEng": "Time", "columnWidth": 10, "columnSort": 13 }, + { "columnCode": "resultStatus", "columnNameChn": "解状态", "columnNameEng": "Result Status", "columnWidth": 10, "columnSort": 14 } + ], + "trajectoryDataModelHead": null, + "__rowTotal": 40, + "rowList": [ + { "id": "1438893959585792", "electrodeNo": 1, "horizontalDistance": 0, "slopeDistance": 0, "longitude": 0, "latitude": null, "projectX": 516838.884, "projectY": 2494251.836, "elevation": 26.172, "lineNo": "ert1", "channelNo": "ERT1-1", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959593984", "electrodeNo": 2, "horizontalDistance": 1.0801616223246715, "slopeDistance": 1.2459494894830485, "longitude": 0, "latitude": null, "projectX": 516840.099, "projectY": 2494252.112, "elevation": 25.551, "lineNo": "ert1", "channelNo": "ERT1-2", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959593985", "electrodeNo": 3, "horizontalDistance": 3.1498647133801647, "slopeDistance": 3.3194843662393243, "longitude": 0, "latitude": null, "projectX": 516842.07, "projectY": 2494252.756, "elevation": 25.425, "lineNo": "ert1", "channelNo": "ERT1-3", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959593986", "electrodeNo": 4, "horizontalDistance": 4.943098993517602, "slopeDistance": 5.123837209632222, "longitude": 0, "latitude": null, "projectX": 516843.874, "projectY": 2494252.792, "elevation": 25.625, "lineNo": "ert1", "channelNo": "ERT1-4", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959593987", "electrodeNo": 5, "horizontalDistance": 7.0998855833076515, "slopeDistance": 7.280646981934403, "longitude": 0, "latitude": null, "projectX": 516846.009, "projectY": 2494253.098, "elevation": 25.615, "lineNo": "ert1", "channelNo": "ERT1-5", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959593988", "electrodeNo": 6, "horizontalDistance": 8.912141076960786, "slopeDistance": 9.092942204649502, "longitude": 0, "latitude": null, "projectX": 516847.815, "projectY": 2494253.249, "elevation": 25.627, "lineNo": "ert1", "channelNo": "ERT1-6", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959593989", "electrodeNo": 7, "horizontalDistance": 11.045738049466081, "slopeDistance": 11.227964462398532, "longitude": 0, "latitude": null, "projectX": 516849.899, "projectY": 2494253.713, "elevation": 25.549, "lineNo": "ert1", "channelNo": "ERT1-7", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959593990", "electrodeNo": 8, "horizontalDistance": 13.093762113301716, "slopeDistance": 13.276838193512734, "longitude": 0, "latitude": null, "projectX": 516851.911, "projectY": 2494254.1, "elevation": 25.49, "lineNo": "ert1", "channelNo": "ERT1-8", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959593991", "electrodeNo": 9, "horizontalDistance": 15.033442059887646, "slopeDistance": 15.264042837022707, "longitude": 0, "latitude": null, "projectX": 516853.844, "projectY": 2494254.561, "elevation": 25.922, "lineNo": "ert1", "channelNo": "ERT1-9", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959593992", "electrodeNo": 10, "horizontalDistance": 16.955987211873353, "slopeDistance": 17.266206881606404, "longitude": 0, "latitude": null, "projectX": 516855.736, "projectY": 2494255.216, "elevation": 25.363, "lineNo": "ert1", "channelNo": "ERT1-10", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959593993", "electrodeNo": 11, "horizontalDistance": 18.929898218381553, "slopeDistance": 19.24013840565029, "longitude": 0, "latitude": null, "projectX": 516857.548, "projectY": 2494255.999, "elevation": 25.354, "lineNo": "ert1", "channelNo": "ERT1-11", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959593994", "electrodeNo": 12, "horizontalDistance": 20.971809019338927, "slopeDistance": 21.282366531758235, "longitude": 0, "latitude": null, "projectX": 516859.35, "projectY": 2494256.96, "elevation": 25.39, "lineNo": "ert1", "channelNo": "ERT1-12", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959593995", "electrodeNo": 13, "horizontalDistance": 22.92873162714211, "slopeDistance": 23.240034045135115, "longitude": 0, "latitude": null, "projectX": 516861.033, "projectY": 2494257.96, "elevation": 25.444, "lineNo": "ert1", "channelNo": "ERT1-13", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959593996", "electrodeNo": 14, "horizontalDistance": 24.816468622227266, "slopeDistance": 25.12811427717324, "longitude": 0, "latitude": null, "projectX": 516862.667, "projectY": 2494258.906, "elevation": 25.48, "lineNo": "ert1", "channelNo": "ERT1-14", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959593997", "electrodeNo": 15, "horizontalDistance": 26.92747540642802, "slopeDistance": 27.241302778689754, "longitude": 0, "latitude": null, "projectX": 516864.416, "projectY": 2494260.092, "elevation": 25.384, "lineNo": "ert1", "channelNo": "ERT1-15", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959593998", "electrodeNo": 16, "horizontalDistance": 28.68966923369523, "slopeDistance": 29.007310447092884, "longitude": 0, "latitude": null, "projectX": 516865.639, "projectY": 2494261.366, "elevation": 25.5, "lineNo": "ert1", "channelNo": "ERT1-16", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959593999", "electrodeNo": 17, "horizontalDistance": 30.880151207446215, "slopeDistance": 31.19815760682996, "longitude": 0, "latitude": null, "projectX": 516867.233, "projectY": 2494262.869, "elevation": 25.54, "lineNo": "ert1", "channelNo": "ERT1-17", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959594000", "electrodeNo": 18, "horizontalDistance": 32.7659271758147, "slopeDistance": 33.084258347213705, "longitude": 0, "latitude": null, "projectX": 516868.457, "projectY": 2494264.304, "elevation": 25.505, "lineNo": "ert1", "channelNo": "ERT1-18", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959594001", "electrodeNo": 19, "horizontalDistance": 34.71171810948364, "slopeDistance": 35.03134421012479, "longitude": 0, "latitude": null, "projectX": 516869.61, "projectY": 2494265.873, "elevation": 25.576, "lineNo": "ert1", "channelNo": "ERT1-19", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }, + { "id": "1438893959594002", "electrodeNo": 20, "horizontalDistance": 36.644544469129066, "slopeDistance": 36.96419152342716, "longitude": 0, "latitude": null, "projectX": 516870.684, "projectY": 2494267.48, "elevation": 25.567, "lineNo": "ert1", "channelNo": "ERT1-20", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" } + ] + } +} diff --git a/tests/net/FakeApiCall.hpp b/tests/net/FakeApiCall.hpp new file mode 100644 index 0000000..71b48a4 --- /dev/null +++ b/tests/net/FakeApiCall.hpp @@ -0,0 +1,15 @@ +#pragma once +#include "IApiCall.hpp" + +namespace geopro::net::test { + +// 测试假 call。不加 Q_OBJECT —— 仅发射继承自 IApiCall 的 finished 信号、override abort。 +class FakeApiCall : public IApiCall { +public: + using IApiCall::IApiCall; + bool aborted = false; + void abort() override { aborted = true; } + void fire(const ApiResponse& r) { emit finished(r); } // 发射继承的信号(无需自身 moc) +}; + +} // namespace geopro::net::test diff --git a/tests/net/test_api_batch.cpp b/tests/net/test_api_batch.cpp new file mode 100644 index 0000000..566517c --- /dev/null +++ b/tests/net/test_api_batch.cpp @@ -0,0 +1,62 @@ +#include +#include +#include "ApiBatch.hpp" +#include "net/FakeApiCall.hpp" + +using namespace geopro::net; +using geopro::net::test::FakeApiCall; + +namespace { +ApiResponse ok(int code = 200) { ApiResponse r; r.code = code; r.httpStatus = 200; return r; } +ApiResponse bad() { ApiResponse r; r.code = 500; r.httpStatus = 200; r.msg = QStringLiteral("boom"); return r; } +// repo 注入的失败谓词:业务码 != 200 或传输错误。 +auto isFailure = [](const ApiResponse& r) { return r.code != 200 || !r.rawError.isEmpty(); }; +} + +TEST(ApiBatch, SucceedsWhenAllOk) { + auto* a = new FakeApiCall; + auto* b = new FakeApiCall; + auto* batch = new ApiBatch({a, b}, isFailure); + QSignalSpy okSpy(batch, &ApiBatch::succeeded); + QSignalSpy failSpy(batch, &ApiBatch::failed); + a->fire(ok()); + EXPECT_EQ(okSpy.count(), 0); // 还差 b + b->fire(ok()); + EXPECT_EQ(okSpy.count(), 1); // 全到齐 + EXPECT_EQ(failSpy.count(), 0); +} + +TEST(ApiBatch, SucceedsWithSingleCall) { + auto* a = new FakeApiCall; + auto* batch = new ApiBatch({a}, isFailure); + QSignalSpy okSpy(batch, &ApiBatch::succeeded); + a->fire(ok()); // N=1:一次 fire 即触发 --remaining_==0 + EXPECT_EQ(okSpy.count(), 1); +} + +TEST(ApiBatch, FailFastAbortsOthers) { + auto* a = new FakeApiCall; + auto* b = new FakeApiCall; // 慢的(永不 fire) + auto* batch = new ApiBatch({a, b}, isFailure); + QSignalSpy failSpy(batch, &ApiBatch::failed); + QSignalSpy okSpy(batch, &ApiBatch::succeeded); + a->fire(bad()); // 首个失败 + EXPECT_EQ(failSpy.count(), 1); + EXPECT_EQ(okSpy.count(), 0); + EXPECT_TRUE(b->aborted); // 其余在飞被 abort +} + +TEST(ApiBatch, AbortGateSuppressesLateSignals) { + auto* a = new FakeApiCall; + auto* b = new FakeApiCall; + auto* batch = new ApiBatch({a, b}, isFailure); + QSignalSpy okSpy(batch, &ApiBatch::succeeded); + QSignalSpy failSpy(batch, &ApiBatch::failed); + batch->abort(); + EXPECT_TRUE(a->aborted); + EXPECT_TRUE(b->aborted); + a->fire(ok()); // 迟到信号 + b->fire(ok()); + EXPECT_EQ(okSpy.count(), 0); // aborted_ 闸门:不 emit + EXPECT_EQ(failSpy.count(), 0); +} diff --git a/tests/net/test_api_chain.cpp b/tests/net/test_api_chain.cpp new file mode 100644 index 0000000..7c7f70a --- /dev/null +++ b/tests/net/test_api_chain.cpp @@ -0,0 +1,70 @@ +#include +#include +#include +#include "ApiChain.hpp" +#include "net/FakeApiCall.hpp" + +using namespace geopro::net; +using geopro::net::test::FakeApiCall; + +namespace { +ApiResponse ok(int v = 0) { ApiResponse r; r.code = 200; r.httpStatus = 200; r.data = QJsonObject{{"v", v}}; return r; } +ApiResponse bad() { ApiResponse r; r.code = 500; r.httpStatus = 200; r.msg = QStringLiteral("boom"); return r; } +auto isFailure = [](const ApiResponse& r) { return r.code != 200 || !r.rawError.isEmpty(); }; +} // namespace + +TEST(ApiChain, RunsStepsInOrderAndPassesPriorResponses) { + auto* s1 = new FakeApiCall; + auto* s2 = new FakeApiCall; + int seenPrior = -1; + QList steps{ + [&](const QList&) -> IApiCall* { return s1; }, + [&](const QList& prior) -> IApiCall* { + seenPrior = prior.size(); // 第二步能看到第一步响应 + return s2; + }}; + auto* chain = new ApiChain(steps, isFailure); + QSignalSpy okSpy(chain, &ApiChain::succeeded); + s1->fire(ok(11)); // 第一步完成 → 触发第二步工厂 + EXPECT_EQ(seenPrior, 1); + EXPECT_EQ(okSpy.count(), 0); // 还差第二步 + s2->fire(ok(22)); + EXPECT_EQ(okSpy.count(), 1); + const auto resps = okSpy.takeFirst().at(0).value>(); + EXPECT_EQ(resps.size(), 2); +} + +TEST(ApiChain, FailFastShortCircuitsRemainingSteps) { + auto* s1 = new FakeApiCall; + bool secondBuilt = false; + QList steps{ + [&](const QList&) -> IApiCall* { return s1; }, + [&](const QList&) -> IApiCall* { secondBuilt = true; return new FakeApiCall; }}; + auto* chain = new ApiChain(steps, isFailure); + QSignalSpy failSpy(chain, &ApiChain::failed); + s1->fire(bad()); // 第一步失败 + EXPECT_EQ(failSpy.count(), 1); + EXPECT_FALSE(secondBuilt); // 后续步骤不再构造 +} + +TEST(ApiChain, AbortGateSuppressesLateSignals) { + auto* s1 = new FakeApiCall; + QList steps{[&](const QList&) -> IApiCall* { return s1; }}; + auto* chain = new ApiChain(steps, isFailure); + QSignalSpy okSpy(chain, &ApiChain::succeeded); + chain->abort(); + EXPECT_TRUE(s1->aborted); // 在飞步骤被 abort + s1->fire(ok()); // 迟到 + EXPECT_EQ(okSpy.count(), 0); // aborted_ 闸门 +} + +TEST(ApiChain, StepFactoryThrowBecomesFailed) { + auto* s1 = new FakeApiCall; + QList steps{ + [&](const QList&) -> IApiCall* { return s1; }, + [&](const QList&) -> IApiCall* { throw std::runtime_error("rsa fail"); }}; + auto* chain = new ApiChain(steps, isFailure); + QSignalSpy failSpy(chain, &ApiChain::failed); + s1->fire(ok()); // 触发第二步工厂 → 抛 → failed + EXPECT_EQ(failSpy.count(), 1); +} diff --git a/tests/net/test_auth.cpp b/tests/net/test_auth.cpp index 21c683e..b1b2647 100644 --- a/tests/net/test_auth.cpp +++ b/tests/net/test_auth.cpp @@ -1,17 +1,19 @@ // net 层端到端登录连通测试(真实站点)。 // 复刻已实测通过的流程:getImageCode -> verifyCodeCheck -> RSA -> login2, // 关键在 ApiClient 内单一 QNetworkAccessManager 共享 JSESSIONID 会话。 -// 需要 QCoreApplication 提供事件循环(ApiClient 用 QEventLoop 同步等待)。 -// 网络不可达时本用例会失败(属环境问题,非逻辑问题)。 +// 异步化:fetchCaptchaAsync/loginAsync 返回自管理句柄,QSignalSpy::wait 驱动事件循环等待。 +// 需要 QCoreApplication 提供事件循环。网络不可达时本用例会失败(属环境问题,非逻辑问题)。 #include #include +#include #include #include #include #include "ApiClient.hpp" +#include "AuthLoads.hpp" #include "AuthService.hpp" namespace { @@ -35,11 +37,24 @@ TEST(AuthLiveTest, FullLoginFlowReturnsToken) { geopro::net::AuthService auth( api, slurp("D:/Git/lanbingtech/geopro/resources/rsa_public_key.pem")); - auto cap = auth.fetchCaptcha(); + auto* cl = auth.fetchCaptchaAsync(); + QSignalSpy capDone(cl, &geopro::net::CaptchaLoad::done); + QSignalSpy capFail(cl, &geopro::net::CaptchaLoad::failed); + ASSERT_TRUE(capDone.wait(10000) || capFail.count() > 0); + ASSERT_EQ(capDone.count(), 1) + << (capFail.count() ? capFail.takeFirst().at(0).toString().toStdString() : "captcha failed"); + auto cap = capDone.takeFirst().at(0).value(); ASSERT_FALSE(cap.codeId.isEmpty()); ASSERT_FALSE(cap.code.isEmpty()); - auto r = auth.login("sydk", "123456", cap.code, cap.codeId); - EXPECT_TRUE(r.ok) << r.error.toStdString(); - EXPECT_TRUE(r.token.startsWith("Geomative ")) << r.token.toStdString(); + auto* ll = auth.loginAsync("sydk", "123456", cap.code, cap.codeId); + QSignalSpy loginDone(ll, &geopro::net::LoginLoad::done); + QSignalSpy loginFail(ll, &geopro::net::LoginLoad::failed); + ASSERT_TRUE(loginDone.wait(10000) || loginFail.count() > 0); + EXPECT_EQ(loginDone.count(), 1) + << (loginFail.count() ? loginFail.takeFirst().at(0).toString().toStdString() : ""); + if (loginDone.count()) { + auto token = loginDone.takeFirst().at(0).toString(); + EXPECT_TRUE(token.startsWith("Geomative ")) << token.toStdString(); + } } diff --git a/tests/net/test_auth_loads.cpp b/tests/net/test_auth_loads.cpp new file mode 100644 index 0000000..bc369ee --- /dev/null +++ b/tests/net/test_auth_loads.cpp @@ -0,0 +1,153 @@ +// AuthLoads 离线单测:CaptchaLoad/LoginLoad 句柄行为(done/failed/abort 闸门), +// 用 FakeApiCall + 真 ApiChain 离线驱动,不联网。QSignalSpy 需 Qt6::Test。 +#include + +#include + +#include + +#include "ApiChain.hpp" +#include "ApiClient.hpp" +#include "AuthLoads.hpp" +#include "AuthService.hpp" +#include "net/FakeApiCall.hpp" + +using namespace geopro::net; +using geopro::net::test::FakeApiCall; + +namespace { + +ApiResponse captchaOk() { + ApiResponse r; + r.code = 200; + r.httpStatus = 200; + r.data = QJsonObject{{"id", "cid-1"}, {"code", "AB12"}}; + return r; +} + +ApiResponse tokenOk() { + ApiResponse r; + r.code = 200; + r.httpStatus = 200; + r.data = QJsonObject{{"accessToken", "Geomative deadbeef"}}; + return r; +} + +ApiResponse plainOk() { + ApiResponse r; + r.code = 200; + r.httpStatus = 200; + return r; +} + +ApiResponse failResp() { + ApiResponse r; + r.code = 500; + r.httpStatus = 200; + r.msg = QStringLiteral("验证码错误"); + return r; +} + +auto isFailure = [](const ApiResponse& r) { return r.code != 200 || !r.rawError.isEmpty(); }; + +} // namespace + +TEST(CaptchaLoad, ParsesCaptchaOnSuccess) { + auto* call = new FakeApiCall; + auto* load = new CaptchaLoad(call); + QSignalSpy doneSpy(load, &CaptchaLoad::done); + QSignalSpy failSpy(load, &CaptchaLoad::failed); + call->fire(captchaOk()); + ASSERT_EQ(doneSpy.count(), 1); + EXPECT_EQ(failSpy.count(), 0); + auto cap = doneSpy.takeFirst().at(0).value(); + EXPECT_EQ(cap.codeId, QStringLiteral("cid-1")); + EXPECT_EQ(cap.code, QStringLiteral("AB12")); +} + +TEST(CaptchaLoad, EmitsFailedOnErrorResponse) { + auto* call = new FakeApiCall; + auto* load = new CaptchaLoad(call); + QSignalSpy doneSpy(load, &CaptchaLoad::done); + QSignalSpy failSpy(load, &CaptchaLoad::failed); + call->fire(failResp()); + EXPECT_EQ(doneSpy.count(), 0); + ASSERT_EQ(failSpy.count(), 1); + EXPECT_EQ(failSpy.takeFirst().at(0).toString(), QStringLiteral("验证码错误")); +} + +TEST(CaptchaLoad, AbortGateSuppressesLateSignal) { + auto* call = new FakeApiCall; + auto* load = new CaptchaLoad(call); + QSignalSpy doneSpy(load, &CaptchaLoad::done); + load->abort(); + EXPECT_TRUE(call->aborted); + call->fire(captchaOk()); // 迟到 + EXPECT_EQ(doneSpy.count(), 0); +} + +TEST(LoginLoad, EmitsTokenOnChainSuccess) { + auto* s1 = new FakeApiCall; + auto* s2 = new FakeApiCall; + QList steps{ + [&](const QList&) -> IApiCall* { return s1; }, + [&](const QList&) -> IApiCall* { return s2; }}; + auto* chain = new ApiChain(steps, isFailure); + auto* load = new LoginLoad(chain); + QSignalSpy doneSpy(load, &LoginLoad::done); + QSignalSpy failSpy(load, &LoginLoad::failed); + s1->fire(plainOk()); // verifyCodeCheck 通过 → 触发 step2 + s2->fire(tokenOk()); // login2 返回 token + ASSERT_EQ(doneSpy.count(), 1); + EXPECT_EQ(failSpy.count(), 0); + EXPECT_EQ(doneSpy.takeFirst().at(0).toString(), QStringLiteral("Geomative deadbeef")); +} + +TEST(LoginLoad, EmitsFailedWhenChainFails) { + auto* s1 = new FakeApiCall; + QList steps{ + [&](const QList&) -> IApiCall* { return s1; }}; + auto* chain = new ApiChain(steps, isFailure); + auto* load = new LoginLoad(chain); + QSignalSpy doneSpy(load, &LoginLoad::done); + QSignalSpy failSpy(load, &LoginLoad::failed); + s1->fire(failResp()); + EXPECT_EQ(doneSpy.count(), 0); + ASSERT_EQ(failSpy.count(), 1); + EXPECT_EQ(failSpy.takeFirst().at(0).toString(), QStringLiteral("验证码错误")); +} + +TEST(LoginLoad, EmitsFailedWhenTokenMissing) { + auto* s1 = new FakeApiCall; + QList steps{ + [&](const QList&) -> IApiCall* { return s1; }}; + auto* chain = new ApiChain(steps, isFailure); + auto* load = new LoginLoad(chain); + QSignalSpy doneSpy(load, &LoginLoad::done); + QSignalSpy failSpy(load, &LoginLoad::failed); + s1->fire(plainOk()); // 成功但无 accessToken + EXPECT_EQ(doneSpy.count(), 0); + ASSERT_EQ(failSpy.count(), 1); +} + +// M-2:step 工厂抛异常(模拟 RSA 失败)时 LoginLoad 应 emit failed。 +// 第一步用 FakeApiCall(不同步 fire,由测试手动触发),第二步工厂抛异常。 +// 构造顺序:new ApiChain(同步执行 step1 工厂,s1 已建立但未 fire) +// → new LoginLoad(连接 chain 的 succeeded/failed) +// → s1->fire(plainOk())(step1 成功 → step2 工厂抛 → ApiChain emit failed +// → LoginLoad 已连接,收到 failed)。 +TEST(LoginLoad, EmitsFailedWhenStepFactoryThrows) { + auto* s1 = new FakeApiCall; + QList steps{ + [&](const QList&) -> IApiCall* { return s1; }, + [&](const QList&) -> IApiCall* { + throw std::runtime_error("rsa fail"); + }}; + auto* chain = new ApiChain(steps, isFailure); + auto* load = new LoginLoad(chain); + QSignalSpy doneSpy(load, &LoginLoad::done); + QSignalSpy failSpy(load, &LoginLoad::failed); + s1->fire(plainOk()); // step1 成功 → step2 工厂抛异常 → ApiChain emit failed + ASSERT_EQ(failSpy.count(), 1); + EXPECT_EQ(doneSpy.count(), 0); +} diff --git a/tests/render/test_contour_bands.cpp b/tests/render/test_contour_bands.cpp new file mode 100644 index 0000000..37996c4 --- /dev/null +++ b/tests/render/test_contour_bands.cpp @@ -0,0 +1,90 @@ +#include +#include "ContourBands.hpp" +using namespace geopro::core; +using namespace geopro::render; + +// 上采样 2x:色带边界更密 → 多边形数应多于不上采样。 +TEST(ContourBands, UpsampleIncreasesDetail) { + Grid g(3, 3); + g.x = {0, 1, 2}; g.y = {0, 1, 2}; + for (int j = 0; j < 3; ++j) + for (int i = 0; i < 3; ++i) g.valueAt(i, j) = static_cast(i * i + j); + g.vmin = 0; g.vmax = 6; + ColorScale cs; + cs.addStop(0, Rgba{0,0,255,255}); cs.addStop(3, Rgba{0,255,0,255}); cs.addStop(6, Rgba{255,0,0,255}); + ContourOptions a; a.upsample = 1; a.smooth = 0; + ContourOptions b; b.upsample = 2; b.smooth = 0; + auto ra = buildContourBands(g, cs, a); + auto rb = buildContourBands(g, cs, b); + EXPECT_GT(rb.bands.size(), ra.bands.size()); +} + +// 含 NaN 无效角的网格:裁剪后该角不应被任何色带覆盖(所有多边形顶点远离该角)。 +TEST(ContourBands, ClipsNaNRegion) { + Grid g(3, 3); + g.x = {0, 1, 2}; g.y = {0, 1, 2}; + for (int j = 0; j < 3; ++j) for (int i = 0; i < 3; ++i) g.valueAt(i, j) = 5.0; + g.valueAt(2, 2) = std::nan(""); // 右上角无效 + g.vmin = 0; g.vmax = 10; + ColorScale cs; cs.addStop(0, Rgba{0,0,255,255}); cs.addStop(10, Rgba{255,0,0,255}); + ContourOptions opt; opt.upsample = 1; opt.smooth = 0; + auto r = buildContourBands(g, cs, opt); + bool coversCorner = false; + for (const auto& b : r.bands) + for (const auto& p : b.ring) + if (p.x > 1.5 && p.y > 1.5) coversCorner = true; + EXPECT_FALSE(coversCorner); +} + +// 2x2 平滑梯度网格 + 2 段色阶 → 至少 1 个色带多边形,多边形顶点 >=3。 +TEST(ContourBands, ProducesNonEmptyBands) { + Grid g(3, 3); + g.x = {0, 1, 2}; g.y = {0, 1, 2}; + for (int j = 0; j < 3; ++j) + for (int i = 0; i < 3; ++i) g.valueAt(i, j) = static_cast(i + j); // 0..4 + g.vmin = 0; g.vmax = 4; + ColorScale cs; + cs.addStop(0.0, Rgba{0, 0, 255, 255}); + cs.addStop(2.0, Rgba{0, 255, 0, 255}); + cs.addStop(4.0, Rgba{255, 0, 0, 255}); + + ContourOptions opt; opt.upsample = 1; opt.smooth = 0; + auto r = buildContourBands(g, cs, opt); + ASSERT_FALSE(r.bands.empty()); + for (const auto& b : r.bands) EXPECT_GE(b.ring.size(), 3u); +} + +// 诊断:等值线(lines)应非空——网格跨越多个色阶级时应产生等值线。 +TEST(ContourBands, ProducesContourLines) { + Grid g(10, 10); + g.x.resize(10); g.y.resize(10); + for (int i = 0; i < 10; ++i) { g.x[i] = i; g.y[i] = i; } + for (int j = 0; j < 10; ++j) + for (int i = 0; i < 10; ++i) g.valueAt(i, j) = i + j; // 0..18 + g.vmin = 0; g.vmax = 18; + ColorScale cs; + cs.addStop(0, Rgba{0,0,255,255}); cs.addStop(6, Rgba{0,255,0,255}); + cs.addStop(12, Rgba{255,255,0,255}); cs.addStop(18, Rgba{255,0,0,255}); + ContourOptions opt; opt.upsample = 1; opt.smooth = 0; opt.makeLines = true; + auto r = buildContourBands(g, cs, opt); + EXPECT_FALSE(r.lines.empty()); +} + +// 诊断(真实参数复现):大网格 + 默认 opt(upsample=2,smooth=0.3,simplifyTol=0.5) + 17 级色阶。 +TEST(ContourBands, ProducesLinesRealisticOptions) { + Grid g(60, 40); + g.x.resize(60); g.y.resize(40); + for (int i = 0; i < 60; ++i) g.x[i] = i * 3.0; + for (int j = 0; j < 40; ++j) g.y[j] = -33.0 + j * 1.2; + for (int j = 0; j < 40; ++j) + for (int i = 0; i < 60; ++i) + g.valueAt(i, j) = 2.0 + 340.0 * (0.5 + 0.5 * std::sin(i * 0.2) * std::cos(j * 0.25)); + g.vmin = 2; g.vmax = 348; + ColorScale cs; + const double bars[17] = {2.08,4.59,7.81,11.02,14.26,17.25,20.56,23.55,27.05, + 31.16,36.83,43.64,51.98,62.09,81.14,108.24,348.52}; + for (int i = 0; i < 17; ++i) cs.addStop(bars[i], Rgba{(unsigned char)(i*15),0,200,255}); + ContourOptions opt; // 默认 upsample=2, smooth=0.3, simplifyTol=0.5, makeLines=true + auto r = buildContourBands(g, cs, opt); + EXPECT_FALSE(r.lines.empty()); +} diff --git a/tests/render/test_qwt_smoke.cpp b/tests/render/test_qwt_smoke.cpp new file mode 100644 index 0000000..8dcf0c2 --- /dev/null +++ b/tests/render/test_qwt_smoke.cpp @@ -0,0 +1,10 @@ +#include + +#include + +// 冒烟:验证项目能编译 Qwt 头文件并链接 Qwt 静态库(QwtText 为值类型,无需 QApplication)。 +// 保障 Qwt 集成(cmake/qwt.cmake + 官方 Qt6)持续可用,不回归。 +TEST(QwtSmoke, LinksAgainstQwt) { + QwtText t(QStringLiteral("hello")); + EXPECT_EQ(t.text(), QStringLiteral("hello")); +}