diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml deleted file mode 100644 index 7a662a61..00000000 --- a/.github/workflows/test-and-build.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Test and Publish -on: - push: - paths: - - 'SimConnect/__init__.py' - -jobs: - build-n-publish: - name: Build and publish Python distributions to PyPI - runs-on: ubuntu-18.04 - steps: - - uses: actions/checkout@master - - name: Set up Python 3.8 - uses: actions/setup-python@v1 - with: - python-version: 3.8 - - name: Install dependencies - run: python3 -m pip install --user --upgrade setuptools wheel twine - - name: Build source - run: python3 setup.py sdist bdist_wheel - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@master - with: - user: __token__ - password: ${{ secrets.pypi_password }} diff --git a/.wheels/numpy-2.5.2-cp314-cp314-win_amd64.whl b/.wheels/numpy-2.5.2-cp314-cp314-win_amd64.whl new file mode 100644 index 00000000..05e6c6cd Binary files /dev/null and b/.wheels/numpy-2.5.2-cp314-cp314-win_amd64.whl differ diff --git a/.wheels/pandas-3.0.5-cp314-cp314-win_amd64.whl b/.wheels/pandas-3.0.5-cp314-cp314-win_amd64.whl new file mode 100644 index 00000000..56f742ee Binary files /dev/null and b/.wheels/pandas-3.0.5-cp314-cp314-win_amd64.whl differ diff --git a/.wheels/python_dateutil-2.9.0.post0-py2.py3-none-any.whl b/.wheels/python_dateutil-2.9.0.post0-py2.py3-none-any.whl new file mode 100644 index 00000000..b9a14e1b Binary files /dev/null and b/.wheels/python_dateutil-2.9.0.post0-py2.py3-none-any.whl differ diff --git a/.wheels/six-1.17.0-py2.py3-none-any.whl b/.wheels/six-1.17.0-py2.py3-none-any.whl new file mode 100644 index 00000000..c506fd05 Binary files /dev/null and b/.wheels/six-1.17.0-py2.py3-none-any.whl differ diff --git a/.wheels/tzdata-2026.3-py2.py3-none-any.whl b/.wheels/tzdata-2026.3-py2.py3-none-any.whl new file mode 100644 index 00000000..c41abfd1 Binary files /dev/null and b/.wheels/tzdata-2026.3-py2.py3-none-any.whl differ diff --git a/Docs/README.md b/Docs/README.md new file mode 100644 index 00000000..5525252c --- /dev/null +++ b/Docs/README.md @@ -0,0 +1,116 @@ +# Flight Assistant + +## 图表模块架构 + +重构后图表功能拆为两层: + +- **`models/`**:数据与配置(与 Qt / UI 无关) +- **`charts/`**:Qt Charts 渲染与交互 + +``` +app_controller + └── ChartHistoryBuffer ← models/chart.py(写数据) + └── read_aq_snapshot() ← models/data_bridge.py(单拍 AQ) + +models/data_bridge.py + └── FieldDef / build_overlay_config() / load_track_bundle() + +charts/ + ├── LineChart ← 读 ChartConfig + field_data + ├── HealthSpanOverlay ← 读 overlay spec + health_span_kind + └── TrendChart (facade) ← 组装为页面可直接使用的 widget +``` + +页面侧用法: + +```python +from charts import TrendChart # 成品趋势图(推荐) +from charts.container import ChartSlot # 自行拼装时才需要 +from charts.base.line_chart import LineChart +``` + +--- + +## `models/chart.py` — 数据层 + +| 内容 | 作用 | 谁在用 | +|------|------|--------| +| `ChartFieldSpec` / `ChartConfig` | 字段定义、Y 轴范围、视窗时长 | `LineChart`、`TrendChart`、`ChartHistoryBuffer` | +| `ChartHistoryBuffer` | 存时间序列;`append` / `slice_view` | **`app_controller.py`** 写数据,图表只读 | +| `TRAINING_CHART` 等预置 | 训练页默认图表配置 | controller、facade | +| `health_band` / `health_span_kind` | 健康度业务阈值判定(96.5 / 97.5 / 99.5) | `app_frame` 指标颜色、`HealthSpanOverlay` 色带 | +| `view_bounds` / `relative_times` / `series_points` | 视窗切片、坐标换算 | `ChartSlot`、`LineChart` | + +数据流:`read_aq_snapshot` → `ChartHistoryBuffer.append()` + 轨迹录制 → 图表 `update(history)` 读切片渲染。不经过 Qt。 + +--- + +## `models/data_bridge.py` — 字段、蒙版与轨迹出入 + +| 内容 | 作用 | 谁在用 | +|------|------|--------| +| `FieldDef` / `FIELDS` | 图表字段出数 + 轴/线型/蒙版元数据 | `extract`、`frame_to_history` | +| `read_aq_snapshot` | 单拍 SimConnect 快照(图表+录制共用) | `app_controller`、`shadow_plane` | +| `load_track_bundle` | CSV 一次加载 → `(Trajectory, ChartHistoryBuffer)` | `track_compare` | +| `ThresholdSpanOverlaySpec` / `LapMarkerOverlaySpec` | 蒙版规格 | `registry.create_overlays()` | +| `build_overlay_config` / `load_overlay_config` | 从 FieldDef 组装蒙版 | `TrendChart` 初始化 | +| `kind_resolver` | `"health_span_kind"` → `models.chart.health_span_kind` | `HealthSpanOverlay` | + +字段蒙版与图级 `lap_marker` 由 `FIELDS` / `CHART_LEVEL_OVERLAYS` 定义(不再使用 JSON)。 + +兼容:`models.chart_overlay` 仍 re-export 上述符号。 + +--- + +## `charts/` 目录结构 + +``` +charts/ + qtcharts.py # PySide2 QtCharts 兼容导入 + context.py # ChartRenderContext(一次 render 的共享上下文) + theme.py # 配色适配(委托 UITheme) + container.py # ChartDisplayArea / ChartPane / ChartSlot + base/ # 基底图表:LineChart、BarChart(速度带可选附图) + overlay/ # 蒙版图层:HealthSpanOverlay、LapMarkerOverlay、registry + facade/ # 对外成品 widget(见下文) + widgets/ # PanChartView、ChartToolbar、TrackMapPane、SpeedBandPane、StickInput… +``` + +### 三层职责 + +1. **容器层**(`container.py`):总显示区域 + 可嵌套二级容器;统一 `update` / 平移 / 布局 +2. **基底图表**(`base/`):按类型只渲染业务数据(折线 / 柱),不含蒙版 +3. **蒙版图层**(`overlay/`):按阈值或标注规则覆盖基底(色带、LAP 竖线等) + +### `charts/facade/` — 外观层 + +Facade 对外提供简单 API,内部组装 container + base + overlay + toolbar。 + +当前仅有 `TrendChart`: + +``` +TrainingAssistPage + └── TrendChart ← facade + ├── ChartToolbar + ├── ChartDisplayArea + │ ├── ChartSlot(LineChart + overlays) 主图 + │ └── TrackMapPane + StickInputPane 底部附图 + ├── TimelineScrubber + └── update / add_lap_marker 等 API +``` + +页面无需了解 `ChartSlot`、`HealthSpanOverlay` 等细节。 + +可选附图:`charts/widgets/speed_band_pane.py`(`BarChart` + 速度带累计)仍保留,可按需挂到 `bottom_widgets`;默认 UI 使用航迹图。 + +--- + +## 修改指南 + +| 要改的内容 | 文件 | +|------------|------| +| 扭矩 / 健康 Y 轴范围、视窗 150s | `models/chart.py` 或 `TRAINING_CHART` | +| 健康阈值 96.5 / 97.5 / 99.5 | `models/chart.py` 中 `HEALTH_BOUND_*` | +| 色带透明度 / 颜色、LAP 标签文案 | `models/data_bridge.py`(`FieldDef.overlays` / `CHART_LEVEL_OVERLAYS`) | +| 折线样式、拖拽、布局、SAVE | `charts/` | +| SimVar / 弧度转度 | `services/simulator.py` | diff --git a/Docs/Simulation Variables.pdf b/Docs/Simulation Variables.pdf new file mode 100644 index 00000000..0a954130 Binary files /dev/null and b/Docs/Simulation Variables.pdf differ diff --git a/Docs/_regen_zh.py b/Docs/_regen_zh.py new file mode 100644 index 00000000..4344b21b --- /dev/null +++ b/Docs/_regen_zh.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +"""重新生成带中文说明的模拟器参数清单。""" + +import re +from collections import OrderedDict, defaultdict +from pathlib import Path + +from simvar_translate import ( + FUEL_ENUM_ZH, + best_chinese, + translate_units, +) + +ROOT = Path(__file__).resolve().parent.parent +text = (ROOT / "SimConnect" / "RequestList.py").read_text(encoding="utf-8") + +cat_map = OrderedDict( + [ + ("__AircraftEngineData", "一、Aircraft Engine Data(发动机)"), + ("__AircraftFuelData", "二、Aircraft Fuel Data(燃油)"), + ("__AircraftLightsData", "三、Aircraft Lights Data(灯光)"), + ("__AircraftPositionandSpeedData", "四、Aircraft Position and Speed Data(位置与速度)"), + ("__AircraftFlightInstrumentationData", "五、Aircraft Flight Instrumentation Data(飞行仪表)"), + ("__AircraftAvionicsData", "六、Aircraft Avionics Data(航电)"), + ("__AircraftControlsData", "七、Aircraft Controls Data(操纵/控制)"), + ("__AircraftAutopilotData", "八、Aircraft Autopilot Data(自动驾驶)"), + ("__AircraftLandingGearData", "九、Aircraft Landing Gear Data(起落架)"), + ("__AircraftEnvironmentData", "十、Aircraft Environment Data(飞机环境)"), + ("__HelicopterSpecificData", "十一、Helicopter Specific Data(直升机)"), + ("__SlingsandHoists", "十二、Slings and Hoists(吊索/绞车)"), + ("__AircraftMiscellaneousSystemsData", "十三、Aircraft Miscellaneous Systems Data(杂项系统)"), + ("__AircraftMiscellaneousData", "十四、Miscellaneous Data(杂项)"), + ("__AircraftStringData", "十五、Aircraft String Data(字符串)"), + ("__AIControlledAircraft", "十六、AI Controlled Aircraft(AI 飞机)"), + ("__CarrierOperations", "十七、Carrier Operations(航母作业)"), + ("__Racing", "十八、Racing(竞速)"), + ("__EnvironmentData", "十九、Environment Data(全局环境)"), + ] +) + +items = [] +pos = 0 +while True: + m = re.search(r"class (__\w+)\(RequestHelper\):\s*\n\s*list = \{", text[pos:]) + if not m: + break + cname = m.group(1) + start = pos + m.end() - 1 + depth = 0 + i = start + while i < len(text): + if text[i] == "{": + depth += 1 + elif text[i] == "}": + depth -= 1 + if depth == 0: + break + i += 1 + d = eval(text[start : i + 1], {"__builtins__": {}}) + for _key, val in d.items(): + desc, simname, units, sett = val + sim = simname.decode() if isinstance(simname, bytes) else simname + u = units.decode() if isinstance(units, bytes) else units + items.append((cname, sim, desc, u, sett == "Y")) + pos = i + 1 + +by = defaultdict(list) +seen = set() +for c, sim, desc, u, sett in items: + if (c, sim) in seen: + continue + seen.add((c, sim)) + by[c].append((sim, desc, u, sett)) + +total = sum(len(v) for v in by.values()) +writable = sum(1 for rows in by.values() for r in rows if r[3]) +readonly = total - writable + +lines = [] +lines.append("# 模拟器 Simulation Variables 参数清单") +lines.append("") +lines.append( + "> 来源:`Docs/Simulation Variables.pdf`(FSX/SimConnect 官方仿真变量表)," + "并与项目 `SimConnect/RequestList.py` 中的结构化定义对齐。" +) +lines.append("> 补充说明见 `Docs/项目原理.md`。") +lines.append("") +lines.append("## 读写规则说明") +lines.append("") +lines.append("| 标记 | 含义 | 本项目用法 |") +lines.append("|---|---|---|") +lines.append( + "| **只读 (N)** | 只能通过 get / request_data 订阅读取 | " + "`AircraftRequests.get(...)` 或 REST `GET /datapoint//get` |" +) +lines.append( + "| **可写 (Y)** | 可通过 set_data 直接写入数值 | " + "`AircraftRequests.set(...)` 或 REST `POST /datapoint//set` |" +) +lines.append("") +lines.append("**交互补充(非变量写入):**") +lines.append("") +lines.append( + "- 许多开关/手柄(如自动驾驶开关、灯光开关)在变量表中为 **只读**," + "需通过 **SimConnect 事件**(`EventList.py` / `POST /event//trigger`)触发才能交互。" +) +lines.append("- 带 `:index` 的变量需要指定系统索引(发动机从 0,通讯设备从 1)。") +lines.append( + "- PDF 另有 **Program Data**(`P:`,如 `SIMULATION RATE`、`UNITS OF MEASURE`)" + "与 **Units of Measurement** 单位表;本清单主体为 Aircraft/Environment 的 `A:` 仿真变量。" +) +lines.append("") +lines.append("## 总览") +lines.append("") +lines.append(f"- **变量总数**:{total}") +lines.append(f"- **可直接写入 (Y)**:{writable}") +lines.append(f"- **只能读取 (N)**:{readonly}") +lines.append("") +lines.append("| 分类 | 总数 | 可写 | 只读 |") +lines.append("|---|---:|---:|---:|") +for c, title in cat_map.items(): + rows = by.get(c, []) + y = sum(1 for r in rows if r[3]) + lines.append(f"| {title} | {len(rows)} | {y} | {len(rows) - y} |") +lines.append("") +lines.append("---") +lines.append("") + +lines.append("## 附录 A:Fuel Tank Selection(油箱选择枚举值)") +lines.append("") +lines.append( + "以下不是独立 SimVar,而是 `FUEL TANK SELECTOR` / " + "`RECIP ENG FUEL TANK SELECTOR` 等枚举取值(文档章节 *Fuel Tank Selection*):" +) +lines.append("") +lines.append("| 枚举值 | 英文含义 | 中文含义 |") +lines.append("|---:|---|---|") +fuel_en = [ + (0, "Off"), (1, "All"), (2, "Left"), (3, "Right"), (4, "Left auxiliary"), + (5, "Right auxiliary"), (6, "Center"), (7, "Center2"), (8, "Center3"), + (9, "External1"), (10, "External2"), (11, "Right tip"), (12, "Left tip"), + (13, "Crossfeed"), (14, "Crossfeed left to right"), (15, "Crossfeed right to left"), + (16, "Both"), (17, "External"), (18, "Isolate"), (19, "Left main"), (20, "Right main"), +] +for v, en in fuel_en: + lines.append(f"| {v} | {en} | {FUEL_ENUM_ZH[v]} |") +lines.append("") + +lines.append("## 附录 B:Program Data(程序数据,PDF 有、RequestList 未收录)") +lines.append("") +lines.append("| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 |") +lines.append("|---|---|---|---|---|") +lines.append( + "| `SIMULATION RATE` | Time acceleration factor | 时间加速倍率 | Number | 只读 |" +) +lines.append( + "| `UNITS OF MEASURE` | Units of measure: 0=English; 1=Metric(alt feet); 2=Metric(alt meters) | " + "单位制:0=英制;1=公制(高度英尺);2=公制(高度米) | Enum | 只读 |" +) +lines.append("") +lines.append("---") +lines.append("") + +for c, title in cat_map.items(): + rows = by.get(c, []) + lines.append(f"## {title}") + lines.append("") + y = sum(1 for r in rows if r[3]) + lines.append(f"共 **{len(rows)}** 项(可写 {y} / 只读 {len(rows) - y})") + lines.append("") + lines.append("| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 |") + lines.append("|---|---|---|---|---|") + for sim, desc, u, sett in rows: + flag = "**可写**" if sett else "只读" + zh = best_chinese(sim, desc) + desc_esc = desc.replace("|", "\\|").replace("\n", " ") + zh_esc = zh.replace("|", "\\|") + u_zh = translate_units(u) + u_cell = f"{u}({u_zh})" if u_zh != u else u + lines.append(f"| `{sim}` | {desc_esc} | {zh_esc} | {u_cell} | {flag} |") + lines.append("") + +out = ROOT / "Docs" / "模拟器参数清单.md" +out.write_text("\n".join(lines), encoding="utf-8") +print("wrote", out) +print("bytes", out.stat().st_size) + +# quality check +content = out.read_text(encoding="utf-8") +rows = [ln for ln in content.splitlines() if ln.startswith("| `")] +still_en = sum(1 for ln in rows if len(re.findall(r"[\u4e00-\u9fff]", ln.split("|")[3])) < 2) +print("rows", len(rows), "low_zh_quality", still_en) diff --git a/Docs/simvar_translate.py b/Docs/simvar_translate.py new file mode 100644 index 00000000..98c84567 --- /dev/null +++ b/Docs/simvar_translate.py @@ -0,0 +1,697 @@ +# -*- coding: utf-8 -*- +"""本地航空术语翻译:为 Simulation Variables 生成中文说明。""" + +import re + +FUEL_ENUM_ZH = { + 0: "关闭", 1: "全部", 2: "左", 3: "右", 4: "左辅助", 5: "右辅助", 6: "中央", + 7: "中央2", 8: "中央3", 9: "外部1", 10: "外部2", 11: "右翼尖", 12: "左翼尖", + 13: "串油", 14: "左向右串油", 15: "右向左串油", 16: "两侧", 17: "外部", + 18: "隔离", 19: "左主", 20: "右主", +} + +UNITS_ZH = { + "Number": "数值", "Mask": "掩码", "Percent": "百分比", "Enum": "枚举", "Bool": "布尔", + "Rpm": "转/分", "Rankine": "兰氏度", "Psf": "磅/平方英尺", "Psi": "磅/平方英寸", + "Hours": "小时", "Position": "位置", "Ratio": "比值", "Celsius": "摄氏度", + "Pounds per hour": "磅/小时", "Foot pounds per second": "英尺·磅/秒", + "Foot pound": "英尺·磅", "Pounds": "磅", "Gallons": "加仑", + "Percent Over 100": "百分比(超100)", "Percent over 100": "百分比(超100)", + "Feet per second": "英尺/秒", "Feet per second squared": "英尺/秒²", + "Degrees": "度", "Radians": "弧度", "Knots": "节", "feet/minute": "英尺/分钟", + "Millibars": "毫巴", "inHg": "英寸汞柱", "Radians per second": "弧度/秒", + "Meters": "米", "String": "字符串", "Seconds": "秒", "Gallons per hour": "加仑/小时", + "Scalar": "标量", "Foot pounds": "英尺·磅", "inHG.": "英寸汞柱", + "Rpm (0 to 16384 = 0 to 100%)": "转/分(0-16384=0-100%)", + "Rpm(0 to 16384 = 0 to 100%)": "转/分(0-16384=0-100%)", + "Ratio (0-16384)": "比值(0-16384)", + "SIMCONNECT_DATA_XYZ structure": "SIMCONNECT_DATA_XYZ 结构", + "Nautical miles": "海里", "MHz": "兆赫", "Hz": "赫兹", "Volts": "伏特", + "PSI": "磅/平方英寸", "Flags": "标志位", "Per radian": "每弧度", + "Per second": "每秒", "Feet": "英尺", "Frequency ADF BCD32": "ADF BCD32 频率", +} + +# 变量名整句翻译(优先) +SIMVAR_ZH = { + "SELECTED DME": "选中的 DME", + "HSI CDI NEEDLE VALID": "HSI CDI 指针信号有效", + "HSI GSI NEEDLE VALID": "HSI GSI 指针信号有效", + "ADF NAME": "ADF 描述名称", + "RETRACT LEFT FLOAT EXTENDED": "左浮筒收放位置", + "RETRACT RIGHT FLOAT EXTENDED": "右浮筒收放位置", + "SIM DISABLED": "模拟器是否禁用", + "SIGMA SQRT": "西格玛平方根", + "LINEAR CL ALPHA": "线性升力系数迎角", + "ZERO LIFT ALPHA": "零升力迎角", + "DECISION HEIGHT": "决断高度", + "SEMIBODY LOADFACTOR YDOT": "半机体法向载荷因子变化率", + "SIMULATED RADIUS": "模拟半径", + "CATEGORY": "飞机类别", + "AI GROUNDCRUISESPEED": "AI 地面巡航速度", + "AI GROUNDTURNSPEED": "AI 地面转弯速度", + "ZULU YEAR": "世界时年份", + "AVIONICS MASTER SWITCH": "航电总开关", + "COM RECIEVE ALL": "通讯全接收标志", + "NAV AVAILABLE:index": "导航设备可用标志(索引)", + "NAV HAS NAV:index": "导航信号可用标志(索引)", + "NAV HAS DME:index": "导航台 DME 可用标志(索引)", + "NAV HAS GLIDE SLOPE:index": "导航台下滑道可用标志(索引)", + "NAV HAS LOCALIZER:index": "调谐台为航向道标志(索引)", + "NAV BACK COURSE FLAGS:index": "导航背台标志位(索引)", + "NAV MAGVAR:index": "导航台磁偏角(索引)", + "NAV RADIAL:index": "飞机所在径向(索引)", + "NAV RADIAL ERROR:index": "当前径向与 OBS 调谐径向之差(索引)", + "NAV LOCALIZER:index": "航向道航道航向(索引)", + "NAV GLIDE SLOPE ERROR:index": "当前位置与下滑道角之差(索引)", + "NAV CDI:index": "CDI 指针偏转(索引)", + "NAV GSI:index": "下滑道指针偏转(索引)", + "NAV GS FLAG:index": "下滑道标志(索引)", + "NAV OBS:index": "OBS 设定(索引)", + "NAV DME:index": "DME 距离(索引)", + "NAV DMESPEED:index": "DME 速度(索引)", + "NAV SIGNAL:index": "导航信号强度(索引)", + "COM TRANSMIT:index": "音频面板通讯发射状态(索引)", + "COM ACTIVE FREQUENCY:index": "通讯频率(索引)", + "COM STANDBY FREQUENCY:index": "通讯备用频率(索引)", + "NAV ACTIVE FREQUENCY:index": "导航激活频率(索引)", + "NAV STANDBY FREQUENCY:index": "导航备用频率(索引)", + "ADF ACTIVE FREQUENCY:index": "ADF 频率(索引)", + "ADF RADIAL:index": "相对 NDB 台当前方位(索引)", + "DISK PITCH ANGLE": "旋翼盘俯仰角", + "DISK BANK ANGLE": "旋翼盘坡度角", + "DISK PITCH PCT": "旋翼盘俯仰百分比", + "DISK BANK PCT": "旋翼盘坡度百分比", + "DISK CONING PCT": "旋翼盘锥度百分比", + "STATIC CG TO GROUND": "静重心离地高度", + "STATIC PITCH": "静俯仰角", + "APU PCT RPM": "APU 转速百分比", + "APU PCT STARTER": "APU 起动机百分比", + "APU VOLTS": "APU 电压", + "APU GENERATOR SWITCH": "APU 发电机开关", + "APU GENERATOR ACTIVE": "APU 发电机激活", + "APU ON FIRE DETECTED": "APU 着火探测", + "PRESSURIZATION CABIN ALTITUDE": "客舱增压高度", + "PRESSURIZATION CABIN ALTITUDE GOAL": "客舱增压目标高度", + "PRESSURIZATION CABIN ALTITUDE RATE": "客舱增压高度变化率", + "PRESSURIZATION PRESSURE DIFFERENTIAL": "客舱增压压差", + "PRESSURIZATION DUMP SWITCH": "客舱增压泄压开关", + "FIRE BOTTLE SWITCH": "灭火瓶开关", + "FIRE BOTTLE DISCHARGED": "灭火瓶已释放", + "CABIN NO SMOKING ALERT SWITCH": "客舱禁烟告警开关", + "CABIN SEATBELTS ALERT SWITCH": "客舱系安全带告警开关", + "GPWS WARNING": "近地警告系统已安装", + "GPWS SYSTEM ACTIVE": "近地警告系统激活", + "IS ALTITUDE FREEZE ON": "高度冻结开启", + "IS ATTITUDE FREEZE ON": "姿态冻结开启", + "SLING HOOK IN PICKUP MODE:index": "吊索钩处于拾取模式(索引)", + "AI TRAFFIC STATE": "AI 交通状态", + "AI TRAFFIC ASSIGNED PARKING": "AI 分配停机位", + "RECIP ENG FUEL TANKS USED:index": "往复式发动机使用油箱(索引)", + "TURB ENG TANKS USED:index": "涡轮发动机使用油箱(索引)", + "ENG TURBINE TEMPERATURE:index": "发动机涡轮温度(索引)", + "ENG ELECTRICAL LOAD:index": "发动机电气负载(索引)", + "ENG TRANSMISSION PRESSURE:index": "发动机传动箱压力(索引)", + "ENG TRANSMISSION TEMPERATURE:index": "发动机传动箱温度(索引)", +} + +# 多词短语(变量名分词用,长串优先) +NAME_COMPOUNDS = [ + ("NUMBER OF ENGINES", "发动机数量"), + ("ENGINE CONTROL SELECT", "发动机控制选择"), + ("THROTTLE LOWER LIMIT", "油门下限"), + ("MASTER IGNITION SWITCH", "主点火开关"), + ("GENERAL ENG", "发动机"), + ("RECIP ENG", "往复式发动机"), + ("TURB ENG", "涡轮发动机"), + ("ENG COMBUSTION", "发动机燃烧"), + ("ENG ON FIRE", "发动机着火"), + ("ENG FUEL FLOW", "发动机燃油流量"), + ("ENG N1 RPM", "发动机 N1 转速"), + ("ENG N2 RPM", "发动机 N2 转速"), + ("ENG MAX RPM", "发动机最大转速"), + ("ENG OIL QUANTITY", "发动机滑油量"), + ("ENG HYDRAULIC", "发动机液压"), + ("ENG MANIFOLD PRESSURE", "发动机歧管压力"), + ("ENG EXHAUST GAS TEMPERATURE", "发动机排气温度"), + ("ENG CYLINDER HEAD TEMPERATURE", "发动机缸盖温度"), + ("ENG OIL TEMPERATURE", "发动机滑油温度"), + ("ENG OIL PRESSURE", "发动机滑油压力"), + ("ENG ANTI ICE", "发动机防冰"), + ("ENG PRESSURE RATIO", "发动机压比"), + ("ENG TORQUE", "发动机扭矩"), + ("ENG VIBRATION", "发动机振动"), + ("ENG FAILED", "发动机故障"), + ("ENG RPM ANIMATION PERCENT", "发动机转速动画百分比"), + ("ENG FUEL FLOW BUG POSITION", "燃油流量游标位置"), + ("PROP MAX RPM PERCENT", "螺旋桨最大转速百分比"), + ("PROP FEATHERING INHIBIT", "螺旋桨顺桨抑制"), + ("PROP AUTO FEATHER ARMED", "螺旋桨自动顺桨预位"), + ("PROP FEATHER SWITCH", "螺旋桨顺桨开关"), + ("PANEL AUTO FEATHER SWITCH", "面板自动顺桨开关"), + ("PROP SYNC DELTA LEVER", "螺旋桨同步增量杆"), + ("PROP SYNC ACTIVE", "螺旋桨同步激活"), + ("PROP DEICE SWITCH", "螺旋桨除冰开关"), + ("GENERAL ENG STARTER ACTIVE", "发动机起动机激活"), + ("GENERAL ENG FUEL USED SINCE START", "自启动以来发动机耗油"), + ("TURB ENG PRIMARY NOZZLE PERCENT", "涡轮发动机主喷管百分比"), + ("TURB ENG IGNITION SWITCH", "涡轮发动机点火开关"), + ("TURB ENG MASTER STARTER SWITCH", "涡轮发动机主起动机开关"), + ("TURB ENG AFTERBURNER STAGE ACTIVE", "涡轮发动机加力级数"), + ("FUEL TANK", "油箱"), + ("FUEL LEFT CAPACITY", "左侧燃油容量"), + ("FUEL RIGHT CAPACITY", "右侧燃油容量"), + ("FUEL TOTAL QUANTITY", "总燃油量"), + ("FUEL TOTAL CAPACITY", "总燃油容量"), + ("FUEL WEIGHT PER GALLON", "每加仑燃油重量"), + ("FUEL CROSS FEED", "燃油串油"), + ("FUEL TRANSFER PUMP", "燃油传输泵"), + ("LIGHT ON STATES", "灯光开启状态"), + ("LIGHT STATES", "灯光状态"), + ("LANDING LIGHT PBH", "着陆灯俯仰/坡度/航向"), + ("LIGHT TAXI ON", "滑行灯开启"), + ("LIGHT STROBE ON", "频闪灯开启"), + ("LIGHT PANEL ON", "面板灯开启"), + ("LIGHT RECOGNITION ON", "识别灯开启"), + ("LIGHT WING ON", "机翼灯开启"), + ("LIGHT LOGO ON", "标志灯开启"), + ("LIGHT CABIN ON", "客舱灯开启"), + ("LIGHT BEACON ON", "信标灯开启"), + ("LIGHT NAV ON", "导航灯开启"), + ("LIGHT LANDING ON", "着陆灯开启"), + ("PLANE LATITUDE", "飞机纬度"), + ("PLANE LONGITUDE", "飞机经度"), + ("PLANE ALTITUDE", "飞机高度"), + ("PLANE ALT ABOVE GROUND", "飞机离地高度"), + ("PLANE PITCH DEGREES", "飞机俯仰角"), + ("PLANE BANK DEGREES", "飞机坡度角"), + ("PLANE HEADING DEGREES TRUE", "飞机真航向"), + ("PLANE HEADING DEGREES MAGNETIC", "飞机磁航向"), + ("PLANE HEADING DEGREES GYRO", "陀螺航向"), + ("VELOCITY BODY", "机体速度"), + ("VELOCITY WORLD", "世界速度"), + ("ACCELERATION BODY", "机体加速度"), + ("ACCELERATION WORLD", "世界加速度"), + ("ROTATION VELOCITY BODY", "机体旋转角速度"), + ("WING FLEX PCT", "机翼弯曲百分比"), + ("AIRSPEED TRUE", "真空速"), + ("AIRSPEED INDICATED", "指示空速"), + ("AIRSPEED TRUE CALIBRATE", "真空速校准角"), + ("VERTICAL SPEED", "垂直速度"), + ("INDICATED ALTITUDE", "指示高度"), + ("KOHLSMAN SETTING MB", "气压高度表设定(毫巴)"), + ("WISKEY COMPASS INDICATION DEGREES", "磁罗盘指示"), + ("DELTA HEADING RATE", "航向变化率"), + ("PARTIAL PANEL", "部分面板"), + ("SUCTION PRESSURE", "真空吸力压力"), + ("YOKE Y POSITION", "操纵杆纵向位置"), + ("YOKE X POSITION", "操纵杆横向位置"), + ("RUDDER PEDAL POSITION", "方向舵踏板位置"), + ("RUDDER POSITION", "方向舵位置"), + ("ELEVATOR POSITION", "升降舵位置"), + ("AILERON POSITION", "副翼位置"), + ("ELEVATOR TRIM", "升降舵配平"), + ("BRAKE LEFT POSITION", "左刹车位置"), + ("BRAKE RIGHT POSITION", "右刹车位置"), + ("BRAKE PARKING", "停留刹车"), + ("SPOILERS ARMED", "扰流板预位"), + ("SPOILERS HANDLE POSITION", "扰流板手柄位置"), + ("SPOILERS LEFT POSITION", "左扰流板位置"), + ("SPOILERS RIGHT POSITION", "右扰流板位置"), + ("FLAPS HANDLE", "襟翼手柄"), + ("TRAILING EDGE FLAPS", "后缘襟翼"), + ("LEADING EDGE FLAPS", "前缘襟翼"), + ("AILERON LEFT DEFLECTION", "左副翼偏转"), + ("AILERON RIGHT DEFLECTION", "右副翼偏转"), + ("AILERON AVERAGE DEFLECTION", "副翼平均偏转"), + ("AILERON TRIM PCT", "副翼配平百分比"), + ("RUDDER DEFLECTION", "方向舵偏转"), + ("RUDDER TRIM PCT", "方向舵配平百分比"), + ("AUTOPILOT MASTER", "自动驾驶主开关"), + ("AUTOPILOT AVAILABLE", "自动驾驶可用"), + ("AUTOPILOT WING LEVELER", "自动驾驶改平"), + ("AUTOPILOT NAV1 LOCK", "自动驾驶 NAV1 锁定"), + ("AUTOPILOT HEADING LOCK", "自动驾驶航向锁定"), + ("AUTOPILOT HEADING LOCK DIR", "自动驾驶航向锁定方向"), + ("AUTOPILOT ALTITUDE LOCK", "自动驾驶高度锁定"), + ("AUTOPILOT ALTITUDE ARM", "自动驾驶高度预位"), + ("AUTOPILOT ATTITUDE HOLD", "自动驾驶姿态保持"), + ("AUTOPILOT GLIDESLOPE HOLD", "自动驾驶下滑道保持"), + ("AUTOPILOT APPROACH HOLD", "自动驾驶进近保持"), + ("AUTOPILOT BACKCOURSE HOLD", "自动驾驶背台保持"), + ("AUTOPILOT FLIGHT DIRECTOR ACTIVE", "飞行指引仪激活"), + ("AUTOPILOT AIRSPEED HOLD", "自动驾驶空速保持"), + ("AUTOPILOT AIRSPEED ARM", "自动驾驶空速预位"), + ("AUTOPILOT YAW DAMPER", "偏航阻尼器"), + ("AUTOPILOT TAKEOFF POWER ACTIVE", "起飞功率激活"), + ("GEAR CENTER POSITION", "中央起落架位置"), + ("GEAR LEFT POSITION", "左起落架位置"), + ("GEAR RIGHT POSITION", "右起落架位置"), + ("GEAR TAIL POSITION", "尾起落架位置"), + ("GEAR AUX POSITION", "辅助起落架位置"), + ("GEAR ANIMATION POSITION", "起落架动画位置"), + ("GEAR TOTAL PCT EXTENDED", "起落架总放出百分比"), + ("GEAR HANDLE POSITION", "起落架手柄位置"), + ("GEAR HYDRAULIC PRESSURE", "起落架液压压力"), + ("GEAR EMERGENCY HANDLE POSITION", "起落架应急手柄位置"), + ("GEAR WARNING", "起落架警告"), + ("GEAR WARNING INDICATOR", "起落架警告指示"), + ("GEAR IS ON GROUND", "起落架接地"), + ("WATER RUDDER HANDLE POSITION", "水舵手柄位置"), + ("TAILHOOK POSITION", "尾钩位置"), + ("TAILHOOK HANDLE", "尾钩手柄"), + ("EXIT OPEN", "出口打开"), + ("EXIT TYPE", "出口类型"), + ("ROTOR RPM PCT", "旋翼转速百分比"), + ("ROTOR ROTATION ANGLE", "旋翼旋转角"), + ("COLLECTIVE POSITION", "总距杆位置"), + ("HELICOPTER MASTER THROTTLE", "直升机主油门"), + ("SLING OBJECT ATTACHED", "吊挂物体已连接"), + ("SLING CABLE BROKEN", "吊索断裂"), + ("SLING CABLE EXTENDED LENGTH", "吊索伸出长度"), + ("TOW RELEASE HANDLE", "拖曳释放手柄"), + ("TOW CONNECTION", "拖曳连接"), + ("CRASH SEQUENCE", "坠毁序列"), + ("CRASH FLAG", "坠毁标志"), + ("AMBIENT WIND", "环境风"), + ("AMBIENT VISIBILITY", "能见度"), + ("AMBIENT PRECIP STATE", "降水状态"), + ("AMBIENT CLOUD COVERAGE", "云量"), + ("AMBIENT DENSITY", "空气密度"), + ("AMBIENT TEMPERATURE", "环境温度"), + ("AMBIENT DEWPOINT", "露点温度"), + ("SEA LEVEL PRESSURE", "海平面气压"), + ("STANDARD ATM TEMPERATURE", "标准大气温度"), + ("TOTAL AIR TEMPERATURE", "总温"), + ("ZULU TIME", "世界时"), + ("ZULU DAY OF YEAR", "世界时年内天数"), + ("ZULU DAY OF MONTH", "世界时日期"), + ("ZULU MONTH OF YEAR", "世界时月份"), + ("LOCAL TIME", "本地时间"), + ("TIME OF DAY", "时段"), + ("TIME ZONE OFFSET", "时区偏移"), + ("SIMULATION RATE", "模拟速率"), + ("UNITS OF MEASURE", "单位制"), + ("AI DESIRED SPEED", "AI 目标速度"), + ("AI CURRENT MANOEUVRE", "AI 当前机动"), + ("AI DESIRED MANOEUVRE", "AI 目标机动"), + ("AI GROUNDTURNTIME", "AI 地面转弯时间"), +] + +NAME_PARTS = { + "NUMBER": "数量", "OF": "", "ENGINES": "发动机", "ENGINE": "发动机", "CONTROL": "控制", + "SELECT": "选择", "THROTTLE": "油门", "LOWER": "下限", "LIMIT": "限制", "TYPE": "类型", + "MASTER": "主", "IGNITION": "点火", "SWITCH": "开关", "GENERAL": "通用", "ENG": "发动机", + "COMBUSTION": "燃烧", "ALTERNATOR": "交流发电机", "FUEL": "燃油", "PUMP": "泵", "ON": "开", + "RPM": "转速", "PCT": "百分比", "MAX": "最大", "REACHED": "达到", "LEVER": "杆", + "POSITION": "位置", "MIXTURE": "混合比", "PROPELLER": "螺旋桨", "STARTER": "起动机", + "EXHAUST": "排气", "GAS": "燃气", "TEMPERATURE": "温度", "OIL": "滑油", "PRESSURE": "压力", + "LEAKED": "泄漏", "PERCENT": "百分比", "SOUND": "声响", "DAMAGE": "损伤", "FAILED": "故障", + "GENERATOR": "发电机", "ACTIVE": "激活", "ANTI": "防", "ICE": "冰", "VALVE": "阀门", + "ELAPSED": "累计", "TIME": "时间", "RECIP": "往复式", "COWL": "整流罩", "FLAP": "襟翼", + "PRIMER": "注油器", "MANIFOLD": "歧管", "ALTERNATE": "备用", "AIR": "空气", "COOLANT": "冷却液", + "RESERVOIR": "储液罐", "LEFT": "左", "RIGHT": "右", "MAGNETO": "磁电机", "BRAKE": "制动", + "POWER": "功率", "TORQUE": "扭矩", "TURBOCHARGER": "涡轮增压器", "EMERGENCY": "紧急", + "BOOST": "增压", "WASTEGATE": "废气门", "TURBINE": "涡轮", "INLET": "进气", "CYLINDER": "气缸", + "HEAD": "缸盖", "RADIATOR": "散热器", "AVAILABLE": "可用", "FLOW": "流量", "TANK": "油箱", + "SELECTOR": "选择器", "TANKS": "油箱", "USED": "使用", "CARBURETOR": "化油器", "RATIO": "比", + "TURB": "涡轮", "N1": "N1", "N2": "N2", "CORRECTED": "修正", "FF": "燃油流量", "ITT": "ITT", + "AFTERBURNER": "加力燃烧室", "JET": "喷气", "THRUST": "推力", "BLEED": "引气", "REVERSE": "反推", + "NOZZLE": "喷管", "VIBRATION": "振动", "ANIMATION": "动画", "FIRE": "着火", "BETA": "桨距角", + "FEATHERING": "顺桨", "INHIBIT": "抑制", "FEATHERED": "顺桨", "SYNC": "同步", "DELTA": "增量", + "AUTO": "自动", "FEATHER": "顺桨", "ARMED": "预位", "PANEL": "面板", "DEICE": "除冰", + "HYDRAULIC": "液压", "QUANTITY": "数量", "SCALER": "比例", "PRIMARY": "主", "STAGE": "级", + "CENTER": "中央", "CENTER2": "中央2", "CENTER3": "中央3", "MAIN": "主", "AUX": "辅助", + "TIP": "翼尖", "EXTERNAL": "外部", "LEVEL": "液位", "CAPACITY": "容量", "CROSS": "串", + "FEED": "油", "TRANSFER": "传输", "TOTAL": "总", "WEIGHT": "重量", "LIGHT": "灯光", + "STATES": "状态", "LANDING": "着陆", "TAXI": "滑行", "STROBE": "频闪", "BEACON": "信标", + "NAV": "导航", "RECOGNITION": "识别", "WING": "机翼", "LOGO": "标志", "CABIN": "客舱", + "PBH": "俯仰/坡度/航向", "PLANE": "飞机", "LATITUDE": "纬度", "LONGITUDE": "经度", + "ALTITUDE": "高度", "PITCH": "俯仰", "BANK": "坡度", "HEADING": "航向", "TRUE": "真", + "MAGNETIC": "磁", "DEGREES": "度", "ABOVE": "高于", "GROUND": "地面", "VELOCITY": "速度", + "BODY": "机体", "WORLD": "世界", "ACCELERATION": "加速度", "ROTATION": "旋转", "FLEX": "弯曲", + "AIRSPEED": "空速", "INDICATED": "指示", "CALIBRATE": "校准", "VERTICAL": "垂直", "SPEED": "速度", + "KOHLSMAN": "气压基准", "SETTING": "设定", "MB": "毫巴", "WISKEY": "威士忌", "COMPASS": "罗盘", + "GYRO": "陀螺", "RATE": "速率", "PARTIAL": "部分", "ADF": "ADF", "COMM": "通讯", + "ALTIMETER": "高度表", "ATTITUDE": "姿态", "ELECTRICAL": "电气", "VACUUM": "真空", "PITOT": "皮托", + "TRANSPONDER": "应答机", "SUCTION": "吸力", "YOKE": "操纵杆", "X": "横向", "Y": "纵向", + "Z": "垂向", "RUDDER": "方向舵", "PEDAL": "踏板", "ELEVATOR": "升降舵", "AILERON": "副翼", + "TRIM": "配平", "INDICATOR": "指示器", "PARKING": "停留", "SPOILERS": "扰流板", "SPOILER": "扰流板", + "HANDLE": "手柄", "TRAILING": "后缘", "EDGE": "缘", "FLAPS": "襟翼", "LEADING": "前缘", + "DEFLECTION": "偏转", "AVERAGE": "平均", "AUTOPILOT": "自动驾驶", "ALT": "高度", "HOLD": "保持", + "GLIDESLOPE": "下滑道", "LOCALIZER": "航向道", "APPROACH": "进近", "BACKCOURSE": "背台", + "FLIGHT": "飞行", "DIRECTOR": "指引", "LOCK": "锁定", "GEAR": "起落架", "WHEEL": "机轮", + "STEER": "转向", "ANGLE": "角度", "GPS": "GPS", "WP": "航路点", "PREV": "上一", "NEXT": "下一", + "DESIRED": "目标", "TARGET": "目标", "DISTANCE": "距离", "VOR": "VOR", "DME": "DME", "ILS": "ILS", + "FREQUENCY": "频率", "STANDBY": "备用", "COURSE": "航向", "DEVIATION": "偏差", "MARKER": "指点标", + "BEARING": "方位", "OBS": "OBS", "TO": "至", "FROM": "来自", "CDI": "CDI", "HSI": "HSI", + "CAGED": "锁定", "ROSE": "罗盘", "AUDIO": "音频", "MUTE": "静音", "VOLUME": "音量", + "SQUAWK": "应答编码", "IDENT": "识别", "MODE": "模式", "COM": "通讯", "AMBIENT": "环境", + "WIND": "风", "VISIBILITY": "能见度", "PRECIP": "降水", "CLOUD": "云", "COVERAGE": "覆盖", + "DENSITY": "密度", "SEA": "海", "DEWPOINT": "露点", "RELATIVE": "相对", "HUMIDITY": "湿度", + "LOCAL": "本地", "DAY": "日", "MONTH": "月", "YEAR": "年", "WEEK": "周", "ZONE": "时区", + "OFFSET": "偏移", "SIMULATION": "模拟", "UNITS": "单位", "MEASURE": "度量", "HELICOPTER": "直升机", + "ROTOR": "旋翼", "COLLECTIVE": "总距", "CYCLIC": "周期变距", "BLADE": "桨叶", "SLING": "吊索", + "HOIST": "绞车", "CARRIER": "航母", "HOOK": "钩", "CABLE": "索", "CATAPULT": "弹射", + "RACE": "竞速", "LAP": "圈", "PENALTY": "罚时", "CHECKPOINT": "检查点", "AI": "AI", + "TITLE": "标题", "ATC": "ATC", "AIRLINE": "航空公司", "STRING": "字符串", "ID": "ID", + "USER": "用户", "CONTROLLED": "受控", "OBJECT": "对象", "REQUEST": "请求", "INDEX": "索引", + "AVIONICS": "航电", "SOUND": "音频", "RECIEVE": "接收", "ALL": "全部", "TRANSMIT": "发射", + "HAS": "具有", "SIGNAL": "信号", "STRENGTH": "强度", "RADIAL": "径向", "ERROR": "误差", + "GLIDE": "下滑", "SLOPE": "道", "GS": "下滑道", "FLAGS": "标志", "BACK": "背台", + "NAME": "名称", "VALID": "有效", "NEEDLE": "指针", "GSI": "GSI", "DMESPEED": "DME 速度", + "STATION": "台站", "TUNED": "调谐", "CURRENT": "当前", "DIRECTION": "方向", "NDB": "NDB", + "DESCRIPTIVE": "描述性", "RETRACT": "收放", "FLOAT": "浮筒", "EXTENDED": "伸出", "SIM": "模拟", + "DISABLED": "禁用", "SIGMA": "西格玛", "SQRT": "平方根", "LINEAR": "线性", "CL": "升力系数", + "ALPHA": "迎角", "ZERO": "零", "LIFT": "升力", "DECISION": "决断", "HEIGHT": "高度", + "SEMIBODY": "半机体", "LOADFACTOR": "载荷因子", "YDOT": "Y 导数", "SIMULATED": "模拟", + "RADIUS": "半径", "CATEGORY": "类别", "GROUNDTURNSPEED": "地面转弯速度", + "GROUNDCRUISESPEED": "地面巡航速度", "GROUNDTURNTIME": "地面转弯时间", "ZULU": "世界时", + "MANOEUVRE": "机动", "MANEUVER": "机动", "CRUISING": "巡航", "TURNING": "转弯", + "PICKUP": "拾取", "TRAFFIC": "交通", "ASSIGNED": "分配", "PARKING": "停机", "STATE": "状态", + "TRANSMISSION": "传动", "LOAD": "负载", "ELECTRICAL": "电气", "PICKUP": "拾取", "MODE": "模式", + "DUMP": "泄压", "DIFFERENTIAL": "压差", "GOAL": "目标", "BOTTLE": "灭火瓶", "DISCHARGED": "释放", + "SMOKING": "吸烟", "ALERT": "告警", "SEATBELTS": "安全带", "GPWS": "近地警告", + "PROXIMITY": "近地", "WARNING": "警告", "SYSTEM": "系统", "FREEZE": "冻结", "IS": "", + "ATTACHED": "已连接", "BROKEN": "断裂", "LENGTH": "长度", "RELEASE": "释放", "TOW": "拖曳", + "CONNECTION": "连接", "SEQUENCE": "序列", "FLAG": "标志", "CRASH": "坠毁", "STATIC": "静", + "CG": "重心", "DISK": "旋翼盘", "CONING": "锥度", "APU": "APU", "VOLTS": "电压", + "DETECTED": "探测", "PRESSURIZATION": "增压", "NO": "禁", "SEATBELTS": "安全带", + "PROPELLER": "螺旋桨", "BUG": "游标", "GPH": "加仑/小时", "PPH": "磅/小时", "GES": "受控设定", + "ANIMATION": "动画", "PRIMARY": "主", "NOZZLE": "喷管", "NUM": "数量", "HANDLE": "手柄", + "POSITIONS": "位置数", "NUM": "数量", "WATER": "水", "TAILHOOK": "尾钩", "EXIT": "出口", + "OPEN": "打开", "PRECIP": "降水", "STANDARD": "标准", "ATM": "大气", "TOTAL": "总", + "MANOEUVRE": "机动", "SPEED": "速度", "MANEUVER": "机动", +} + +# 英文说明整句翻译 +DESC_PHRASES = [ + ("Number of engines (minimum 0, maximum 4)", "发动机数量(最小0,最大4)"), + ("Selected engines (combination of bit flags); 1 = Engine 1; 2 = Engine 2; 4 = Engine 3; 8 = Engine 4", "选中的发动机(位标志组合);1=发动机1;2=发动机2;4=发动机3;8=发动机4"), + ("Percent throttle defining lower limit (negative for reverse thrust equipped airplanes)", "定义油门下限的百分比(反推飞机可为负值)"), + ("Engine type:; 0 = Piston; 1 = Jet; 2 = None; 3 = Helo(Bell) turbine; 4 = Unsupported; 5 = Turboprop", "发动机类型:0=活塞;1=喷气;2=无;3=贝尔涡轮(直升机);4=不支持;5=涡桨"), + ("Aircraft master ignition switch (grounds all engines magnetos)", "飞机主点火开关(接地所有发动机磁电机)"), + ("Percent of maximum capacity", "占最大容量的百分比"), + ("Percent of max rated rpm", "占最大额定转速的百分比"), + ("Percent of max throttle position", "油门杆最大行程的百分比"), + ("Percent of max mixture lever position", "混合比杆最大行程的百分比"), + ("Percent of max prop lever position", "桨距杆最大行程的百分比"), + ("Engine exhaust gas temperature.", "发动机排气温度"), + ("Engine oil pressure", "发动机滑油压力"), + ("Engine oil temperature", "发动机滑油温度"), + ("Engine fuel pressure", "发动机燃油压力"), + ("Combustion flag", "燃烧状态标志"), + ("Fuel pump switch", "燃油泵开关"), + ("Fuel pump on/off", "燃油泵开/关"), + ("Engine rpm", "发动机转速"), + ("Engine starter on/off", "发动机起动机开/关"), + ("Alternator (generator) switch", "交流发电机开关"), + ("Alternator (generator) on/off", "交流发电机开/关"), + ("Fail flag", "故障标志"), + ("Failure flag", "故障标志"), + ("On fire state", "着火状态"), + ("Magnetic variation", "磁偏角"), + ("Signal valid", "信号有效"), + ("Descriptive name", "描述性名称"), + ("If aircraft has retractable floats.", "飞机具有可收放浮筒"), + ("Is sim disabled", "模拟器是否禁用"), + ("Sigma sqrt", "西格玛平方根"), + ("Linear CL alpha", "线性升力系数迎角"), + ("Zero lift alpha", "零升力迎角"), + ("Design decision height", "设计决断高度"), + ("Semibody loadfactory ydot", "半机体法向载荷因子变化率"), + ("Simulated radius", "模拟半径"), + ("Cruising speed.", "巡航速度"), + ("Turning speed.", "转弯速度"), + ("GMT year", "世界时年份"), + ("Avionics switch state", "航电开关状态"), + ("Nav audio flag. Index of 1 or 2.", "导航音频标志(索引1或2)"), + ("DME audio flag", "DME 音频标志"), + ("ADF audio flag. Index of 0 or 1.", "ADF 音频标志(索引0或1)"), + ("Marker audio flag", "指点标音频标志"), + ("Audio panel com transmit state. Index of 1 or 2.", "音频面板通讯发射状态(索引1或2)"), + ("Flag if all Coms receiving", "所有通讯接收标志"), + ("Com frequency. Index is 1 or 2.", "通讯频率(索引1或2)"), + ("Com standby frequency. Index is 1 or 2.", "通讯备用频率(索引1或2)"), + ("Flag if Nav equipped on aircraft", "飞机是否配备导航设备"), + ("Nav active frequency. Index is 1 or 2.", "导航激活频率(索引1或2)"), + ("Nav standby frequency. Index is 1 or 2.", "导航备用频率(索引1或2)"), + ("Nav signal strength", "导航信号强度"), + ("Flag if Nav has signal", "导航是否有信号"), + ("Flag if tuned station is a localizer", "调谐台是否为航向道"), + ("Flag if tuned station has a DME", "调谐台是否有 DME"), + ("Flag if tuned station has a glideslope", "调谐台是否有下滑道"), + ("Magnetic variation of tuned nav station", "调谐导航台磁偏角"), + ("Radial that aircraft is on", "飞机所在径向"), + ("Difference between current radial and OBS tuned radial", "当前径向与 OBS 调谐径向之差"), + ("Localizer course heading", "航向道航道航向"), + ("Difference between current position and glideslope angle. Note that this provides 32 bit floating point precision, rather than the 8 bit integer precision of NAV GSI.", "当前位置与下滑道角之差(32位浮点精度)"), + ("CDI needle deflection (+/- 127)", "CDI 指针偏转(±127)"), + ("Glideslope needle deflection (+/- 119). Note that this provides only 8 bit precision, whereas NAV GLIDE SLOPE ERROR provides 32 bit floating point precision.", "下滑道指针偏转(±119,8位精度)"), + ("Glideslope flag", "下滑道标志"), + ("OBS setting. Index of 1 or 2.", "OBS 设定(索引1或2)"), + ("DME distance", "DME 距离"), + ("DME speed", "DME 速度"), + ("ADF frequency. Index of 1 or 2.", "ADF 频率(索引1或2)"), + ("ADF standby frequency", "ADF 备用频率"), + ("Current direction from NDB station", "相对 NDB 台当前方位"), + ("Current quantity in volume", "当前油量(容积)"), + ("Maximum capacity in volume", "最大容积容量"), + ("True airspeed", "真空速"), + ("Indicated airspeed", "指示空速"), + ("Vertical speed indication", "垂直速度指示"), + ("Altimeter indication", "高度表指示"), + ("Altimeter setting", "高度表气压设定"), + ("Gauge fail flag (0 = ok, 1 = fail, 2 = blank)", "仪表故障标志(0=正常,1=故障,2=空白)"), + ("Return true if the light is on.", "灯光开启时返回真"), + ("Obsolete", "已废弃"), + ("Active", "激活"), + ("4-digit code", "四位编码"), + ("Alternate static air source", "备用静压源"), + ("The trim position of the ailerons. Zero is fully retracted.", "副翼配平位置,零为完全收回"), + ("The trim position of the rudder. Zero is no trim.", "方向舵配平位置,零为无配平"), + ("One of:; 0: off; 1: complete; 3: reset; 4: pause; 11: start", "枚举:0=关闭;1=完成;3=重置;4=暂停;11=开始"), + ("One of:; 0: None; 2: Mountain; 4: General; 6: Building; 8: Splash; 10: Gear up; 12: Overstress; 14: Building; 16: Aircraft; 18: Fuel Truck", "枚举:0=无;2=撞山;4=一般;6=建筑物;8=落水;10=起落架收起;12=过载;14=建筑物;16=飞机;18=加油车"), + ("Position of tow release handle. 100 is fully deployed.", "拖曳释放手柄位置,100为完全放出"), + ("True if a towline is connected to both tow plane and glider.", "拖缆同时连接拖机与滑翔机时为真"), + ("Auxiliary power unit rpm, as a percentage", "APU 转速百分比"), + ("Auxiliary power unit starter, as a percentage", "APU 起动机百分比"), + ("Auxiliary power unit voltage", "APU 电压"), + ("True if APU generator switch on", "APU 发电机开关打开时为真"), + ("True if APU generator active", "APU 发电机激活时为真"), + ("True if APU on fire", "APU 着火时为真"), + ("The current altitude of the cabin pressurization..", "当前客舱增压高度"), + ("The set altitude of the cabin pressurization.", "客舱增压目标高度"), + ("The rate at which cabin pressurization changes.", "客舱增压高度变化率"), + ("The difference in pressure between the set altitude pressurization and the current pressurization.", "设定增压高度与当前增压高度之间的压差"), + ("True if the cabin pressurization dump switch is on.", "客舱增压泄压开关打开时为真"), + ("True if the fire bottle switch is on.", "灭火瓶开关打开时为真"), + ("True if the fire bottle is discharged.", "灭火瓶已释放时为真"), + ("True if the No Smoking switch is on.", "禁烟开关打开时为真"), + ("True if the Seatbelts switch is on.", "系安全带开关打开时为真"), + ("True if Ground Proximity Warning System installed.", "已安装近地警告系统时为真"), + ("True if the Ground Proximity Warning System is active", "近地警告系统激活时为真"), + ("True if the altitude of the aircraft is frozen.", "飞机高度冻结时为真"), + ("True if the attitude (pitch, bank and heading) of the aircraft is frozen.", "飞机姿态(俯仰、坡度、航向)冻结时为真"), + ("Main rotor pitch angle (helicopters only)", "主旋翼俯仰角(仅直升机)"), + ("Main rotor bank angle (helicopters only)", "主旋翼坡度角(仅直升机)"), + ("Main rotor pitch percent (helicopters only)", "主旋翼俯仰百分比(仅直升机)"), + ("Main rotor bank percent (helicopters only)", "主旋翼坡度百分比(仅直升机)"), + ("Main rotor coning percent (helicopters only)", "主旋翼锥度百分比(仅直升机)"), + ("Main rotor rotation angle (helicopters only)", "主旋翼旋转角(仅直升机)"), + ("Static CG to ground", "静重心离地高度"), + ("Static pitch", "静俯仰角"), + ("Time to make a 90 degree turn.", "90度转弯所需时间"), + ("Selected DME", "选中的 DME"), +] + +DESC_WORDS = { + "true": "真", "if": "若", "the": "", "is": "为", "on": "开", "off": "关", "flag": "标志", + "index": "索引", "of": "的", "or": "或", "and": "和", "a": "", "an": "", "to": "至", + "from": "来自", "for": "用于", "in": "在", "at": "在", "by": "由", "with": "带", + "has": "具有", "have": "具有", "equipped": "配备", "aircraft": "飞机", "engine": "发动机", + "fuel": "燃油", "oil": "滑油", "pressure": "压力", "temperature": "温度", "speed": "速度", + "position": "位置", "state": "状态", "switch": "开关", "active": "激活", "available": "可用", + "current": "当前", "maximum": "最大", "minimum": "最小", "percent": "百分比", "number": "数量", + "signal": "信号", "strength": "强度", "frequency": "频率", "standby": "备用", "nav": "导航", + "com": "通讯", "audio": "音频", "panel": "面板", "transmit": "发射", "receive": "接收", + "receiving": "接收", "all": "全部", "tuned": "调谐", "station": "台站", "localizer": "航向道", + "glideslope": "下滑道", "glide": "下滑", "slope": "道", "radial": "径向", "difference": "差值", + "between": "之间", "setting": "设定", "distance": "距离", "direction": "方向", "ndb": "NDB", + "adf": "ADF", "dme": "DME", "vor": "VOR", "ils": "ILS", "gps": "GPS", "obs": "OBS", + "needle": "指针", "deflection": "偏转", "course": "航道", "heading": "航向", "magnetic": "磁", + "variation": "偏角", "valid": "有效", "descriptive": "描述性", "name": "名称", "return": "返回", + "light": "灯光", "when": "当", "installed": "已安装", "system": "系统", "warning": "警告", + "ground": "地面", "proximity": "近地", "altitude": "高度", "attitude": "姿态", "pitch": "俯仰", + "bank": "坡度", "frozen": "冻结", "freeze": "冻结", "cabin": "客舱", "pressurization": "增压", + "rate": "速率", "set": "设定", "dump": "泄压", "fire": "火", "bottle": "瓶", "discharged": "释放", + "smoking": "吸烟", "seatbelts": "安全带", "rotor": "旋翼", "main": "主", "helicopters": "直升机", + "only": "仅", "static": "静", "cg": "重心", "tow": "拖曳", "release": "释放", "handle": "手柄", + "fully": "完全", "deployed": "放出", "connected": "连接", "both": "双方", "plane": "飞机", + "glider": "滑翔机", "towline": "拖缆", "auxiliary": "辅助", "power": "动力", "unit": "装置", + "generator": "发电机", "voltage": "电压", "starter": "起动机", "percentage": "百分比", "as": "作为", + "one": "一", "complete": "完成", "reset": "重置", "pause": "暂停", "start": "开始", "none": "无", + "mountain": "山", "building": "建筑", "splash": "落水", "gear": "起落架", "up": "收起", + "overstress": "过载", "truck": "车", "time": "时间", "make": "完成", "degree": "度", "turn": "转弯", + "seconds": "秒", "cruising": "巡航", "turning": "转弯", "year": "年", "gmt": "世界时", + "sim": "模拟", "disabled": "禁用", "sigma": "西格玛", "sqrt": "平方根", "linear": "线性", + "alpha": "迎角", "zero": "零", "lift": "升力", "decision": "决断", "height": "高度", + "design": "设计", "semibody": "半机体", "loadfactory": "载荷因子", "ydot": "Y导数", "simulated": "半径", + "radius": "半径", "category": "类别", "float": "浮筒", "retractable": "可收放", "floats": "浮筒", + "returns": "返回", "following": "以下", "bit": "位", "flags": "标志", "back": "背台", + "course": "航道", "localizer": "航向道", "tuned": "调谐", "note": "注意", "that": "", + "this": "此", "provides": "提供", "floating": "浮点", "point": "点", "precision": "精度", + "rather": "而非", "than": "比", "integer": "整数", "error": "误差", "angle": "角", +} + + +def translate_units(units: str) -> str: + return UNITS_ZH.get(units, units) + + +def _quality(text: str) -> float: + if not text: + return -999 + zh = len(re.findall(r"[\u4e00-\u9fff]", text)) + en_words = len(re.findall(r"[A-Za-z]{3,}", text)) + caps = len(re.findall(r"\b[A-Z]{2,}\b", text)) + return zh * 3 - en_words * 2 - caps * 4 + + +def translate_simvar_name(name: str) -> str: + base = name.strip() + if base in SIMVAR_ZH: + return SIMVAR_ZH[base] + has_index = ":index" in base.lower() + core = re.sub(r":index", "", base, flags=re.IGNORECASE).strip() + if core in SIMVAR_ZH: + return SIMVAR_ZH[core] + ("(索引)" if has_index else "") + + text = core + parts = [] + while text: + matched = False + for phrase, zh in sorted(NAME_COMPOUNDS, key=lambda x: len(x[0]), reverse=True): + if text.startswith(phrase + " ") or text == phrase: + parts.append(zh) + text = text[len(phrase):].strip() + matched = True + break + if not matched: + tok = text.split(" ", 1)[0] + zh = NAME_PARTS.get(tok, "") + parts.append(zh if zh else tok) + text = text[len(tok):].strip() + + result = "".join(p for p in parts if p) + if has_index and "索引" not in result: + result += "(索引)" + return result if result else core + + +def _translate_enum_tail(text: str) -> str: + text = re.sub(r"\b0\s*=\s*", "0=", text) + text = re.sub(r";\s*", ";", text) + mapping = { + "Piston": "活塞", "Jet": "喷气", "None": "无", "Unsupported": "不支持", + "Turboprop": "涡桨", "Day": "白天", "Dusk/Dawn": "黄昏/黎明", "Night": "夜晚", + "English": "英制", "Metric": "公制", "ok": "正常", "fail": "故障", "blank": "空白", + "off": "关闭", "complete": "完成", "reset": "重置", "pause": "暂停", "start": "开始", + "Mountain": "撞山", "General": "一般", "Building": "建筑物", "Splash": "落水", + "Gear up": "起落架收起", "Overstress": "过载", "Aircraft": "飞机", "Fuel Truck": "加油车", + } + for en, zh in mapping.items(): + text = text.replace(en, zh) + return text + + +def translate_description(desc: str) -> str: + if not desc or not desc.strip(): + return "" + text = desc.strip() + for en, zh in sorted(DESC_PHRASES, key=lambda x: len(x[0]), reverse=True): + if text == en: + return zh + + # 模式规则 + m = re.match(r"^True if (.+?)\.?$", text, re.I) + if m: + inner = _translate_free_text(m.group(1)) + return f"当{inner}时为真" + m = re.match(r"^Flag if (.+?)\.?$", text, re.I) + if m: + inner = _translate_free_text(m.group(1)) + return f"{inner}标志" + m = re.match(r"^Return true if (.+?)\.?$", text, re.I) + if m: + inner = _translate_free_text(m.group(1)) + return f"当{inner}时返回真" + m = re.match(r"^(.+?)\. Index of (\d+) or (\d+)\.?$", text, re.I) + if m: + return f"{_translate_free_text(m.group(1))}(索引{m.group(2)}或{m.group(3)})" + m = re.match(r"^(.+?)\. Index is (\d+) or (\d+)\.?$", text, re.I) + if m: + return f"{_translate_free_text(m.group(1))}(索引{m.group(2)}或{m.group(3)})" + + result = text + for en, zh in sorted(DESC_PHRASES, key=lambda x: len(x[0]), reverse=True): + if en in result: + result = result.replace(en, zh) + if "0 =" in result or "1 =" in result: + result = _translate_enum_tail(result) + if _quality(result) > 2: + return result + return _translate_free_text(text) + + +def _translate_free_text(text: str) -> str: + tokens = re.findall(r"[A-Za-z0-9]+|[^A-Za-z0-9\s]+", text) + out = [] + for tok in tokens: + key = tok.lower() + if key in DESC_WORDS and DESC_WORDS[key]: + out.append(DESC_WORDS[key]) + elif tok.upper() in NAME_PARTS and NAME_PARTS[tok.upper()]: + out.append(NAME_PARTS[tok.upper()]) + elif tok.isdigit(): + out.append(tok) + else: + out.append(tok) + joined = "".join(out) + joined = re.sub(r"\s+", "", joined) + return joined if joined else text + + +def best_chinese(sim_name: str, desc: str) -> str: + if sim_name in SIMVAR_ZH: + zh = SIMVAR_ZH[sim_name] + if desc and ("0 =" in desc or "1 =" in desc): + enum_part = translate_description(desc) + if enum_part and enum_part != desc: + return zh + ";" + enum_part.split(";", 1)[-1] if ";" in enum_part else zh + return zh + + # 英文说明有精确短语匹配时优先使用 + if desc and desc.strip(): + for en, zh in DESC_PHRASES: + if desc.strip() == en: + has_index = ":index" in sim_name.lower() + return zh + ("(索引)" if has_index and "索引" not in zh else "") + + zh_name = translate_simvar_name(sim_name) + zh_desc = translate_description(desc) + + if not desc or not desc.strip(): + return zh_name + + q_name = _quality(zh_name) + q_desc = _quality(zh_desc) + + if "0 =" in desc or "1 =" in desc or "One of:" in desc: + if q_desc >= q_name - 5: + return zh_desc + + if q_desc > q_name + 3: + return zh_desc + if q_name >= q_desc: + return zh_name + return zh_desc diff --git a/Docs/status.md b/Docs/status.md new file mode 100644 index 00000000..047be968 --- /dev/null +++ b/Docs/status.md @@ -0,0 +1,559 @@ +# Flight Assistant 状态标签体系分析报告 + +> 生成日期:2026-08-28 +> 更新日期:2026-08-28(对齐 `RuntimeWatch` 命名可读化) +> 范围:模拟器暂停/恢复暂停、坠毁检测、任务名称、任务模式,以及所有相关 UI 状态标签 + +--- + +## 1. 概述 + +Flight Assistant 是一款基于 **Python + PySide2** 的 FSX 竞速训练桌面应用。业务编排仍在 `AppController`(`app_controller.py`),但**运行时状态判别**已集中到 `models/runtime_watch.py` 的 `RuntimeWatch`:监测阶段、暂停判定、坠毁锁存与轮询门控均围绕该对象上的判别链工作。 + +会话模式、任务名称、影子机对象与 UI 呈现仍由 `AppController` 持有并驱动;UI 通过 Qt 信号(`_UiBridge`)和页面方法更新。 + +### 1.1 五类状态标签 + +| 类别 | UI 呈现 | 内部状态载体 | +|------|---------|-------------| +| **运行状态消息** | `StatusBar`(训练页底部彩色文字) | `train.set_status(text, kind)` | +| **会话模式标签** | `LapBoard.mode_label`(Mission Mode 等) | `AppController._session_mode` | +| **任务名称标签** | `LapBoard.mission_label` | `AppController._mission_title` | +| **监测按钮状态** | 开始监测 / 停止监测 / 保存当前数据 | `TrainingAssistPage._monitor_mode` + `_mission_ready`(由 `_runtime` 阶段推导) | +| **启动检查状态** | `SimCheckPanel` 信号灯 | 独立的检查流程状态 | + +### 1.2 重构后的职责划分 + +| 模块 | 职责 | +|------|------| +| `models/runtime_watch.py` | 三条核心判别链:监测阶段、暂停、坠毁锁存;轮询/指标定时器门控 | +| `app_controller.py` | 编排业务:监测启停、崩溃处理、会话信息、跟飞、UI 状态文案 | +| `models/shadow_plane.py` | 轨迹录制 `paused`、影子机回放 `_replay_paused`(由 pause 链同步) | +| `services/simulator.py` | `simulation_clock_advancing`、`.flt` 解析等纯函数 | + +--- + +## 2. RuntimeWatch:运行时状态中枢 + +### 2.1 三条不可合并的判别链 + +```mermaid +flowchart TB + subgraph Watch["RuntimeWatch(self._runtime)"] + PHASE["MonitorPhase
IDLE / WAITING / RECORDING"] + PAUSE["PauseDetector
_last_sim_time + is_paused"] + CRASH["crash_latched
+ aircraft_has_crashed(aq)"] + end + + PHASE --> Q1["is_active / is_waiting / is_recording"] + PAUSE --> Q2["_poll 每轮 pause_detector.refresh(aq, sm)"] + CRASH --> Q3["_handle_detected_crash"] + + Q1 --> CTRL["AppController 业务分支"] + Q2 --> CTRL + Q3 --> CTRL +``` + +| 判别链 | 类型 | 为何不可合并 | +|--------|------|----------------| +| **监测阶段** `MonitorPhase` | 有限状态机(三态互斥) | 描述应用自身生命周期,不能由 simvar 直接读出 | +| **暂停** `PauseDetector` | 每轮由 ABSOLUTE TIME 刷新 | 需要跨 poll 的时钟基准;结果可缓存供同轮只读 | +| **坠毁锁存** `crash_latched` | 一次性闩锁 | simvar 可直接读,但“是否已处理”必须锁存,避免重复停机 | + +已移除、改由上述链条推导或内聚的标志: + +| 旧标志(已删除) | 现归属 | +|------------------|--------| +| `_monitoring` + `_awaiting_motion` | `MonitorPhase`(IDLE/WAITING/RECORDING) | +| `_recording_paused` | 边沿检测:`paused` + `TrajectoryRecorder.paused` | +| `_last_sim_clock` | `PauseDetector._last_sim_time` | +| `_crash_handled` | `RuntimeWatch.crash_latched` | +| `_sync_crash_watch` / `_sync_metrics_timer` / `_should_refresh_metrics` | `_refresh_runtime_timers()` + `should_poll` / `should_refresh_metrics` | + +### 2.2 MonitorPhase 状态机 + +```mermaid +stateDiagram-v2 + [*] --> IDLE + + IDLE --> WAITING: enter_waiting()\n_start_monitor_waiting + WAITING --> RECORDING: enter_recording()\n_start_recording + RECORDING --> IDLE: return_to_idle()\n_stop_monitoring + WAITING --> IDLE: return_to_idle()\n_stop_monitoring + + IDLE --> IDLE: reset()\nsim lost / close + WAITING --> IDLE: reset() + RECORDING --> IDLE: reset() + + note right of WAITING + is_active=True + is_waiting=True + enter_waiting 会 clear pause_detector + 并 crash_latched=False + end note + + note right of RECORDING + is_recording=True + 正式采样与图表写入 + end note + + note right of IDLE + return_to_idle: + 不清除 crash_latched + reset: + 清除 crash_latched + end note +``` + +查询 API(业务只读这些,不直接写枚举): + +| 方法 | 真值条件 | +|------|----------| +| `is_active()` | `phase != IDLE` | +| `is_waiting()` | `phase == WAITING` | +| `is_recording()` | `phase == RECORDING` | + +阶段迁移 API: + +| 方法 | 效果 | +|------|------| +| `enter_waiting()` | → WAITING;`pause_detector.clear()`;`crash_latched=False` | +| `enter_recording()` | → RECORDING | +| `return_to_idle()` | → IDLE;`pause_detector.clear()`;**保留** `crash_latched` | +| `reset()` | → IDLE;`pause_detector.clear()`;`crash_latched=False` | + +### 2.3 架构总览(当前) + +```mermaid +flowchart TB + subgraph SimConnect["SimConnect 层"] + SM["sm.paused / events"] + AQ["aq: ABSOLUTE TIME / CRASH_*"] + EVT["FLIGHT_LOADED / MISSION_COMPLETED / MP_*"] + end + + subgraph Watch["models/runtime_watch.py"] + RW["RuntimeWatch"] + RW --> PHASE["MonitorPhase"] + RW --> SPT["PauseDetector"] + RW --> CH["crash_latched"] + ICA["aircraft_has_crashed(aq)"] + end + + subgraph Controller["AppController"] + SES["_session_mode / _mission_title / _mp_active"] + SHD["_shadow_controller"] + TR["_trajectory_recorder"] + LIVE["_live_readouts_enabled"] + CHART["_chart_time_origin"] + end + + subgraph UI["UI"] + SB["StatusBar"] + LB["LapBoard"] + MB["监测按钮"] + end + + AQ --> SPT + AQ --> ICA + SM --> SPT + EVT --> Controller + Controller --> RW + RW --> Controller + Controller --> TR + Controller --> SHD + Controller --> UI +``` + +### 2.4 轮询门控 + +两个 `QTimer` 仍由 `AppController` 持有,启停统一走 `_refresh_runtime_timers()`: + +| 定时器 | 回调 | 门控 | +|--------|------|------| +| `_poll_timer` | `_poll()` | `should_poll(shadow_active, live_readouts)` = 监测中 **或** 影子机活跃 **或** 实时读数 | +| `_metrics_timer` | `_poll_metrics()` | `should_refresh_metrics(live_readouts)` = 监测中 **或** 实时读数 | + +--- + +## 3. 模拟器暂停与恢复暂停 + +### 3.1 设计原则 + +1. **主动暂停**:`_request_sim_pause()` 发送 `PAUSE_ON`。 +2. **被动检测**:`PauseDetector.update(aq, sm)` 以 `ABSOLUTE TIME` 是否推进为准(FSX 暂停时 `SIMULATION_RATE` 仍可能为 1)。 +3. **无自动恢复**:从不发送 `PAUSE_OFF`;用户在 FSX 解除暂停后,应用仅检测并同步。 +4. **同步冻结**:轨迹录制、图表 X 轴、影子机回放均随 `paused` 联动。 + +### 3.2 暂停判定(PauseDetector) + +每轮 `_poll` 首选: + +```text +paused = self._runtime.pause.update(self.aq, self.sm) +``` + +逻辑与历史 `_is_paused` 一致: + +``` +若 sm 为空 → paused=False +若 ABSOLUTE TIME 在推进 → 纠正 sm.paused=False,is_paused=False +否则若 sm.paused → is_paused=True +否则(时钟未推进)→ is_paused=True +``` + +时钟推进由 `services.simulator.simulation_clock_advancing(aq, last_clock)` 完成;基准保存在 `PauseDetector._last_sim_time`(`enter_waiting` / `return_to_idle` / `reset` 时 `pause_detector.clear()`)。 + +### 3.3 录制暂停:边沿检测(无独立 `_recording_paused`) + +录制同步暂停**不再**在 Controller 上维护布尔副本,而用: + +| 条件 | 动作 | +|------|------| +| `paused` 且 `recorder.active` 且 `not recorder.paused` | `set_paused(True)` + StatusBar「模拟器已暂停,监测同步暂停」 | +| `not paused` 且 `recorder.paused` | `set_paused(False)` + StatusBar「监测中...」 | +| `is_recording()` | `sample_from_aq`(内部若 `paused` 则跳过采样) | + +影子机仍通过 `_apply_shadow_pause(paused)` → `set_replay_paused(paused)`。 + +### 3.4 暂停相关状态一览 + +| 变量 | 位置 | 含义 | +|------|------|------| +| `PauseDetector.is_paused` / `_last_sim_time` | `runtime_watch.py` | 本轮暂停结论与时钟基准 | +| `sm.paused` | `SimConnect.py` | 事件侧标志;时钟推进时可被纠正 | +| `TrajectoryRecorder.paused` | `shadow_plane.py` | 是否跳过采样(边沿由 poll 驱动) | +| `ShadowPlaneController._replay_paused` | `shadow_plane.py` | 回放进度是否冻结 | + +### 3.5 监测 + 暂停联合流转 + +```mermaid +stateDiagram-v2 + [*] --> IDLE + + IDLE --> WAITING: _start_monitor_waiting\nPAUSE_ON + enter_waiting + WAITING --> WAITING: paused\n「等待解除暂停...」 + WAITING --> RECORDING: !paused\n「请移动飞机」→ _start_recording + + RECORDING --> RECORD_PAUSED: paused 边沿\nrecorder.set_paused(True) + RECORD_PAUSED --> RECORDING: !paused 边沿\nrecorder.set_paused(False) + + RECORDING --> IDLE: 用户停止 / 坠毁 halt + WAITING --> IDLE: 用户停止 / 坠毁 halt + RECORD_PAUSED --> IDLE: 用户停止 / 坠毁 halt +``` + +说明:`RECORD_PAUSED` 不是 `MonitorPhase` 的第四态;阶段仍为 `RECORDING`,仅录制器/影子机处于暂停边沿状态。 + +### 3.6 主动暂停触发场景 + +| 场景 | 调用链 | 目的 | +|------|--------|------| +| 开始监测(当前任务) | `_request_sim_pause` → `_start_monitor_waiting` | 进入 WAITING,等待用户解除暂停 | +| 开始监测(重置任务) | `_reset_task_and_request_pause`(`SITUATION_RESET` + `PAUSE_ON`) | 重置后保持暂停 | +| 影子机跟飞准备 | `_request_sim_pause` / 跟飞完成后再次暂停 | 跟飞前/后冻结 | +| 冷舱重置完成后 | `_request_sim_pause` | 冷舱后保持暂停 | + +### 3.7 影子机暂停 + +`launch_shadow_follow()` 启动后立即 `set_replay_paused(True)`;主线程 `_poll` / 跟飞完成回调用当前 `paused` 持续 `_apply_shadow_pause`。回放线程仅在 `_replay_paused` 为假时累加 `replay_elapsed`。 + +--- + +## 4. 坠毁检测与处理 + +### 4.1 检测(可读、无状态) + +`aircraft_has_crashed(aq)`(`runtime_watch.py`):`CRASH_FLAG` 或 `CRASH_SEQUENCE` 任意非零即真。 + +- `CRASH_SEQUENCE=4` 文档含义为 pause,当前**仍按坠毁处理**(与历史行为一致)。 +- `SIMCONNECT_MISSION_CRASHED` 不用于此路径。 + +### 4.2 处理(锁存 + 编排) + +```mermaid +flowchart TD + POLL["_poll 首选"] + POLL --> LATCH{"crash_latched?"} + LATCH -->|是| SKIP[跳过] + LATCH -->|否| RAW{"aircraft_has_crashed(aq)?"} + RAW -->|否| CONT[继续 poll] + RAW -->|是| ACTIVE{"is_active 或影子机活跃?"} + ACTIVE -->|否| IGNORE[忽略闲置坠毁
不置 crash_latched] + ACTIVE -->|是| SET["crash_latched = True"] + SET --> MON{"is_active?"} + MON -->|是| HALT["_stop_monitoring
return_to_idle"] + MON -->|否| SH["_stop_shadow_soft"] + HALT --> UI[StatusBar 坠毁文案] + SH --> UI + UI --> SYNC["_refresh_runtime_timers"] +``` + +要点: + +- 锁存在 `RuntimeWatch.crash_latched`。 +- `_stop_monitoring` → `return_to_idle()`:**不**清除 `crash_latched`。 +- `_start_monitor_waiting` → `enter_waiting()`:清除锁存。 +- `_handle_sim_lost` / `_on_close` → `reset()`:清除锁存。 + +--- + +## 5. 任务名称(Mission Title) + +仍由 `AppController._mission_title` 持有(非 RuntimeWatch)。 + +### 5.1 数据流 + +```mermaid +flowchart LR + FLT[".flt [Main].Title"] + PARSE["parse_mission_title"] + TITLE["_mission_title"] + UI["LapBoard.mission_label"] + GATE["开始监测门控"] + LIVE["_live_readouts_enabled"] + + FLT --> PARSE --> TITLE --> UI + TITLE --> GATE + TITLE --> LIVE +``` + +### 5.2 解析与事件 + +`parse_mission_title`:读 Title;非 FreeFlight 且非占位符直接用;否则沿 `OriginalFlight` 最多 3 层;过滤 Previous Flight 等占位符。 + +| 事件 | 效果 | +|------|------| +| `EVENT_FLIGHT_LOADED` | 更新标题与会话模式 | +| `EVENT_MISSION_COMPLETED`(非 MP) | 清空标题,模式 → freeflight | +| 开始监测 | `_refresh_session_info` | + +### 5.3 耦合 + +- 标题非空 → 启用「开始监测」、可开实时读数。 +- 标题清空且 `not is_active()` → 关闭实时读数。 +- `_on_start_monitor` 入口仍校验标题非空。 + +--- + +## 6. 任务模式 + +### 6.1 会话模式(持久) + +| `_session_mode` | LapBoard | 颜色 | +|-----------------|----------|------| +| `freeflight` | Free Flight Mode | 绿 | +| `mission_sp` | Mission Mode | 蓝 | +| `mission_mp` | Multiplay Mode | 橙 | + +```mermaid +stateDiagram-v2 + [*] --> freeflight + freeflight --> mission_sp: 任务 .flt / 监测中无 freeflight + mission_sp --> mission_mp: MP 启动 + mission_mp --> mission_sp: MP 结束 + mission_sp --> freeflight: 任务完成 / 断连 +``` + +与监测耦合:解析为 freeflight 但 `_runtime.is_active()` 时,强制 `mission_sp`/`mission_mp`。`mission_mp` 下启动对话框禁用「重置任务」与影子机跟飞。 + +### 6.2 启动方式(瞬时) + +对话框 `settings["task"]`:`current` / `reset`,不写入 RuntimeWatch。 + +--- + +## 7. StatusBar 与监测按钮 + +### 7.1 kind 颜色 + +| kind | 颜色 | 语义 | +|------|------|------| +| `ok` | `#2ecc71` | 成功/正常 | +| `warn` | `#f39c12` | 暂停/停止/坠毁 | +| `error` | `#e74c3c` | 失败 | +| `muted` | `#7A9BB8` | 进行中 | + +### 7.2 与 MonitorPhase / 暂停相关的关键文案 + +| 消息 | kind | 条件(当前实现) | +|------|------|------------------| +| 等待解除暂停状态后开始监测... | warn | WAITING 且 `paused` | +| 请移动飞机以开始监测 | warn | WAITING 且解除暂停后进入录制前 | +| 监测中... | ok | 进入 RECORDING / 录制从暂停边沿恢复 | +| 模拟器已暂停,监测同步暂停 | warn | RECORDING 路径上 `paused` 上升沿且 recorder 活跃 | +| 检测到坠毁,已停止… | warn | 坠毁处理完成 | +| 监测已停止 / 监测已停止,跟飞已结束 | warn | 用户停止 | + +(连接、冷舱、倍速、圈时、轨迹保存等文案逻辑未变,仍由 Controller 直接 `set_status`。) + +### 7.3 监测按钮(由 `_runtime` 推导) + +| `_runtime` 状态 | `set_monitor_button` | +|---------------|----------------------| +| `is_recording()` | `stop` | +| `is_waiting()`(WAITING) | `pending` | +| IDLE 且有未保存轨迹 | `save` | +| 其他 IDLE | `None`(开始监测) | + +「开始监测」可用性仍由 `_mission_title` → `_mission_ready` 门控。 + +--- + +## 8. `_poll` 执行时序与优先级 + +时序与重构前保持一致,仅状态源改为 `_runtime`: + +```mermaid +flowchart TD + A["_poll 入口"] --> B{"closing / !aq?"} + B -->|是| Z[return] + B -->|否| C["_handle_detected_crash"] + C -->|已处理| Z + C -->|否| D["pause_detector.refresh → paused"] + D --> E["_apply_shadow_pause"] + E --> F{"is_active?"} + F -->|否| G[断连检测 / _refresh_runtime_timers] + F -->|是| H{suspend / quit?} + H --> I{"is_waiting?"} + I -->|是且 paused| J[等待解除暂停 / return] + I -->|是且未 paused| K["_start_recording → RECORDING"] + I -->|否| L{paused?} + L -->|是| M[录制器暂停边沿 / return] + L -->|否| N[录制器恢复边沿] + N --> O[图表 + 若 is_recording 则采样] + O --> P["_refresh_runtime_timers"] +``` + +| 优先级 | 条件 | 行为 | +|--------|------|------| +| 1 | closing / 无 aq | 跳过 | +| 2 | 坠毁处理成功 | 本轮结束 | +| 3 | 非监测 | 仅断连与定时器同步 | +| 4 | WAITING + paused | 等待解除暂停 | +| 5 | WAITING + 未 paused | `_start_recording` | +| 6 | RECORDING 路径 + paused | 同步暂停录制并 return | +| 7 | 正常 RECORDING | 采样与刷新 | + +--- + +## 9. 核心耦合关系(当前) + +```mermaid +flowchart TB + TITLE["_mission_title"] + MODE["_session_mode"] + PHASE["MonitorPhase"] + PAUSE["PauseDetector.is_paused"] + CRASH["crash_latched"] + REC["TrajectoryRecorder.paused"] + SHP["ShadowPlane._replay_paused"] + + TITLE -->|门控| START[开始监测] + MODE -->|限制对话框| START + START --> PHASE + + PAUSE -->|边沿| REC + PAUSE -->|每轮同步| SHP + PAUSE --> PHASE + + PHASE -->|监测中| CRASH + CRASH -->|halt| PHASE +``` + +--- + +## 10. 关键代码位置 + +| 主题 | 文件 | 符号 | +|------|------|------| +| 运行时中枢 | `models/runtime_watch.py` | `RuntimeWatch`, `MonitorPhase`, `PauseDetector`, `aircraft_has_crashed` | +| 编排与 poll | `app_controller.py` | `_runtime`, `_poll`, `_start_monitor_waiting`, `_stop_monitoring`, `_refresh_runtime_timers` | +| 时钟推进 | `services/simulator.py` | `simulation_clock_advancing`, `chart_elapsed_time_s` | +| 任务解析 | `services/simulator.py` | `parse_mission_title`, `parse_flt_mission_type` | +| 录制暂停 | `models/shadow_plane.py` | `TrajectoryRecorder.set_paused` / `paused` | +| 影子机暂停 | `models/shadow_plane.py` | `set_replay_paused` | +| StatusBar / LapBoard | `app_frame.py` | `StatusBar`, `LapBoard`, `StartMonitoringDialog` | +| 训练页按钮 | `ui/training_assist_page.py` | `set_monitor_button`, `set_start_monitor_mission_ready` | + +--- + +## 11. 设计注意点 + +1. **无自动解除暂停**:只发 `PAUSE_ON`。 +2. **ABSOLUTE TIME 优先于事件标志**:`PauseDetector` 以时钟为准,推进时可纠正 `sm.paused`。 +3. **RECORD 暂停不是第四阶段**:阶段仍为 RECORDING,录制/影子机用边沿与属性同步。 +4. **`return_to_idle` vs `reset`**:前者保留坠毁锁存;后者用于断连/关闭完整复位。 +5. **闲置坠毁忽略**:非监测且无影子机时不置 `crash_latched`。 +6. **`CRASH_SEQUENCE=4`**:仍可能与暂停语义混淆(历史行为保留)。 +7. **会话模式与跟飞限制**:`mission_mp` 禁用重置任务与影子机跟飞。 + +--- + +## 12. 状态变量速查(当前) + +| 变量 | 位置 | 说明 | +|------|------|------| +| `phase` | `RuntimeWatch` | IDLE / WAITING / RECORDING | +| `pause_detector` / `is_paused` | `PauseDetector` | 暂停判别链 | +| `crash_latched` | `RuntimeWatch` | 坠毁已处理闩锁 | +| `_session_mode` | `AppController` | freeflight / mission_sp / mission_mp | +| `_mission_title` | `AppController` | 任务标题 | +| `_mp_active` | `AppController` | 多人是否活跃 | +| `_live_readouts_enabled` | `AppController` | 实时读数 | +| `_chart_time_origin` | `AppController` | 图表时间原点 | +| `_shadow_controller` | `AppController` | 影子机控制器 | +| `TrajectoryRecorder.paused` | `shadow_plane` | 采样暂停 | +| `ShadowPlaneController._replay_paused` | `shadow_plane` | 回放冻结 | +| `_monitor_mode` / `_mission_ready` | `TrainingAssistPage` | 按钮 UI 态 | + +--- + +## 13. 相对上一版文档的变更摘要 + +| 项目 | 变更 | +|------|------| +| 状态中枢 | 由散落的 Controller 布尔 → `RuntimeWatch` 三条判别链 | +| 监测双布尔 | `_monitoring` + `_awaiting_motion` → `MonitorPhase`(IDLE/WAITING/RECORDING) | +| 录制暂停标志 | 删除 Controller `_recording_paused`,改用 recorder.paused 边沿 | +| 暂停判定入口 | `_is_sim_paused` → `_runtime.pause_detector.refresh` | +| 坠毁读数 | Controller 方法 → 模块函数 `aircraft_has_crashed` | +| 定时器同步 | `_sync_crash_watch` 等 → `_refresh_runtime_timers` | +| 命名可读化 | 见第 14 节对照表;业务语义 / 时序不变 | + +--- + +## 14. 命名对照(本次可读化重命名) + +### `runtime_watch.py` + +| 旧名 | 新名 | +|------|------| +| `MonitorPhase.PREP` / `RECORD` | `WAITING` / `RECORDING` | +| `SimPauseTracker` | `PauseDetector` | +| `sim_paused` / `_last_clock` / `update` / `reset` | `is_paused` / `_last_sim_time` / `refresh` / `clear` | +| `is_aircraft_crashed` | `aircraft_has_crashed` | +| `monitor_phase` / `pause` / `crash_handled` | `phase` / `pause_detector` / `crash_latched` | +| `is_monitoring` / `is_awaiting_motion` | `is_active` / `is_waiting` | +| `begin_prep` / `set_record` | `enter_waiting` / `enter_recording` | +| `stop_monitor_phase` / `reset_runtime` | `return_to_idle` / `reset` | +| `needs_poll_timer` / `needs_metrics_timer` | `should_poll` / `should_refresh_metrics` | + +### `app_controller.py` + +| 旧名 | 新名 | +|------|------| +| `_watch` | `_runtime` | +| `_prepare_monitoring` | `_start_monitor_waiting` | +| `_halt_monitoring` | `_stop_monitoring` | +| `_begin_recording` | `_start_recording` | +| `_handle_crash_if_detected` | `_handle_detected_crash` | +| `_ensure_sim_paused` | `_request_sim_pause` | +| `_reset_task_and_pause` | `_reset_task_and_request_pause` | +| `_sync_poll_timers` | `_refresh_runtime_timers` | +| `_sync_shadow_pause` | `_apply_shadow_pause` | +| `_shadow_active` | `_is_shadow_active` | + +--- + +*本报告依据 2026-08-28 代码静态分析更新,以 `models/runtime_watch.py` + `app_controller.py` 为准。* diff --git "a/Docs/\346\250\241\346\213\237\345\231\250\345\217\202\346\225\260\346\270\205\345\215\225.md" "b/Docs/\346\250\241\346\213\237\345\231\250\345\217\202\346\225\260\346\270\205\345\215\225.md" new file mode 100644 index 00000000..b6a28cf1 --- /dev/null +++ "b/Docs/\346\250\241\346\213\237\345\231\250\345\217\202\346\225\260\346\270\205\345\215\225.md" @@ -0,0 +1,1049 @@ +# 模拟器 Simulation Variables 参数清单 + +> 来源:`Docs/Simulation Variables.pdf`(FSX/SimConnect 官方仿真变量表),并与项目 `SimConnect/RequestList.py` 中的结构化定义对齐。 +> 补充说明见 `Docs/项目原理.md`。 + +**表格列说明:** 变量名 | 英文说明(官方原文) | 中文说明 | 单位 | 读写 + +## 读写规则说明 + +| 标记 | 含义 | 本项目用法 | +|---|---|---| +| **只读 (N)** | 只能通过 get / request_data 订阅读取 | `AircraftRequests.get(...)` 或 REST `GET /datapoint//get` | +| **可写 (Y)** | 可通过 set_data 直接写入数值 | `AircraftRequests.set(...)` 或 REST `POST /datapoint//set` | + +**交互补充(非变量写入):** + +- 许多开关/手柄(如自动驾驶开关、灯光开关)在变量表中为 **只读**,需通过 **SimConnect 事件**(`EventList.py` / `POST /event//trigger`)触发才能交互。 +- 带 `:index` 的变量需要指定系统索引(发动机从 0,通讯设备从 1)。 +- PDF 另有 **Program Data**(`P:`,如 `SIMULATION RATE`、`UNITS OF MEASURE`)与 **Units of Measurement** 单位表;本清单主体为 Aircraft/Environment 的 `A:` 仿真变量。 + +## 总览 + +- **变量总数**:830 +- **可直接写入 (Y)**:191 +- **只能读取 (N)**:639 + +| 分类 | 总数 | 可写 | 只读 | +|---|---:|---:|---:| +| 一、Aircraft Engine Data(发动机) | 110 | 40 | 70 | +| 二、Aircraft Fuel Data(燃油) | 48 | 22 | 26 | +| 三、Aircraft Lights Data(灯光) | 24 | 0 | 24 | +| 四、Aircraft Position and Speed Data(位置与速度) | 34 | 24 | 10 | +| 五、Aircraft Flight Instrumentation Data(飞行仪表) | 45 | 24 | 21 | +| 六、Aircraft Avionics Data(航电) | 115 | 4 | 111 | +| 七、Aircraft Controls Data(操纵/控制) | 48 | 18 | 30 | +| 八、Aircraft Autopilot Data(自动驾驶) | 38 | 0 | 38 | +| 九、Aircraft Landing Gear Data(起落架) | 54 | 6 | 48 | +| 十、Aircraft Environment Data(飞机环境) | 19 | 0 | 19 | +| 十一、Helicopter Specific Data(直升机) | 18 | 0 | 18 | +| 十二、Slings and Hoists(吊索/绞车) | 9 | 2 | 7 | +| 十三、Aircraft Miscellaneous Systems Data(杂项系统) | 50 | 24 | 26 | +| 十四、Miscellaneous Data(杂项) | 159 | 14 | 145 | +| 十五、Aircraft String Data(字符串) | 12 | 3 | 9 | +| 十六、AI Controlled Aircraft(AI 飞机) | 13 | 6 | 7 | +| 十七、Carrier Operations(航母作业) | 10 | 0 | 10 | +| 十八、Racing(竞速) | 10 | 4 | 6 | +| 十九、Environment Data(全局环境) | 14 | 0 | 14 | + +--- + +## 附录 A:Fuel Tank Selection(油箱选择枚举值) + +以下不是独立 SimVar,而是 `FUEL TANK SELECTOR` / `RECIP ENG FUEL TANK SELECTOR` 等枚举取值(文档章节 *Fuel Tank Selection*): + +| 枚举值 | 英文含义 | 中文含义 | +|---:|---|---| +| 0 | Off | 关闭 | +| 1 | All | 全部 | +| 2 | Left | 左 | +| 3 | Right | 右 | +| 4 | Left auxiliary | 左辅助 | +| 5 | Right auxiliary | 右辅助 | +| 6 | Center | 中央 | +| 7 | Center2 | 中央2 | +| 8 | Center3 | 中央3 | +| 9 | External1 | 外部1 | +| 10 | External2 | 外部2 | +| 11 | Right tip | 右翼尖 | +| 12 | Left tip | 左翼尖 | +| 13 | Crossfeed | 串油 | +| 14 | Crossfeed left to right | 左向右串油 | +| 15 | Crossfeed right to left | 右向左串油 | +| 16 | Both | 两侧 | +| 17 | External | 外部 | +| 18 | Isolate | 隔离 | +| 19 | Left main | 左主 | +| 20 | Right main | 右主 | + +## 附录 B:Program Data(程序数据,PDF 有、RequestList 未收录) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `SIMULATION RATE` | Time acceleration factor | 时间加速倍率 | Number | 只读 | +| `UNITS OF MEASURE` | Units of measure: 0=English; 1=Metric(alt feet); 2=Metric(alt meters) | 单位制:0=英制;1=公制(高度英尺);2=公制(高度米) | Enum | 只读 | + +--- + +## 一、Aircraft Engine Data(发动机) + +共 **110** 项(可写 40 / 只读 70) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `NUMBER OF ENGINES` | Number of engines (minimum 0, maximum 4) | 发动机数量(最小0,最大4) | Number(数值) | 只读 | +| `ENGINE CONTROL SELECT` | Selected engines (combination of bit flags); 1 = Engine 1; 2 = Engine 2; 4 = Engine 3; 8 = Engine 4 | 选中的发动机(位标志组合);1=发动机1;2=发动机2;4=发动机3;8=发动机4 | Mask(掩码) | **可写** | +| `THROTTLE LOWER LIMIT` | Percent throttle defining lower limit (negative for reverse thrust equipped airplanes) | 定义油门下限的百分比(反推飞机可为负值) | Percent(百分比) | 只读 | +| `ENGINE TYPE` | Engine type:; 0 = Piston; 1 = Jet; 2 = None; 3 = Helo(Bell) turbine; 4 = Unsupported; 5 = Turboprop | 发动机类型:0=活塞;1=喷气;2=无;3=贝尔涡轮(直升机);4=不支持;5=涡桨 | Enum(枚举) | 只读 | +| `MASTER IGNITION SWITCH` | Aircraft master ignition switch (grounds all engines magnetos) | 飞机主点火开关(接地所有发动机磁电机) | Bool(布尔) | 只读 | +| `GENERAL ENG COMBUSTION:index` | Combustion flag | 燃烧状态标志(索引) | Bool(布尔) | **可写** | +| `GENERAL ENG MASTER ALTERNATOR:index` | Alternator (generator) switch | 交流发电机开关(索引) | Bool(布尔) | 只读 | +| `GENERAL ENG FUEL PUMP SWITCH:index` | Fuel pump switch | 燃油泵开关(索引) | Bool(布尔) | 只读 | +| `GENERAL ENG FUEL PUMP ON:index` | Fuel pump on/off | 燃油泵开/关(索引) | Bool(布尔) | 只读 | +| `GENERAL ENG RPM:index` | Engine rpm | 发动机转速(索引) | Rpm(转/分) | 只读 | +| `GENERAL ENG PCT MAX RPM:index` | Percent of max rated rpm | 占最大额定转速的百分比(索引) | Percent(百分比) | 只读 | +| `GENERAL ENG MAX REACHED RPM:index` | Maximum attained rpm | 发动机最大达到转速(索引) | Rpm(转/分) | 只读 | +| `GENERAL ENG THROTTLE LEVER POSITION:index` | Percent of max throttle position | 油门杆最大行程的百分比(索引) | Percent(百分比) | **可写** | +| `GENERAL ENG MIXTURE LEVER POSITION:index` | Percent of max mixture lever position | 混合比杆最大行程的百分比(索引) | Percent(百分比) | **可写** | +| `GENERAL ENG PROPELLER LEVER POSITION:index` | Percent of max prop lever position | 桨距杆最大行程的百分比(索引) | Percent(百分比) | **可写** | +| `GENERAL ENG STARTER:index` | Engine starter on/off | 发动机起动机开/关(索引) | Bool(布尔) | 只读 | +| `GENERAL ENG EXHAUST GAS TEMPERATURE:index` | Engine exhaust gas temperature. | 发动机排气温度(索引) | Rankine(兰氏度) | **可写** | +| `GENERAL ENG OIL PRESSURE:index` | Engine oil pressure | 发动机滑油压力(索引) | Psf(磅/平方英尺) | **可写** | +| `GENERAL ENG OIL LEAKED PERCENT:index` | Percent of max oil capacity leaked | 发动机滑油泄漏百分比(索引) | Percent(百分比) | 只读 | +| `GENERAL ENG COMBUSTION SOUND PERCENT:index` | Percent of maximum engine sound | 发动机燃烧音频百分比(索引) | Percent(百分比) | 只读 | +| `GENERAL ENG DAMAGE PERCENT:index` | Percent of total engine damage | 发动机损伤百分比(索引) | Percent(百分比) | 只读 | +| `GENERAL ENG OIL TEMPERATURE:index` | Engine oil temperature | 发动机滑油温度(索引) | Rankine(兰氏度) | **可写** | +| `GENERAL ENG FAILED:index` | Fail flag | 故障标志(索引) | Bool(布尔) | 只读 | +| `GENERAL ENG GENERATOR SWITCH:index` | Alternator (generator) switch | 交流发电机开关(索引) | Bool(布尔) | 只读 | +| `GENERAL ENG GENERATOR ACTIVE:index` | Alternator (generator) on/off | 交流发电机开/关(索引) | Bool(布尔) | **可写** | +| `GENERAL ENG ANTI ICE POSITION:index` | Engine anti-ice switch | 发动机防冰位置(索引) | Bool(布尔) | 只读 | +| `GENERAL ENG FUEL VALVE:index` | Fuel valve state | 发动机燃油阀门(索引) | Bool(布尔) | 只读 | +| `GENERAL ENG FUEL PRESSURE:index` | Engine fuel pressure | 发动机燃油压力(索引) | Psi(磅/平方英寸) | **可写** | +| `GENERAL ENG ELAPSED TIME:index` | Total engine elapsed time | 发动机累计时间(索引) | Hours(小时) | 只读 | +| `RECIP ENG COWL FLAP POSITION:index` | Percent cowl flap opened | 往复式发动机整流罩襟翼位置(索引) | Percent(百分比) | **可写** | +| `RECIP ENG PRIMER:index` | Engine primer position | 往复式发动机注油器(索引) | Bool(布尔) | **可写** | +| `RECIP ENG MANIFOLD PRESSURE:index` | Engine manifold pressure | 往复式发动机歧管压力(索引) | Psi(磅/平方英寸) | **可写** | +| `RECIP ENG ALTERNATE AIR POSITION:index` | Alternate air control | 往复式发动机备用空气位置(索引) | Position(位置) | **可写** | +| `RECIP ENG COOLANT RESERVOIR PERCENT:index` | Percent coolant available | 往复式发动机冷却液储液罐百分比(索引) | Percent(百分比) | **可写** | +| `RECIP ENG LEFT MAGNETO:index` | Left magneto state | 往复式发动机左磁电机(索引) | Bool(布尔) | **可写** | +| `RECIP ENG RIGHT MAGNETO:index` | Right magneto state | 往复式发动机右磁电机(索引) | Bool(布尔) | **可写** | +| `RECIP ENG BRAKE POWER:index` | Brake power produced by engine | 往复式发动机制动功率(索引) | Foot pounds per second(英尺·磅/秒) | **可写** | +| `RECIP ENG STARTER TORQUE:index` | Torque produced by engine | 往复式发动机起动机扭矩(索引) | Foot pound(英尺·磅) | **可写** | +| `RECIP ENG TURBOCHARGER FAILED:index` | Turbo failed state | 往复式发动机涡轮增压器故障(索引) | Bool(布尔) | **可写** | +| `RECIP ENG EMERGENCY BOOST ACTIVE:index` | War emergency power active | 往复式发动机紧急增压激活(索引) | Bool(布尔) | **可写** | +| `RECIP ENG EMERGENCY BOOST ELAPSED TIME:index` | Elapsed time war emergency power active | 往复式发动机紧急增压累计时间(索引) | Hours(小时) | **可写** | +| `RECIP ENG WASTEGATE POSITION:index` | Percent turbo wastegate closed | 往复式发动机废气门位置(索引) | Percent(百分比) | **可写** | +| `RECIP ENG TURBINE INLET TEMPERATURE:index` | Engine turbine inlet temperature | 往复式发动机涡轮进气温度(索引) | Celsius(摄氏度) | **可写** | +| `RECIP ENG CYLINDER HEAD TEMPERATURE:index` | Engine cylinder head temperature | 往复式发动机气缸缸盖温度(索引) | Celsius(摄氏度) | **可写** | +| `RECIP ENG RADIATOR TEMPERATURE:index` | Engine radiator temperature | 往复式发动机散热器温度(索引) | Celsius(摄氏度) | **可写** | +| `RECIP ENG FUEL AVAILABLE:index` | True if fuel is available | 往复式发动机燃油可用(索引) | Bool(布尔) | **可写** | +| `RECIP ENG FUEL FLOW:index` | Engine fuel flow | 往复式发动机燃油流量(索引) | Pounds per hour(磅/小时) | **可写** | +| `RECIP ENG FUEL TANK SELECTOR:index` | Fuel tank selected for engine. See fuel tank list. | 往复式发动机油箱选择器(索引) | Enum(枚举) | 只读 | +| `RECIP ENG FUEL NUMBER TANKS USED:index` | Number of tanks currently being used | 往复式发动机燃油数量油箱使用(索引) | Number(数值) | 只读 | +| `RECIP CARBURETOR TEMPERATURE:index` | Carburetor temperature | 往复式化油器温度(索引) | Celsius(摄氏度) | **可写** | +| `RECIP MIXTURE RATIO:index` | Fuel / Air mixture ratio | 往复式混合比比(索引) | Ratio(比值) | **可写** | +| `TURB ENG N1:index` | Turbine engine N1 | 涡轮发动机N1(索引) | Percent(百分比) | **可写** | +| `TURB ENG N2:index` | Turbine engine N2 | 涡轮发动机N2(索引) | Percent(百分比) | **可写** | +| `TURB ENG CORRECTED N1:index` | Turbine engine corrected N1 | 涡轮发动机修正N1(索引) | Percent(百分比) | **可写** | +| `TURB ENG CORRECTED N2:index` | Turbine engine corrected N2 | 涡轮发动机修正N2(索引) | Percent(百分比) | **可写** | +| `TURB ENG CORRECTED FF:index` | Corrected fuel flow | 涡轮发动机修正燃油流量(索引) | Pounds per hour(磅/小时) | **可写** | +| `TURB ENG MAX TORQUE PERCENT:index` | Percent of max rated torque | 涡轮发动机最大扭矩百分比(索引) | Percent(百分比) | **可写** | +| `TURB ENG PRESSURE RATIO:index` | Engine pressure ratio | 涡轮发动机压力比(索引) | Ratio(比值) | **可写** | +| `TURB ENG ITT:index` | Engine ITT | 涡轮发动机ITT(索引) | Rankine(兰氏度) | **可写** | +| `TURB ENG AFTERBURNER:index` | Afterburner state | 涡轮发动机加力燃烧室(索引) | Bool(布尔) | 只读 | +| `TURB ENG JET THRUST:index` | Engine jet thrust | 涡轮发动机喷气推力(索引) | Pounds(磅) | 只读 | +| `TURB ENG BLEED AIR:index` | Bleed air pressure | 涡轮发动机引气空气(索引) | Psi(磅/平方英寸) | 只读 | +| `TURB ENG TANK SELECTOR:index` | Fuel tank selected for engine. See fuel tank list. | 涡轮发动机油箱选择器(索引) | Enum(枚举) | 只读 | +| `TURB ENG NUM TANKS USED:index` | Number of tanks currently being used | 涡轮发动机数量油箱使用(索引) | Number(数值) | 只读 | +| `TURB ENG FUEL FLOW PPH:index` | Engine fuel flow | 涡轮发动机燃油流量磅/小时(索引) | Pounds per hour(磅/小时) | 只读 | +| `TURB ENG FUEL AVAILABLE:index` | True if fuel is available | 涡轮发动机燃油可用(索引) | Bool(布尔) | 只读 | +| `TURB ENG REVERSE NOZZLE PERCENT:index` | Percent thrust reverser nozzles deployed | 涡轮发动机反推喷管百分比(索引) | Percent(百分比) | 只读 | +| `TURB ENG VIBRATION:index` | Engine vibration value | 涡轮发动机振动(索引) | Number(数值) | 只读 | +| `ENG FAILED:index` | Failure flag | 故障标志(索引) | Number(数值) | 只读 | +| `ENG RPM ANIMATION PERCENT:index` | Percent max rated rpm used for visual animation | 发动机转速动画百分比(索引) | Percent(百分比) | 只读 | +| `ENG ON FIRE:index` | On fire state | 着火状态(索引) | Bool(布尔) | **可写** | +| `ENG FUEL FLOW BUG POSITION:index` | Fuel flow reference | 燃油流量游标位置(索引) | Pounds per hour(磅/小时) | 只读 | +| `PROP RPM:index` | Propeller rpm | 螺旋桨转速 | Rpm(转/分) | **可写** | +| `PROP MAX RPM PERCENT:index` | Percent of max rated rpm | 占最大额定转速的百分比(索引) | Percent(百分比) | 只读 | +| `PROP THRUST:index` | Propeller thrust | 螺旋桨推力 | Pounds(磅) | 只读 | +| `PROP BETA:index` | Prop blade pitch angle | PROP桨距角(索引) | Radians(弧度) | 只读 | +| `PROP FEATHERING INHIBIT:index` | Feathering inhibit flag | 螺旋桨顺桨抑制(索引) | Bool(布尔) | 只读 | +| `PROP FEATHERED:index` | Feathered state | 顺桨状态 | Bool(布尔) | 只读 | +| `PROP SYNC DELTA LEVER:index` | Corrected prop correction input on slaved engine | 螺旋桨同步增量杆(索引) | Position(位置) | 只读 | +| `PROP AUTO FEATHER ARMED:index` | Auto-feather armed state | 螺旋桨自动顺桨预位(索引) | Bool(布尔) | 只读 | +| `PROP FEATHER SWITCH:index` | Prop feather switch | 螺旋桨顺桨开关(索引) | Bool(布尔) | 只读 | +| `PANEL AUTO FEATHER SWITCH:index` | Auto-feather arming switch | 面板自动顺桨开关(索引) | Bool(布尔) | 只读 | +| `PROP SYNC ACTIVE:index` | True if prop sync is active | 螺旋桨同步激活(索引) | Bool(布尔) | 只读 | +| `PROP DEICE SWITCH:index` | True if prop deice switch on | 螺旋桨除冰开关(索引) | Bool(布尔) | 只读 | +| `ENG COMBUSTION` | True if the engine is running | 当the发动机为running时为真 | Bool(布尔) | 只读 | +| `ENG N1 RPM:index` | Engine N1 rpm | 发动机 N1 转速(索引) | Rpm (0 to 16384 = 0 to 100%)(转/分(0-16384=0-100%)) | 只读 | +| `ENG N2 RPM:index` | Engine N2 rpm | 发动机 N2 转速(索引) | Rpm(0 to 16384 = 0 to 100%)(转/分(0-16384=0-100%)) | 只读 | +| `ENG FUEL FLOW GPH:index` | Engine fuel flow | 发动机燃油流量加仑/小时(索引) | Gallons per hour(加仑/小时) | 只读 | +| `ENG FUEL FLOW PPH:index` | Engine fuel flow | 发动机燃油流量磅/小时(索引) | Pounds per hour(磅/小时) | 只读 | +| `ENG TORQUE:index` | Torque | 发动机扭矩(索引) | Foot pounds(英尺·磅) | 只读 | +| `ENG ANTI ICE:index` | Anti-ice switch | 发动机防冰(索引) | Bool(布尔) | 只读 | +| `ENG PRESSURE RATIO:index` | Engine pressure ratio | 发动机压比(索引) | Ratio (0-16384)(比值(0-16384)) | 只读 | +| `ENG EXHAUST GAS TEMPERATURE:index` | Exhaust gas temperature | 发动机排气温度(索引) | Rankine(兰氏度) | 只读 | +| `ENG EXHAUST GAS TEMPERATURE GES:index` | Governed engine setting | 发动机排气温度受控设定(索引) | Percent over 100(百分比(超100)) | 只读 | +| `ENG CYLINDER HEAD TEMPERATURE:index` | Engine cylinder head temperature | 发动机缸盖温度(索引) | Rankine(兰氏度) | 只读 | +| `ENG OIL TEMPERATURE:index` | Engine oil temperature | 发动机滑油温度(索引) | Rankine(兰氏度) | 只读 | +| `ENG OIL PRESSURE:index` | Engine oil pressure | 发动机滑油压力(索引) | foot pounds | 只读 | +| `ENG OIL QUANTITY:index` | Engine oil quantitiy as a percentage of full capacity | 发动机滑油quantitiy作为a百分比的full容量 | Percent over 100(百分比(超100)) | 只读 | +| `ENG HYDRAULIC PRESSURE:index` | Engine hydraulic pressure | 发动机液压压力(索引) | foot pounds | 只读 | +| `ENG HYDRAULIC QUANTITY:index` | Engine hydraulic fluid quantity, as a percentage of total capacity | 发动机液压fluid数量,作为a百分比的总容量 | Percent over 100(百分比(超100)) | 只读 | +| `ENG MANIFOLD PRESSURE:index` | Engine manifold pressure. | 发动机歧管压力(索引) | inHG.(英寸汞柱) | 只读 | +| `ENG VIBRATION:index` | Engine vibration | 发动机振动(索引) | Number(数值) | 只读 | +| `ENG RPM SCALER:index` | Obsolete | 已废弃(索引) | Scalar(标量) | 只读 | +| `ENG MAX RPM` | Maximum rpm | 发动机最大转速 | Rpm(转/分) | 只读 | +| `GENERAL ENG STARTER ACTIVE` | True if engine starter is active | 当发动机起动机为激活时为真 | Bool(布尔) | 只读 | +| `GENERAL ENG FUEL USED SINCE START` | Fuel used since the engines were last started | 自启动以来发动机耗油 | Pounds(磅) | 只读 | +| `TURB ENG PRIMARY NOZZLE PERCENT:index` | Percent thrust of primary nozzle | 涡轮发动机主喷管百分比(索引) | Percent over 100(百分比(超100)) | 只读 | +| `TURB ENG IGNITION SWITCH` | True if the turbine engine ignition switch is on | 当the涡轮发动机点火开关为开时为真 | Bool(布尔) | 只读 | +| `TURB ENG MASTER STARTER SWITCH` | True if the turbine engine master starter switch is on | 当the涡轮发动机主起动机开关为开时为真 | Bool(布尔) | 只读 | +| `TURB ENG AFTERBURNER STAGE ACTIVE` | The stage of the afterburner, or 0 if the afterburner is not active. | The级的the加力燃烧室,或0若the加力燃烧室为not激活. | Number(数值) | 只读 | + +## 二、Aircraft Fuel Data(燃油) + +共 **48** 项(可写 22 / 只读 26) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `FUEL TANK CENTER LEVEL` | Percent of maximum capacity | 占最大容量的百分比 | Percent Over 100(百分比(超100)) | **可写** | +| `FUEL TANK CENTER2 LEVEL` | Percent of maximum capacity | 占最大容量的百分比 | Percent Over 100(百分比(超100)) | **可写** | +| `FUEL TANK CENTER3 LEVEL` | Percent of maximum capacity | 占最大容量的百分比 | Percent Over 100(百分比(超100)) | **可写** | +| `FUEL TANK LEFT MAIN LEVEL` | Percent of maximum capacity | 占最大容量的百分比 | Percent Over 100(百分比(超100)) | **可写** | +| `FUEL TANK LEFT AUX LEVEL` | Percent of maximum capacity | 占最大容量的百分比 | Percent Over 100(百分比(超100)) | **可写** | +| `FUEL TANK LEFT TIP LEVEL` | Percent of maximum capacity | 占最大容量的百分比 | Percent Over 100(百分比(超100)) | **可写** | +| `FUEL TANK RIGHT MAIN LEVEL` | Percent of maximum capacity | 占最大容量的百分比 | Percent Over 100(百分比(超100)) | **可写** | +| `FUEL TANK RIGHT AUX LEVEL` | Percent of maximum capacity | 占最大容量的百分比 | Percent Over 100(百分比(超100)) | **可写** | +| `FUEL TANK RIGHT TIP LEVEL` | Percent of maximum capacity | 占最大容量的百分比 | Percent Over 100(百分比(超100)) | **可写** | +| `FUEL TANK EXTERNAL1 LEVEL` | Percent of maximum capacity | 占最大容量的百分比 | Percent Over 100(百分比(超100)) | **可写** | +| `FUEL TANK EXTERNAL2 LEVEL` | Percent of maximum capacity | 占最大容量的百分比 | Percent Over 100(百分比(超100)) | **可写** | +| `FUEL TANK CENTER CAPACITY` | Maximum capacity in volume | 最大容积容量 | Gallons(加仑) | 只读 | +| `FUEL TANK CENTER2 CAPACITY` | Maximum capacity in volume | 最大容积容量 | Gallons(加仑) | 只读 | +| `FUEL TANK CENTER3 CAPACITY` | Maximum capacity in volume | 最大容积容量 | Gallons(加仑) | 只读 | +| `FUEL TANK LEFT MAIN CAPACITY` | Maximum capacity in volume | 最大容积容量 | Gallons(加仑) | 只读 | +| `FUEL TANK LEFT AUX CAPACITY` | Maximum capacity in volume | 最大容积容量 | Gallons(加仑) | 只读 | +| `FUEL TANK LEFT TIP CAPACITY` | Maximum capacity in volume | 最大容积容量 | Gallons(加仑) | 只读 | +| `FUEL TANK RIGHT MAIN CAPACITY` | Maximum capacity in volume | 最大容积容量 | Gallons(加仑) | 只读 | +| `FUEL TANK RIGHT AUX CAPACITY` | Maximum capacity in volume | 最大容积容量 | Gallons(加仑) | 只读 | +| `FUEL TANK RIGHT TIP CAPACITY` | Maximum capacity in volume | 最大容积容量 | Gallons(加仑) | 只读 | +| `FUEL TANK EXTERNAL1 CAPACITY` | Maximum capacity in volume | 最大容积容量 | Gallons(加仑) | 只读 | +| `FUEL TANK EXTERNAL2 CAPACITY` | Maximum capacity in volume | 最大容积容量 | Gallons(加仑) | 只读 | +| `FUEL LEFT CAPACITY` | Maximum capacity in volume | 最大容积容量 | Gallons(加仑) | 只读 | +| `FUEL RIGHT CAPACITY` | Maximum capacity in volume | 最大容积容量 | Gallons(加仑) | 只读 | +| `FUEL TANK CENTER QUANTITY` | Current quantity in volume | 当前油量(容积) | Gallons(加仑) | **可写** | +| `FUEL TANK CENTER2 QUANTITY` | Current quantity in volume | 当前油量(容积) | Gallons(加仑) | **可写** | +| `FUEL TANK CENTER3 QUANTITY` | Current quantity in volume | 当前油量(容积) | Gallons(加仑) | **可写** | +| `FUEL TANK LEFT MAIN QUANTITY` | Current quantity in volume | 当前油量(容积) | Gallons(加仑) | **可写** | +| `FUEL TANK LEFT AUX QUANTITY` | Current quantity in volume | 当前油量(容积) | Gallons(加仑) | **可写** | +| `FUEL TANK LEFT TIP QUANTITY` | Current quantity in volume | 当前油量(容积) | Gallons(加仑) | **可写** | +| `FUEL TANK RIGHT MAIN QUANTITY` | Current quantity in volume | 当前油量(容积) | Gallons(加仑) | **可写** | +| `FUEL TANK RIGHT AUX QUANTITY` | Current quantity in volume | 当前油量(容积) | Gallons(加仑) | **可写** | +| `FUEL TANK RIGHT TIP QUANTITY` | Current quantity in volume | 当前油量(容积) | Gallons(加仑) | **可写** | +| `FUEL TANK EXTERNAL1 QUANTITY` | Current quantity in volume | 当前油量(容积) | Gallons(加仑) | **可写** | +| `FUEL TANK EXTERNAL2 QUANTITY` | Current quantity in volume | 当前油量(容积) | Gallons(加仑) | **可写** | +| `FUEL LEFT QUANTITY` | Current quantity in volume | 当前油量(容积) | Gallons(加仑) | 只读 | +| `FUEL RIGHT QUANTITY` | Current quantity in volume | 当前油量(容积) | Gallons(加仑) | 只读 | +| `FUEL TOTAL QUANTITY` | Current quantity in volume | 当前油量(容积) | Gallons(加仑) | 只读 | +| `FUEL WEIGHT PER GALLON` | Fuel weight per gallon | 每加仑燃油重量 | Pounds(磅) | 只读 | +| `FUEL TANK SELECTOR:index` | Which tank is selected. See fuel tank list. | 油箱选择器(索引) | Enum(枚举) | 只读 | +| `FUEL CROSS FEED` | Cross feed valve:; 0 = Closed; 1 = Open | 串油阀门:;0=Closed;1=打开 | Enum(枚举) | 只读 | +| `FUEL TOTAL CAPACITY` | Total capacity of the aircraft | 总容量的the飞机 | Gallons(加仑) | 只读 | +| `FUEL SELECTED QUANTITY PERCENT` | Percent or capacity for selected tank | 百分比或容量用于selected油箱 | Percent Over 100(百分比(超100)) | 只读 | +| `FUEL SELECTED QUANTITY` | Quantity of selected tank | 数量的selected油箱 | Gallons(加仑) | 只读 | +| `FUEL TOTAL QUANTITY WEIGHT` | Current total fuel weight of the aircraft | 当前总燃油重量的the飞机 | Pounds(磅) | 只读 | +| `NUM FUEL SELECTORS` | Number of selectors on the aircraft | 数量的selectors开the飞机 | Number(数值) | 只读 | +| `UNLIMITED FUEL` | Unlimited fuel flag | Unlimited燃油标志 | Bool(布尔) | 只读 | +| `ESTIMATED FUEL FLOW` | Estimated fuel flow at cruise | Estimated燃油流量在cruise | Pounds per hour(磅/小时) | 只读 | + +## 三、Aircraft Lights Data(灯光) + +共 **24** 项(可写 0 / 只读 24) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `LIGHT STROBE` | Light switch state | 灯光开关状态 | Bool(布尔) | 只读 | +| `LIGHT PANEL` | Light switch state | 灯光开关状态 | Bool(布尔) | 只读 | +| `LIGHT LANDING` | Light switch state | 灯光开关状态 | Bool(布尔) | 只读 | +| `LIGHT TAXI` | Light switch state | 灯光开关状态 | Bool(布尔) | 只读 | +| `LIGHT BEACON` | Light switch state | 灯光开关状态 | Bool(布尔) | 只读 | +| `LIGHT NAV` | Light switch state | 灯光开关状态 | Bool(布尔) | 只读 | +| `LIGHT LOGO` | Light switch state | 灯光开关状态 | Bool(布尔) | 只读 | +| `LIGHT WING` | Light switch state | 灯光开关状态 | Bool(布尔) | 只读 | +| `LIGHT RECOGNITION` | Light switch state | 灯光开关状态 | Bool(布尔) | 只读 | +| `LIGHT CABIN` | Light switch state | 灯光开关状态 | Bool(布尔) | 只读 | +| `LIGHT ON STATES` | Bit mask:; 0x0001: Nav; 0x0002: Beacon; 0x0004: Landing; 0x0008: Taxi; 0x0010: Strobe; 0x0020: Panel; 0x0040: Recognition; 0x0080: Wing; 0x0100: Logo; 0x0200: Cabin | 位mask:;0x0001:导航;0x0002:信标;0x0004:着陆;0x0008:滑行;0x0010:频闪;0x0020:面板;0x0040:识别;0x0080:机翼;0x0100:标志;0x0200:客舱 | Mask(掩码) | 只读 | +| `LIGHT STATES` | Same as LIGHT ON STATES | Same作为灯光开状态 | Mask(掩码) | 只读 | +| `LIGHT TAXI ON` | Return true if the light is on. | 灯光开启时返回真 | Bool(布尔) | 只读 | +| `LIGHT STROBE ON` | Return true if the light is on. | 灯光开启时返回真 | Bool(布尔) | 只读 | +| `LIGHT PANEL ON` | Return true if the light is on. | 灯光开启时返回真 | Bool(布尔) | 只读 | +| `LIGHT RECOGNITION ON` | Return true if the light is on. | 灯光开启时返回真 | Bool(布尔) | 只读 | +| `LIGHT WING ON` | Return true if the light is on. | 灯光开启时返回真 | Bool(布尔) | 只读 | +| `LIGHT LOGO ON` | Return true if the light is on. | 灯光开启时返回真 | Bool(布尔) | 只读 | +| `LIGHT CABIN ON` | Return true if the light is on. | 灯光开启时返回真 | Bool(布尔) | 只读 | +| `LIGHT HEAD ON` | Return true if the light is on. | 灯光开启时返回真 | Bool(布尔) | 只读 | +| `LIGHT BRAKE ON` | Return true if the light is on. | 灯光开启时返回真 | Bool(布尔) | 只读 | +| `LIGHT NAV ON` | Return true if the light is on. | 灯光开启时返回真 | Bool(布尔) | 只读 | +| `LIGHT BEACON ON` | Return true if the light is on. | 灯光开启时返回真 | Bool(布尔) | 只读 | +| `LIGHT LANDING ON` | Return true if the light is on. | 灯光开启时返回真 | Bool(布尔) | 只读 | + +## 四、Aircraft Position and Speed Data(位置与速度) + +共 **34** 项(可写 24 / 只读 10) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `GROUND VELOCITY` | Speed relative to the earths surface | 速度相对至theearthssurface | Knots(节) | 只读 | +| `TOTAL WORLD VELOCITY` | Speed relative to the earths center | 速度相对至theearths中央 | Feet per second(英尺/秒) | 只读 | +| `VELOCITY BODY Z` | True longitudinal speed, relative to aircraft axis | 真longitudinal速度,相对至飞机axis | Feet per second(英尺/秒) | **可写** | +| `VELOCITY BODY X` | True lateral speed, relative to aircraft axis | 真lateral速度,相对至飞机axis | Feet per second(英尺/秒) | **可写** | +| `VELOCITY BODY Y` | True vertical speed, relative to aircraft axis | 真垂直速度,相对至飞机axis | Feet per second(英尺/秒) | **可写** | +| `VELOCITY WORLD Z` | Speed relative to earth, in North/South direction | 世界速度垂向 | Feet per second(英尺/秒) | **可写** | +| `VELOCITY WORLD X` | Speed relative to earth, in East/West direction | 世界速度横向 | Feet per second(英尺/秒) | **可写** | +| `VELOCITY WORLD Y` | Speed relative to earth, in vertical direction | 速度相对至earth,在垂直方向 | Feet per second(英尺/秒) | **可写** | +| `ACCELERATION WORLD X` | Acceleration relative to earth, in east/west direction | 世界加速度横向 | Feet per second squared(英尺/秒²) | **可写** | +| `ACCELERATION WORLD Y` | Acceleration relative to earch, in vertical direction | 加速度相对至earch,在垂直方向 | Feet per second squared(英尺/秒²) | **可写** | +| `ACCELERATION WORLD Z` | Acceleration relative to earth, in north/south direction | 世界加速度垂向 | Feet per second squared(英尺/秒²) | **可写** | +| `ACCELERATION BODY X` | Acceleration relative to aircraft axix, in east/west direction | 加速度相对至飞机axix,在east/west方向 | Feet per second squared(英尺/秒²) | **可写** | +| `ACCELERATION BODY Y` | Acceleration relative to aircraft axis, in vertical direction | 加速度相对至飞机axis,在垂直方向 | Feet per second squared(英尺/秒²) | **可写** | +| `ACCELERATION BODY Z` | Acceleration relative to aircraft axis, in north/south direction | 加速度相对至飞机axis,在north/south方向 | Feet per second squared(英尺/秒²) | **可写** | +| `ROTATION VELOCITY BODY X` | Rotation relative to aircraft axis | 机体旋转角速度横向 | Feet per second(英尺/秒) | **可写** | +| `ROTATION VELOCITY BODY Y` | Rotation relative to aircraft axis | 机体旋转角速度纵向 | Feet per second(英尺/秒) | **可写** | +| `ROTATION VELOCITY BODY Z` | Rotation relative to aircraft axis | 机体旋转角速度垂向 | Feet per second(英尺/秒) | **可写** | +| `RELATIVE WIND VELOCITY BODY X` | Lateral speed relative to wind | 相对风机体速度横向 | Feet per second(英尺/秒) | 只读 | +| `RELATIVE WIND VELOCITY BODY Y` | Vertical speed relative to wind | 相对风机体速度纵向 | Feet per second(英尺/秒) | 只读 | +| `RELATIVE WIND VELOCITY BODY Z` | Longitudinal speed relative to wind | 相对风机体速度垂向 | Feet per second(英尺/秒) | 只读 | +| `PLANE ALT ABOVE GROUND` | Altitude above the surface | 飞机离地高度 | Feet(英尺) | **可写** | +| `PLANE LATITUDE` | Latitude of aircraft, North is positive, South negative | 飞机纬度 | Degrees(度) | **可写** | +| `PLANE LONGITUDE` | Longitude of aircraft, East is positive, West negative | 飞机经度 | Degrees(度) | **可写** | +| `PLANE ALTITUDE` | Altitude of aircraft | 高度的飞机 | Feet(英尺) | **可写** | +| `PLANE PITCH DEGREES` | Pitch angle, although the name mentions degrees the units used are radians | 俯仰角,althoughthe名称mentions度the单位使用areradians | Radians(弧度) | **可写** | +| `PLANE BANK DEGREES` | Bank angle, although the name mentions degrees the units used are radians | 坡度角,althoughthe名称mentions度the单位使用areradians | Radians(弧度) | **可写** | +| `PLANE HEADING DEGREES TRUE` | Heading relative to true north, although the name mentions degrees the units used are radians | 航向相对至真north,althoughthe名称mentions度the单位使用areradians | Radians(弧度) | **可写** | +| `PLANE HEADING DEGREES MAGNETIC` | Heading relative to magnetic north, although the name mentions degrees the units used are radians | 航向相对至磁north,althoughthe名称mentions度the单位使用areradians | Radians(弧度) | **可写** | +| `MAGVAR` | Magnetic variation | 磁偏角 | Degrees(度) | 只读 | +| `GROUND ALTITUDE` | Altitude of surface | 地面高度 | Meters(米) | 只读 | +| `SIM ON GROUND` | On ground flag | 模拟开地面 | Bool(布尔) | 只读 | +| `INCIDENCE ALPHA` | Angle of attack | INCIDENCE迎角 | Radians(弧度) | 只读 | +| `INCIDENCE BETA` | Sideslip angle | INCIDENCE桨距角 | Radians(弧度) | 只读 | +| `WING FLEX PCT:index` | The current wing flex. Different values can be set for each wing (for example, during banking). Set an index of 1 for the left wing, and 2 for the right wing. | The当前机翼弯曲.Differentvaluescanbe设定用于each机翼(用于example,duringbanking).设定an索引的1用于the左机翼,和2用于the右机翼. | Percent over 100(百分比(超100)) | **可写** | + +## 五、Aircraft Flight Instrumentation Data(飞行仪表) + +共 **45** 项(可写 24 / 只读 21) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `AIRSPEED TRUE` | True airspeed | 真空速 | Knots(节) | **可写** | +| `AIRSPEED INDICATED` | Indicated airspeed | 指示空速 | Knots(节) | **可写** | +| `AIRSPEED TRUE CALIBRATE` | Angle of True calibration scale on airspeed indicator | 角的真calibrationscale开空速指示器 | Degrees(度) | **可写** | +| `AIRSPEED BARBER POLE` | Redline airspeed (dynamic on some aircraft) | Redline空速(dynamic开some飞机) | Knots(节) | 只读 | +| `AIRSPEED MACH` | Current mach | 空速MACH | Mach | 只读 | +| `VERTICAL SPEED` | Vertical speed indication | 垂直速度指示 | feet/minute(英尺/分钟) | **可写** | +| `MACH MAX OPERATE` | Maximum design mach | 最大设计mach | Mach | 只读 | +| `STALL WARNING` | Stall warning state | Stall警告状态 | Bool(布尔) | 只读 | +| `OVERSPEED WARNING` | Overspeed warning state | Overspeed警告状态 | Bool(布尔) | 只读 | +| `BARBER POLE MACH` | Mach associated with maximum airspeed | Machassociated带最大空速 | Mach | 只读 | +| `INDICATED ALTITUDE` | Altimeter indication | 高度表指示 | Feet(英尺) | **可写** | +| `KOHLSMAN SETTING MB` | Altimeter setting | 高度表气压设定 | Millibars(毫巴) | **可写** | +| `KOHLSMAN SETTING HG` | Altimeter setting | 高度表气压设定 | inHg(英寸汞柱) | 只读 | +| `ATTITUDE INDICATOR PITCH DEGREES` | AI pitch indication | 姿态指示器俯仰度 | Radians(弧度) | 只读 | +| `ATTITUDE INDICATOR BANK DEGREES` | AI bank indication | 姿态指示器坡度度 | Radians(弧度) | 只读 | +| `ATTITUDE BARS POSITION` | AI reference pitch reference bars | 姿态BARS位置 | Percent Over 100(百分比(超100)) | 只读 | +| `ATTITUDE CAGE` | AI caged state | AI锁定状态 | Bool(布尔) | 只读 | +| `WISKEY COMPASS INDICATION DEGREES` | Magnetic compass indication | 磁罗盘指示 | Degrees(度) | **可写** | +| `PLANE HEADING DEGREES GYRO` | Heading indicator (directional gyro) indication | 航向指示器(directional陀螺)indication | Radians(弧度) | **可写** | +| `HEADING INDICATOR` | Heading indicator (directional gyro) indication | 航向指示器(directional陀螺)indication | Radians(弧度) | 只读 | +| `GYRO DRIFT ERROR` | Angular error of heading indicator | Angular误差的航向指示器 | Radians(弧度) | 只读 | +| `DELTA HEADING RATE` | Rate of turn of heading indicator | 速率的转弯的航向指示器 | Radians per second(弧度/秒) | **可写** | +| `TURN COORDINATOR BALL` | Turn coordinator ball position | 转弯coordinatorball位置 | Position(位置) | 只读 | +| `ANGLE OF ATTACK INDICATOR` | AoA indication | 角度OFATTACK指示器 | Radians(弧度) | 只读 | +| `RADIO HEIGHT` | Radar altitude | RADIO高度 | Feet(英尺) | 只读 | +| `PARTIAL PANEL ADF` | Gauge fail flag (0 = ok, 1 = fail, 2 = blank) | 仪表故障标志(0=正常,1=故障,2=空白) | Enum(枚举) | **可写** | +| `PARTIAL PANEL AIRSPEED` | Gauge fail flag (0 = ok, 1 = fail, 2 = blank) | 仪表故障标志(0=正常,1=故障,2=空白) | Enum(枚举) | **可写** | +| `PARTIAL PANEL ALTIMETER` | Gauge fail flag (0 = ok, 1 = fail, 2 = blank) | 仪表故障标志(0=正常,1=故障,2=空白) | Enum(枚举) | **可写** | +| `PARTIAL PANEL ATTITUDE` | Gauge fail flag (0 = ok, 1 = fail, 2 = blank) | 仪表故障标志(0=正常,1=故障,2=空白) | Enum(枚举) | **可写** | +| `PARTIAL PANEL COMM` | Gauge fail flag (0 = ok, 1 = fail, 2 = blank) | 仪表故障标志(0=正常,1=故障,2=空白) | Enum(枚举) | **可写** | +| `PARTIAL PANEL COMPASS` | Gauge fail flag (0 = ok, 1 = fail, 2 = blank) | 仪表故障标志(0=正常,1=故障,2=空白) | Enum(枚举) | **可写** | +| `PARTIAL PANEL ELECTRICAL` | Gauge fail flag (0 = ok, 1 = fail, 2 = blank) | 仪表故障标志(0=正常,1=故障,2=空白) | Enum(枚举) | **可写** | +| `PARTIAL PANEL AVIONICS` | Gauge fail flag (0 = ok, 1 = fail, 2 = blank) | 仪表故障标志(0=正常,1=故障,2=空白) | Enum(枚举) | 只读 | +| `PARTIAL PANEL ENGINE` | Gauge fail flag (0 = ok, 1 = fail, 2 = blank) | 仪表故障标志(0=正常,1=故障,2=空白) | Enum(枚举) | **可写** | +| `PARTIAL PANEL FUEL INDICATOR` | Gauge fail flag (0 = ok, 1 = fail, 2 = blank) | 仪表故障标志(0=正常,1=故障,2=空白) | Enum(枚举) | 只读 | +| `PARTIAL PANEL HEADING` | Gauge fail flag (0 = ok, 1 = fail, 2 = blank) | 仪表故障标志(0=正常,1=故障,2=空白) | Enum(枚举) | **可写** | +| `PARTIAL PANEL VERTICAL VELOCITY` | Gauge fail flag (0 = ok, 1 = fail, 2 = blank) | 仪表故障标志(0=正常,1=故障,2=空白) | Enum(枚举) | **可写** | +| `PARTIAL PANEL TRANSPONDER` | Gauge fail flag (0 = ok, 1 = fail, 2 = blank) | 仪表故障标志(0=正常,1=故障,2=空白) | Enum(枚举) | **可写** | +| `PARTIAL PANEL NAV` | Gauge fail flag (0 = ok, 1 = fail, 2 = blank) | 仪表故障标志(0=正常,1=故障,2=空白) | Enum(枚举) | **可写** | +| `PARTIAL PANEL PITOT` | Gauge fail flag (0 = ok, 1 = fail, 2 = blank) | 仪表故障标志(0=正常,1=故障,2=空白) | Enum(枚举) | **可写** | +| `PARTIAL PANEL TURN COORDINATOR` | Gauge fail flag (0 = ok, 1 = fail, 2 = blank) | 仪表故障标志(0=正常,1=故障,2=空白) | Enum(枚举) | 只读 | +| `PARTIAL PANEL VACUUM` | Gauge fail flag (0 = ok, 1 = fail, 2 = blank) | 仪表故障标志(0=正常,1=故障,2=空白) | Enum(枚举) | **可写** | +| `MAX G FORCE` | Maximum G force attained | 最大GFORCE | Gforce | 只读 | +| `MIN G FORCE` | Minimum G force attained | 最小Gforceattained | Gforce | 只读 | +| `SUCTION PRESSURE` | Vacuum system suction pressure | 真空系统吸力压力 | inHg(英寸汞柱) | **可写** | + +## 六、Aircraft Avionics Data(航电) + +共 **115** 项(可写 4 / 只读 111) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `AVIONICS MASTER SWITCH` | Avionics switch state | 航电总开关 | Bool(布尔) | 只读 | +| `NAV SOUND:index` | Nav audio flag. Index of 1 or 2. | 导航音频标志(索引1或2) | Bool(布尔) | 只读 | +| `DME SOUND` | DME audio flag | DME 音频标志 | Bool(布尔) | 只读 | +| `ADF SOUND:index` | ADF audio flag. Index of 0 or 1. | ADF 音频标志(索引0或1) | Bool(布尔) | 只读 | +| `MARKER SOUND` | Marker audio flag | 指点标音频标志 | Bool(布尔) | 只读 | +| `COM TRANSMIT:index` | Audio panel com transmit state. Index of 1 or 2. | 音频面板通讯发射状态(索引) | Bool(布尔) | 只读 | +| `COM RECIEVE ALL` | Flag if all Coms receiving | 通讯全接收标志 | Bool(布尔) | 只读 | +| `COM ACTIVE FREQUENCY:index` | Com frequency. Index is 1 or 2. | 通讯频率(索引) | MHz(兆赫) | 只读 | +| `COM STANDBY FREQUENCY:index` | Com standby frequency. Index is 1 or 2. | 通讯备用频率(索引) | MHz(兆赫) | 只读 | +| `NAV AVAILABLE:index` | Flag if Nav equipped on aircraft | 导航设备可用标志(索引) | Bool(布尔) | 只读 | +| `NAV ACTIVE FREQUENCY:index` | Nav active frequency. Index is 1 or 2. | 导航激活频率(索引) | MHz(兆赫) | 只读 | +| `NAV STANDBY FREQUENCY:index` | Nav standby frequency. Index is 1 or 2. | 导航备用频率(索引) | MHz(兆赫) | 只读 | +| `NAV SIGNAL:index` | Nav signal strength | 导航信号强度(索引) | Number(数值) | 只读 | +| `NAV HAS NAV:index` | Flag if Nav has signal | 导航信号可用标志(索引) | Bool(布尔) | 只读 | +| `NAV HAS LOCALIZER:index` | Flag if tuned station is a localizer | 调谐台为航向道标志(索引) | Bool(布尔) | 只读 | +| `NAV HAS DME:index` | Flag if tuned station has a DME | 导航台 DME 可用标志(索引) | Bool(布尔) | 只读 | +| `NAV HAS GLIDE SLOPE:index` | Flag if tuned station has a glideslope | 导航台下滑道可用标志(索引) | Bool(布尔) | 只读 | +| `NAV BACK COURSE FLAGS:index` | Returns the following bit flags:; BIT0: 1=back course available; BIT1: 1=localizer tuned in; BIT2: 1=on course; BIT7: 1=station active | 导航背台标志位(索引) | Flags(标志位) | 只读 | +| `NAV MAGVAR:index` | Magnetic variation of tuned nav station | 导航台磁偏角(索引) | Degrees(度) | 只读 | +| `NAV RADIAL:index` | Radial that aircraft is on | 飞机所在径向(索引) | Degrees(度) | 只读 | +| `NAV RADIAL ERROR:index` | Difference between current radial and OBS tuned radial | 当前径向与 OBS 调谐径向之差(索引) | Degrees(度) | 只读 | +| `NAV LOCALIZER:index` | Localizer course heading | 航向道航道航向(索引) | Degrees(度) | 只读 | +| `NAV GLIDE SLOPE ERROR:index` | Difference between current position and glideslope angle. Note that this provides 32 bit floating point precision, rather than the 8 bit integer precision of NAV GSI. | 当前位置与下滑道角之差(索引) | Degrees(度) | 只读 | +| `NAV CDI:index` | CDI needle deflection (+/- 127) | CDI 指针偏转(索引) | Number(数值) | 只读 | +| `NAV GSI:index` | Glideslope needle deflection (+/- 119). Note that this provides only 8 bit precision, whereas NAV GLIDE SLOPE ERROR provides 32 bit floating point precision. | 下滑道指针偏转(索引) | Number(数值) | 只读 | +| `NAV GS FLAG:index` | Glideslope flag | 下滑道标志(索引) | Bool(布尔) | 只读 | +| `NAV OBS:index` | OBS setting. Index of 1 or 2. | OBS 设定(索引) | Degrees(度) | 只读 | +| `NAV DME:index` | DME distance | DME 距离(索引) | Nautical miles(海里) | 只读 | +| `NAV DMESPEED:index` | DME speed | DME 速度(索引) | Knots(节) | 只读 | +| `ADF ACTIVE FREQUENCY:index` | ADF frequency. Index of 1 or 2. | ADF 频率(索引) | Frequency ADF BCD32(ADF BCD32 频率) | 只读 | +| `ADF STANDBY FREQUENCY:index` | ADF standby frequency | ADF 备用频率(索引) | Hz(赫兹) | 只读 | +| `ADF RADIAL:index` | Current direction from NDB station | 相对 NDB 台当前方位(索引) | Degrees(度) | 只读 | +| `ADF SIGNAL:index` | Signal strength | 信号强度 | Number(数值) | 只读 | +| `TRANSPONDER CODE:index` | 4-digit code | 四位编码(索引) | BCO16 | 只读 | +| `MARKER BEACON STATE` | Marker beacon state:; 0 = None; 1 = Outer; 2 = Middle; 3 = Inner | 指点标信标状态:;0=无;1=Outer;2=Middle;3=Inner | Enum(枚举) | **可写** | +| `INNER MARKER` | Inner marker state | Inner指点标状态 | Bool(布尔) | **可写** | +| `MIDDLE MARKER` | Middle marker state | Middle指点标状态 | Bool(布尔) | **可写** | +| `OUTER MARKER` | Outer marker state | Outer指点标状态 | Bool(布尔) | **可写** | +| `NAV RAW GLIDE SLOPE:index` | Glide slope angle | 导航RAW下滑道(索引) | Degrees(度) | 只读 | +| `ADF CARD` | ADF compass rose setting | ADF罗盘罗盘设定 | Degrees(度) | 只读 | +| `HSI CDI NEEDLE` | Needle deflection (+/- 127) | 指针偏转(+/-127) | Number(数值) | 只读 | +| `HSI GSI NEEDLE` | Needle deflection (+/- 119) | 指针偏转(+/-119) | Number(数值) | 只读 | +| `HSI CDI NEEDLE VALID` | Signal valid | HSI CDI 指针信号有效 | Bool(布尔) | 只读 | +| `HSI GSI NEEDLE VALID` | Signal valid | HSI GSI 指针信号有效 | Bool(布尔) | 只读 | +| `HSI TF FLAGS` | Nav TO/FROM flag:; 0 = Off; 1 = TO; 2 = FROM | 导航至/来自标志:;0=关;1=至;2=来自 | Enum(枚举) | 只读 | +| `HSI BEARING VALID` | This will return true if the HSI BEARING variable contains valid data. | 此will返回真若theHSI方位variablecontains有效data. | Bool(布尔) | 只读 | +| `HSI BEARING` | If the GPS DRIVES NAV1 variable is true and the HSI BEARING VALID variable is true, this variable contains the HSI needle bearing. If the GPS DRIVES NAV1 variable is false and the HSI BEARING VALID variable is true, this variable contains the ADF1 frequency. | 若theGPSDRIVESNAV1variable为真和theHSI方位有效variable为真,此variablecontainstheHSI指针方位.若theGPSDRIVESNAV1variable为false和theHSI方位有效variable为真,此variablecontainstheADF1频率. | Degrees(度) | 只读 | +| `HSI HAS LOCALIZER` | Station is a localizer | 台站为a航向道 | Bool(布尔) | 只读 | +| `HSI SPEED` | DME/GPS speed | HSI速度 | Knots(节) | 只读 | +| `HSI DISTANCE` | DME/GPS distance | HSI距离 | Nautical miles(海里) | 只读 | +| `GPS POSITION LAT` | Current GPS latitude | 当前GPS纬度 | Degrees(度) | 只读 | +| `GPS POSITION LON` | Current GPS longitude | 当前GPS经度 | Degrees(度) | 只读 | +| `GPS POSITION ALT` | Current GPS altitude | GPS位置高度 | Meters(米) | 只读 | +| `GPS MAGVAR` | Current GPS magnetic variation | 当前GPS磁偏角 | Radians(弧度) | 只读 | +| `GPS IS ACTIVE FLIGHT PLAN` | Flight plan mode active | 飞行plan模式激活 | Bool(布尔) | 只读 | +| `GPS IS ACTIVE WAY POINT` | Waypoint mode active | Waypoint模式激活 | Bool(布尔) | 只读 | +| `GPS IS ARRIVED` | Is flight plan destination reached | 为飞行plandestination达到 | Bool(布尔) | 只读 | +| `GPS IS DIRECTTO FLIGHTPLAN` | Is Direct To Waypoint mode active | 为Direct至Waypoint模式激活 | Bool(布尔) | 只读 | +| `GPS GROUND SPEED` | Current ground speed | 当前地面速度 | Meters per second | 只读 | +| `GPS GROUND TRUE HEADING` | Current true heading | 当前真航向 | Radians(弧度) | 只读 | +| `GPS GROUND MAGNETIC TRACK` | Current magnetic ground track | 当前磁地面track | Radians(弧度) | 只读 | +| `GPS GROUND TRUE TRACK` | Current true ground track | 当前真地面track | Radians(弧度) | 只读 | +| `GPS WP DISTANCE` | Distance to waypoint | GPS航路点距离 | Meters(米) | 只读 | +| `GPS WP BEARING` | Magnetic bearing to waypoint | GPS航路点方位 | Radians(弧度) | 只读 | +| `GPS WP TRUE BEARING` | True bearing to waypoint | GPS航路点真方位 | Radians(弧度) | 只读 | +| `GPS WP CROSS TRK` | Cross track distance | GPS航路点串TRK | Meters(米) | 只读 | +| `GPS WP DESIRED TRACK` | Desired track to waypoint | GPS航路点目标TRACK | Radians(弧度) | 只读 | +| `GPS WP TRUE REQ HDG` | Required true heading to waypoint | GPS航路点真REQHDG | Radians(弧度) | 只读 | +| `GPS WP VERTICAL SPEED` | Vertical speed to waypoint | GPS航路点垂直速度 | Meters per second | 只读 | +| `GPS WP TRACK ANGLE ERROR` | Tracking angle error to waypoint | GPS航路点TRACK角度误差 | Radians(弧度) | 只读 | +| `GPS ETE` | Estimated time enroute to destination | Estimated时间enroute至destination | Seconds(秒) | 只读 | +| `GPS ETA` | Estimated time of arrival at destination | Estimated时间的arrival在destination | Seconds(秒) | 只读 | +| `GPS WP NEXT LAT` | Latitude of next waypoint | 纬度的下一waypoint | Degrees(度) | 只读 | +| `GPS WP NEXT LON` | Longitude of next waypoint | 经度的下一waypoint | Degrees(度) | 只读 | +| `GPS WP NEXT ALT` | Altitude of next waypoint | GPS航路点下一高度 | Meters(米) | 只读 | +| `GPS WP PREV VALID` | Is previous waypoint valid (i.e. current waypoint is not the first waypoint) | GPS航路点上一有效 | Bool(布尔) | 只读 | +| `GPS WP PREV LAT` | Latitude of previous waypoint | GPS航路点上一LAT | Degrees(度) | 只读 | +| `GPS WP PREV LON` | Longitude of previous waypoint | GPS航路点上一LON | Degrees(度) | 只读 | +| `GPS WP PREV ALT` | Altitude of previous waypoint | GPS航路点上一高度 | Meters(米) | 只读 | +| `GPS WP ETE` | Estimated time enroute to waypoint | GPS航路点ETE | Seconds(秒) | 只读 | +| `GPS WP ETA` | Estimated time of arrival at waypoint | Estimated时间的arrival在waypoint | Seconds(秒) | 只读 | +| `GPS COURSE TO STEER` | Suggested heading to steer (for autopilot) | Suggested航向至转向(用于自动驾驶) | Radians(弧度) | 只读 | +| `GPS FLIGHT PLAN WP INDEX` | Index of waypoint | GPS飞行PLAN航路点索引 | Number(数值) | 只读 | +| `GPS FLIGHT PLAN WP COUNT` | Number of waypoints | GPS飞行PLAN航路点COUNT | Number(数值) | 只读 | +| `GPS IS ACTIVE WP LOCKED` | Is switching to next waypoint locked | GPSIS激活航路点LOCKED | Bool(布尔) | 只读 | +| `GPS IS APPROACH LOADED` | Is approach loaded | 为进近loaded | Bool(布尔) | 只读 | +| `GPS IS APPROACH ACTIVE` | Is approach mode active | 为进近模式激活 | Bool(布尔) | 只读 | +| `GPS APPROACH IS WP RUNWAY` | Waypoint is the runway | GPS进近IS航路点RUNWAY | Bool(布尔) | 只读 | +| `GPS APPROACH APPROACH INDEX` | Index of approach for given airport | 索引的进近用于givenairport | Number(数值) | 只读 | +| `GPS APPROACH TRANSITION INDEX` | Index of approach transition | 索引的进近transition | Number(数值) | 只读 | +| `GPS APPROACH IS FINAL` | Is approach transition final approach segment | 为进近transitionfinal进近segment | Bool(布尔) | 只读 | +| `GPS APPROACH IS MISSED` | Is approach segment missed approach segment | 为进近segmentmissed进近segment | Bool(布尔) | 只读 | +| `GPS APPROACH TIMEZONE DEVIATION` | Deviation of local time from GMT | 偏差的本地时间来自世界时 | Seconds(秒) | 只读 | +| `GPS APPROACH WP INDEX` | Index of current waypoint | GPS进近航路点索引 | Number(数值) | 只读 | +| `GPS APPROACH WP COUNT` | Number of waypoints | GPS进近航路点COUNT | Number(数值) | 只读 | +| `GPS DRIVES NAV1` | GPS is driving Nav 1 indicator | GPS为driving导航1指示器 | Bool(布尔) | 只读 | +| `COM RECEIVE ALL` | Toggles all COM radios to receive on | Toggles全部通讯radios至接收开 | Bool(布尔) | 只读 | +| `COM AVAILABLE` | True if either COM1 or COM2 is available | 当eitherCOM1或COM2为可用时为真 | Bool(布尔) | 只读 | +| `COM TEST:index` | Enter an index of 1 or 2. True if the COM system is working. | Enteran索引的1或2.真若the通讯系统为working. | Bool(布尔) | 只读 | +| `TRANSPONDER AVAILABLE` | True if a transponder is available | 当a应答机为可用时为真 | Bool(布尔) | 只读 | +| `ADF AVAILABLE` | True if ADF is available | 当ADF为可用时为真 | Bool(布尔) | 只读 | +| `ADF FREQUENCY:index` | Legacy, use ADF ACTIVE FREQUENCY | ADF频率(索引) | Frequency BCD16 | 只读 | +| `ADF EXT FREQUENCY:index` | Legacy, use ADF ACTIVE FREQUENCY | ADFEXT频率(索引) | Frequency BCD16 | 只读 | +| `ADF IDENT` | ICAO code | ADF识别 | String(字符串) | 只读 | +| `ADF NAME` | Descriptive name | ADF 描述名称 | String(字符串) | 只读 | +| `NAV IDENT` | ICAO code | 导航识别 | String(字符串) | 只读 | +| `NAV NAME` | Descriptive name | 描述性名称 | String(字符串) | 只读 | +| `NAV CODES:index` | Returns bit flags with the following meaning:; BIT7: 0= VOR 1= Localizer; BIT6: 1= glideslope available; BIT5: 1= no localizer backcourse; BIT4: 1= DME transmitter at glide slope transmitter; BIT3: 1= no nav signal available; BIT2: 1= voice available; BIT1: 1 = TACAN available; BIT0: 1= DME available | 返回位标志带the以下meaning:;BIT7:0=VOR1=航向道;BIT6:1=下滑道可用;BIT5:1=禁航向道背台;BIT4:1=DMEtransmitter在下滑道transmitter;BIT3:1=禁导航信号可用;BIT2:1=voice可用;BIT1:1=TACAN可用;BIT0:1=DME可用 | Flags(标志位) | 只读 | +| `NAV GLIDE SLOPE` | The glide slope gradient. | 导航下滑道 | Number(数值) | 只读 | +| `NAV RELATIVE BEARING TO STATION:index` | Relative bearing to station | 导航相对方位至台站(索引) | Degrees(度) | 只读 | +| `SELECTED DME` | Selected DME | 选中的 DME | Number(数值) | 只读 | +| `GPS WP NEXT ID` | ID of next GPS waypoint | GPS航路点下一ID | String(字符串) | 只读 | +| `GPS WP PREV ID` | ID of previous GPS waypoint | GPS航路点上一ID | String(字符串) | 只读 | +| `GPS TARGET DISTANCE` | Distance to target | 距离至目标 | Meters(米) | 只读 | +| `GPS TARGET ALTITUDE` | Altitude of GPS target | 高度的GPS目标 | Meters(米) | 只读 | + +## 七、Aircraft Controls Data(操纵/控制) + +共 **48** 项(可写 18 / 只读 30) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `YOKE Y POSITION` | Percent control deflection fore/aft (for animation) | 百分比控制偏转fore/aft(用于动画) | Position(位置) | **可写** | +| `YOKE X POSITION` | Percent control deflection left/right (for animation) | 百分比控制偏转左/右(用于动画) | Position(位置) | **可写** | +| `RUDDER PEDAL POSITION` | Percent rudder pedal deflection (for animation) | 百分比方向舵踏板偏转(用于动画) | Position(位置) | **可写** | +| `RUDDER POSITION` | Percent rudder input deflection | 百分比方向舵input偏转 | Position(位置) | **可写** | +| `ELEVATOR POSITION` | Percent elevator input deflection | 百分比升降舵input偏转 | Position(位置) | **可写** | +| `AILERON POSITION` | Percent aileron input left/right | 百分比副翼input左/右 | Position(位置) | **可写** | +| `ELEVATOR TRIM POSITION` | Elevator trim deflection | 升降舵配平位置 | Radians(弧度) | **可写** | +| `ELEVATOR TRIM INDICATOR` | Percent elevator trim (for indication) | 百分比升降舵配平(用于indication) | Position(位置) | 只读 | +| `ELEVATOR TRIM PCT` | Percent elevator trim | 升降舵配平百分比 | Percent Over 100(百分比(超100)) | 只读 | +| `BRAKE LEFT POSITION` | Percent left brake | 百分比左制动 | Position(位置) | **可写** | +| `BRAKE RIGHT POSITION` | Percent right brake | 百分比右制动 | Position(位置) | **可写** | +| `BRAKE INDICATOR` | Brake on indication | 制动指示器 | Position(位置) | 只读 | +| `BRAKE PARKING POSITION` | Parking brake on | 停留刹车位置 | Position(位置) | **可写** | +| `BRAKE PARKING INDICATOR` | Parking brake indicator | 停留刹车指示器 | Bool(布尔) | 只读 | +| `SPOILERS ARMED` | Auto-spoilers armed | 自动-扰流板预位 | Bool(布尔) | 只读 | +| `SPOILERS HANDLE POSITION` | Spoiler handle position | 扰流板手柄位置 | Percent Over 100(百分比(超100)) | **可写** | +| `SPOILERS LEFT POSITION` | Percent left spoiler deflected | 百分比左扰流板deflected | Percent Over 100(百分比(超100)) | 只读 | +| `SPOILERS RIGHT POSITION` | Percent right spoiler deflected | 百分比右扰流板deflected | Percent Over 100(百分比(超100)) | 只读 | +| `FLAPS HANDLE PERCENT` | Percent flap handle extended | 百分比襟翼手柄伸出 | Percent Over 100(百分比(超100)) | 只读 | +| `FLAPS HANDLE INDEX` | Index of current flap position | 索引的当前襟翼位置 | Number(数值) | **可写** | +| `FLAPS NUM HANDLE POSITIONS` | Number of flap positions | 襟翼数量手柄位置数 | Number(数值) | 只读 | +| `TRAILING EDGE FLAPS LEFT PERCENT` | Percent left trailing edge flap extended | 百分比左后缘缘襟翼伸出 | Percent Over 100(百分比(超100)) | **可写** | +| `TRAILING EDGE FLAPS RIGHT PERCENT` | Percent right trailing edge flap extended | 百分比右后缘缘襟翼伸出 | Percent Over 100(百分比(超100)) | **可写** | +| `TRAILING EDGE FLAPS LEFT ANGLE` | Angle left trailing edge flap extended. Use TRAILING EDGE FLAPS LEFT PERCENT to set a value. | 角左后缘缘襟翼伸出.Use后缘缘襟翼左百分比至设定avalue. | Radians(弧度) | 只读 | +| `TRAILING EDGE FLAPS RIGHT ANGLE` | Angle right trailing edge flap extended. Use TRAILING EDGE FLAPS RIGHT PERCENT to set a value. | 角右后缘缘襟翼伸出.Use后缘缘襟翼右百分比至设定avalue. | Radians(弧度) | 只读 | +| `LEADING EDGE FLAPS LEFT PERCENT` | Percent left leading edge flap extended | 百分比左前缘缘襟翼伸出 | Percent Over 100(百分比(超100)) | **可写** | +| `LEADING EDGE FLAPS RIGHT PERCENT` | Percent right leading edge flap extended | 百分比右前缘缘襟翼伸出 | Percent Over 100(百分比(超100)) | **可写** | +| `LEADING EDGE FLAPS LEFT ANGLE` | Angle left leading edge flap extended. Use LEADING EDGE FLAPS LEFT PERCENT to set a value. | 角左前缘缘襟翼伸出.Use前缘缘襟翼左百分比至设定avalue. | Radians(弧度) | 只读 | +| `LEADING EDGE FLAPS RIGHT ANGLE` | Angle right leading edge flap extended. Use LEADING EDGE FLAPS RIGHT PERCENT to set a value. | 角右前缘缘襟翼伸出.Use前缘缘襟翼右百分比至设定avalue. | Radians(弧度) | 只读 | +| `AILERON LEFT DEFLECTION` | Angle deflection | 左副翼偏转 | Radians(弧度) | 只读 | +| `AILERON LEFT DEFLECTION PCT` | Percent deflection | 左副翼偏转百分比 | Percent Over 100(百分比(超100)) | 只读 | +| `AILERON RIGHT DEFLECTION` | Angle deflection | 右副翼偏转 | Radians(弧度) | 只读 | +| `AILERON RIGHT DEFLECTION PCT` | Percent deflection | 右副翼偏转百分比 | Percent Over 100(百分比(超100)) | 只读 | +| `AILERON AVERAGE DEFLECTION` | Angle deflection | 副翼平均偏转 | Radians(弧度) | 只读 | +| `AILERON TRIM` | Angle deflection | 副翼配平 | Radians(弧度) | 只读 | +| `AILERON TRIM PCT` | The trim position of the ailerons. Zero is fully retracted. | 副翼配平位置,零为完全收回 | Percent over 100(百分比(超100)) | **可写** | +| `RUDDER DEFLECTION` | Angle deflection | 方向舵偏转 | Radians(弧度) | 只读 | +| `RUDDER DEFLECTION PCT` | Percent deflection | 方向舵偏转百分比 | Percent Over 100(百分比(超100)) | 只读 | +| `RUDDER TRIM` | Angle deflection | 方向舵配平 | Radians(弧度) | 只读 | +| `RUDDER TRIM PCT` | The trim position of the rudder. Zero is no trim. | 方向舵配平位置,零为无配平 | Percent over 100(百分比(超100)) | **可写** | +| `FLAPS AVAILABLE` | True if flaps available | 当襟翼可用时为真 | Bool(布尔) | 只读 | +| `FLAP DAMAGE BY SPEED` | True if flagps are damaged by excessive speed | 襟翼损伤BY速度 | Bool(布尔) | 只读 | +| `FLAP SPEED EXCEEDED` | True if safe speed limit for flaps exceeded | 当safe速度限制用于襟翼exceeded时为真 | Bool(布尔) | 只读 | +| `ELEVATOR DEFLECTION` | Angle deflection | 升降舵偏转 | Radians(弧度) | 只读 | +| `ELEVATOR DEFLECTION PCT` | Percent deflection | 升降舵偏转百分比 | Percent Over 100(百分比(超100)) | 只读 | +| `ALTERNATE STATIC SOURCE OPEN` | Alternate static air source | 备用静压源 | Bool(布尔) | 只读 | +| `FOLDING WING HANDLE POSITION` | True if the folding wing handle is engaged. | 当thefolding机翼手柄为engaged时为真 | Bool(布尔) | 只读 | +| `FUEL DUMP SWITCH` | If true the aircraft is dumping fuel at the rate set in the configuration file. | 若真the飞机为dumping燃油在the速率设定在theconfigurationfile. | Bool(布尔) | 只读 | + +## 八、Aircraft Autopilot Data(自动驾驶) + +共 **38** 项(可写 0 / 只读 38) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `AUTOPILOT AVAILABLE` | Available flag | 自动驾驶可用 | Bool(布尔) | 只读 | +| `AUTOPILOT MASTER` | On/off flag | 自动驾驶主开关 | Bool(布尔) | 只读 | +| `AUTOPILOT NAV SELECTED` | Index of Nav radio selected | 自动驾驶导航SELECTED | Number(数值) | 只读 | +| `AUTOPILOT WING LEVELER` | Wing leveler active | 自动驾驶改平 | Bool(布尔) | 只读 | +| `AUTOPILOT NAV1 LOCK` | True if autopilot nav1 lock applied | 当自动驾驶nav1锁定applied时为真 | Bool(布尔) | 只读 | +| `AUTOPILOT HEADING LOCK` | Heading mode active | 自动驾驶航向锁定 | Bool(布尔) | 只读 | +| `AUTOPILOT HEADING LOCK DIR` | Selected heading | 自动驾驶航向锁定方向 | Degrees(度) | 只读 | +| `AUTOPILOT ALTITUDE LOCK` | Altitude hole active | 自动驾驶高度锁定 | Bool(布尔) | 只读 | +| `AUTOPILOT ALTITUDE LOCK VAR` | Selected altitude | 自动驾驶高度锁定VAR | Feet(英尺) | 只读 | +| `AUTOPILOT ATTITUDE HOLD` | Attitude hold active | 自动驾驶姿态保持 | Bool(布尔) | 只读 | +| `AUTOPILOT GLIDESLOPE HOLD` | GS hold active | 自动驾驶下滑道保持 | Bool(布尔) | 只读 | +| `AUTOPILOT PITCH HOLD REF` | Current reference pitch | 自动驾驶俯仰保持REF | Radians(弧度) | 只读 | +| `AUTOPILOT APPROACH HOLD` | Approach mode active | 自动驾驶进近保持 | Bool(布尔) | 只读 | +| `AUTOPILOT BACKCOURSE HOLD` | Back course mode active | 自动驾驶背台保持 | Bool(布尔) | 只读 | +| `AUTOPILOT VERTICAL HOLD VAR` | Selected vertical speed | 自动驾驶垂直保持VAR | Feet/minute | 只读 | +| `AUTOPILOT PITCH HOLD` | Set to True if the autopilot pitch hold has is engaged. | 设定至真若the自动驾驶俯仰保持具有为engaged. | Bool(布尔) | 只读 | +| `AUTOPILOT FLIGHT DIRECTOR ACTIVE` | Flight director active | 飞行指引仪激活 | Bool(布尔) | 只读 | +| `AUTOPILOT FLIGHT DIRECTOR PITCH` | Reference pitch angle | 自动驾驶飞行指引俯仰 | Radians(弧度) | 只读 | +| `AUTOPILOT FLIGHT DIRECTOR BANK` | Reference bank angle | 自动驾驶飞行指引坡度 | Radians(弧度) | 只读 | +| `AUTOPILOT AIRSPEED HOLD` | Airspeed hold active | 自动驾驶空速保持 | Bool(布尔) | 只读 | +| `AUTOPILOT AIRSPEED HOLD VAR` | Selected airspeed | 自动驾驶空速保持VAR | Knots(节) | 只读 | +| `AUTOPILOT MACH HOLD` | Mach hold active | 自动驾驶MACH保持 | Bool(布尔) | 只读 | +| `AUTOPILOT MACH HOLD VAR` | Selected mach | 自动驾驶MACH保持VAR | Number(数值) | 只读 | +| `AUTOPILOT YAW DAMPER` | Yaw damper active | 偏航阻尼器 | Bool(布尔) | 只读 | +| `AUTOPILOT RPM HOLD VAR` | Selected rpm | 自动驾驶转速保持VAR | Number(数值) | 只读 | +| `AUTOPILOT THROTTLE ARM` | Autothrottle armed | 自动驾驶油门ARM | Bool(布尔) | 只读 | +| `AUTOPILOT TAKEOFF POWER ACTIVE` | Takeoff / Go Around power mode active | 起飞功率激活 | Bool(布尔) | 只读 | +| `AUTOTHROTTLE ACTIVE` | Auto-throttle active | 自动-油门激活 | Bool(布尔) | 只读 | +| `AUTOPILOT VERTICAL HOLD` | True if autopilot vertical hold applied | 当自动驾驶垂直保持applied时为真 | Bool(布尔) | 只读 | +| `AUTOPILOT RPM HOLD` | True if autopilot rpm hold applied | 当自动驾驶转速保持applied时为真 | Bool(布尔) | 只读 | +| `AUTOPILOT MAX BANK` | True if autopilot max bank applied | 当自动驾驶最大坡度applied时为真 | Radians(弧度) | 只读 | +| `FLY BY WIRE ELAC SWITCH` | True if the fly by wire Elevators and Ailerons computer is on. | 当thefly由wireElevators和Aileronscomputer为开时为真 | Bool(布尔) | 只读 | +| `FLY BY WIRE FAC SWITCH` | True if the fly by wire Flight Augmentation computer is on. | 当thefly由wire飞行Augmentationcomputer为开时为真 | Bool(布尔) | 只读 | +| `FLY BY WIRE SEC SWITCH` | True if the fly by wire Spoilers and Elevators computer is on. | 当thefly由wire扰流板和Elevatorscomputer为开时为真 | Bool(布尔) | 只读 | +| `FLY BY WIRE ELAC FAILED` | True if the Elevators and Ailerons computer has failed. | 当theElevators和Aileronscomputer具有故障时为真 | Bool(布尔) | 只读 | +| `FLY BY WIRE FAC FAILED` | True if the Flight Augmentation computer has failed. | 当the飞行Augmentationcomputer具有故障时为真 | Bool(布尔) | 只读 | +| `FLY BY WIRE SEC FAILED` | True if the Spoilers and Elevators computer has failed. | 当the扰流板和Elevatorscomputer具有故障时为真 | Bool(布尔) | 只读 | +| `AUTOPILOT FLIGHT LEVEL CHANGE` | True if autopilot FLC mode applied | 当自动驾驶FLC模式applied时为真 | Bool(布尔) | 只读 | + +## 九、Aircraft Landing Gear Data(起落架) + +共 **54** 项(可写 6 / 只读 48) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `IS GEAR RETRACTABLE` | True if gear can be retracted | 当起落架canberetracted时为真 | Bool(布尔) | 只读 | +| `IS GEAR SKIS` | True if landing gear is skis | 当着陆起落架为skis时为真 | Bool(布尔) | 只读 | +| `IS GEAR FLOATS` | True if landing gear is floats | 当着陆起落架为浮筒时为真 | Bool(布尔) | 只读 | +| `IS GEAR SKIDS` | True if landing gear is skids | 当着陆起落架为skids时为真 | Bool(布尔) | 只读 | +| `IS GEAR WHEELS` | True if landing gear is wheels | 当着陆起落架为wheels时为真 | Bool(布尔) | 只读 | +| `GEAR HANDLE POSITION` | True if gear handle is applied | 当起落架手柄为applied时为真 | Bool(布尔) | **可写** | +| `GEAR HYDRAULIC PRESSURE` | Gear hydraulic pressure | 起落架液压压力 | psf | 只读 | +| `TAILWHEEL LOCK ON` | True if tailwheel lock applied | 当tailwheel锁定applied时为真 | Bool(布尔) | 只读 | +| `GEAR CENTER POSITION` | Percent center gear extended | 百分比中央起落架伸出 | Percent Over 100(百分比(超100)) | **可写** | +| `GEAR LEFT POSITION` | Percent left gear extended | 百分比左起落架伸出 | Percent Over 100(百分比(超100)) | **可写** | +| `GEAR RIGHT POSITION` | Percent right gear extended | 百分比右起落架伸出 | Percent Over 100(百分比(超100)) | **可写** | +| `GEAR TAIL POSITION` | Percent tail gear extended | 百分比tail起落架伸出 | Percent Over 100(百分比(超100)) | 只读 | +| `GEAR AUX POSITION` | Percent auxiliary gear extended | 百分比辅助起落架伸出 | Percent Over 100(百分比(超100)) | 只读 | +| `GEAR POSITION:index` | Position of landing gear:; 0 = unknown; 1 = up; 2 = down | 位置的着陆起落架:;0=unknown;1=收起;2=down | Enum(枚举) | **可写** | +| `GEAR ANIMATION POSITION:index` | Percent gear animation extended | 百分比起落架动画伸出 | Number(数值) | 只读 | +| `GEAR TOTAL PCT EXTENDED` | Percent total gear extended | 起落架总放出百分比 | Percentage | 只读 | +| `AUTO BRAKE SWITCH CB` | Auto brake switch position | 自动制动开关位置 | Number(数值) | 只读 | +| `WATER RUDDER HANDLE POSITION` | Position of the water rudder handle (0 handle retracted, 100 rudder handle applied) | 位置的the水方向舵手柄(0手柄retracted,100方向舵手柄applied) | Percent Over 100(百分比(超100)) | **可写** | +| `WATER LEFT RUDDER EXTENDED` | Percent extended | 水左方向舵伸出 | Percentage | 只读 | +| `WATER RIGHT RUDDER EXTENDED` | Percent extended | 水右方向舵伸出 | Percentage | 只读 | +| `GEAR CENTER STEER ANGLE` | Center wheel angle, negative to the left, positive to the right. | 起落架中央转向角度 | Percent Over 100(百分比(超100)) | 只读 | +| `GEAR LEFT STEER ANGLE` | Left wheel angle, negative to the left, positive to the right. | 起落架左转向角度 | Percent Over 100(百分比(超100)) | 只读 | +| `GEAR RIGHT STEER ANGLE` | Right wheel angle, negative to the left, positive to the right. | 起落架右转向角度 | Percent Over 100(百分比(超100)) | 只读 | +| `GEAR AUX STEER ANGLE` | Aux wheel angle, negative to the left, positive to the right. The aux wheel is the fourth set of gear, sometimes used on helicopters. | 辅助机轮角,negative至the左,positive至the右.The辅助机轮为thefourth设定的起落架,sometimes使用开直升机. | Percent Over 100(百分比(超100)) | 只读 | +| `GEAR STEER ANGLE:index` | Alternative method of getting the steer angle. Index is; 0 = center; 1 = left; 2 = right; 3 = aux | Alternativemethod的gettingthe转向角.索引为;0=中央;1=左;2=右;3=辅助 | Percent Over 100(百分比(超100)) | 只读 | +| `WATER LEFT RUDDER STEER ANGLE` | Water left rudder angle, negative to the left, positive to the right. | 水左方向舵转向角度 | Percent Over 100(百分比(超100)) | 只读 | +| `WATER RIGHT RUDDER STEER ANGLE` | Water right rudder angle, negative to the left, positive to the right. | 水右方向舵转向角度 | Percent Over 100(百分比(超100)) | 只读 | +| `GEAR CENTER STEER ANGLE PCT` | Center steer angle as a percentage | 起落架中央转向角度百分比 | Percent Over 100(百分比(超100)) | 只读 | +| `GEAR LEFT STEER ANGLE PCT` | Left steer angle as a percentage | 起落架左转向角度百分比 | Percent Over 100(百分比(超100)) | 只读 | +| `GEAR RIGHT STEER ANGLE PCT` | Right steer angle as a percentage | 起落架右转向角度百分比 | Percent Over 100(百分比(超100)) | 只读 | +| `GEAR AUX STEER ANGLE PCT` | Aux steer angle as a percentage | 起落架辅助转向角度百分比 | Percent Over 100(百分比(超100)) | 只读 | +| `GEAR STEER ANGLE PCT:index` | Alternative method of getting steer angle as a percentage. Index is; 0 = center; 1 = left; 2 = right; 3 = aux | Alternativemethod的getting转向角作为a百分比.索引为;0=中央;1=左;2=右;3=辅助 | Percent Over 100(百分比(超100)) | 只读 | +| `WATER LEFT RUDDER STEER ANGLE PCT` | Water left rudder angle as a percentage | 水左方向舵转向角度百分比 | Percent Over 100(百分比(超100)) | 只读 | +| `WATER RIGHT RUDDER STEER ANGLE PCT` | Water right rudder as a percentage | 水右方向舵转向角度百分比 | Percent Over 100(百分比(超100)) | 只读 | +| `WHEEL RPM:index` | Wheel rpm. Index is; 0 = center; 1 = left; 2 = right; 3 = aux | 机轮转速.索引为;0=中央;1=左;2=右;3=辅助 | Rpm(转/分) | 只读 | +| `CENTER WHEEL RPM` | Center landing gear rpm | 中央着陆起落架转速 | Rpm(转/分) | 只读 | +| `LEFT WHEEL RPM` | Left landing gear rpm | 左着陆起落架转速 | Rpm(转/分) | 只读 | +| `RIGHT WHEEL RPM` | Right landing gear rpm | 右着陆起落架转速 | Rpm(转/分) | 只读 | +| `AUX WHEEL RPM` | Rpm of fourth set of gear wheels. | 转速的fourth设定的起落架wheels. | Rpm(转/分) | 只读 | +| `WHEEL ROTATION ANGLE:index` | Wheel rotation angle. Index is; 0 = center; 1 = left; 2 = right; 3 = aux | 机轮旋转角.索引为;0=中央;1=左;2=右;3=辅助 | Radians(弧度) | 只读 | +| `CENTER WHEEL ROTATION ANGLE` | Center wheel rotation angle | 中央机轮旋转角度 | Radians(弧度) | 只读 | +| `LEFT WHEEL ROTATION ANGLE` | Left wheel rotation angle | 左机轮旋转角度 | Radians(弧度) | 只读 | +| `RIGHT WHEEL ROTATION ANGLE` | Right wheel rotation angle | 右机轮旋转角度 | Radians(弧度) | 只读 | +| `AUX WHEEL ROTATION ANGLE` | Aux wheel rotation angle | 辅助机轮旋转角度 | Radians(弧度) | 只读 | +| `GEAR EMERGENCY HANDLE POSITION` | True if gear emergency handle applied | 当起落架紧急手柄applied时为真 | Bool(布尔) | 只读 | +| `GEAR WARNING` | One of:; 0: unknown; 1: normal; 2: amphib | 起落架警告 | Enum(枚举) | 只读 | +| `ANTISKID BRAKES ACTIVE` | True if antiskid brakes active | 当antiskidbrakes激活时为真 | Bool(布尔) | 只读 | +| `RETRACT FLOAT SWITCH` | True if retract float switch on | 当收放浮筒开关开时为真 | Bool(布尔) | 只读 | +| `RETRACT LEFT FLOAT EXTENDED` | If aircraft has retractable floats. | 左浮筒收放位置 | Percent(百分比) | 只读 | +| `RETRACT RIGHT FLOAT EXTENDED` | If aircraft has retractable floats. | 右浮筒收放位置 | Percent(百分比) | 只读 | +| `STEER INPUT CONTROL` | Position of steering tiller | 转向INPUT控制 | Percent over 100(百分比(超100)) | 只读 | +| `GEAR DAMAGE BY SPEED` | True if gear has been damaged by excessive speed | 当起落架具有beendamaged由excessive速度时为真 | Bool(布尔) | 只读 | +| `GEAR SPEED EXCEEDED` | True if safe speed limit for gear exceeded | 当safe速度限制用于起落架exceeded时为真 | Bool(布尔) | 只读 | +| `NOSEWHEEL LOCK ON` | True if the nosewheel lock is engaged. | 当thenosewheel锁定为engaged时为真 | Bool(布尔) | 只读 | + +## 十、Aircraft Environment Data(飞机环境) + +共 **19** 项(可写 0 / 只读 19) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `AMBIENT DENSITY` | Ambient density | 空气密度 | Slugs per cubic feet | 只读 | +| `AMBIENT TEMPERATURE` | Ambient temperature | 环境温度 | Celsius(摄氏度) | 只读 | +| `AMBIENT PRESSURE` | Ambient pressure | 环境压力 | inHg(英寸汞柱) | 只读 | +| `AMBIENT WIND VELOCITY` | Wind velocity | 环境风速度 | Knots(节) | 只读 | +| `AMBIENT WIND DIRECTION` | Wind direction | 环境风方向 | Degrees(度) | 只读 | +| `AMBIENT WIND X` | Wind component in East/West direction. | 环境风横向 | Meters per second | 只读 | +| `AMBIENT WIND Y` | Wind component in vertical direction. | 风component在垂直方向. | Meters per second | 只读 | +| `AMBIENT WIND Z` | Wind component in North/South direction. | 环境风垂向 | Meters per second | 只读 | +| `STRUCT AMBIENT WIND` | X (latitude), Y (vertical) and Z (longitude) components of the wind. | 横向(纬度),纵向(垂直)和垂向(经度)components的the风. | Feet per second(英尺/秒) | 只读 | +| `AIRCRAFT WIND X` | Wind component in aircraft lateral axis | 风component在飞机lateralaxis | Knots(节) | 只读 | +| `AIRCRAFT WIND Y` | Wind component in aircraft vertical axis | 风component在飞机垂直axis | Knots(节) | 只读 | +| `AIRCRAFT WIND Z` | Wind component in aircraft longitudinal axis | 风component在飞机longitudinalaxis | Knots(节) | 只读 | +| `BAROMETER PRESSURE` | Barometric pressure | BAROMETER压力 | Millibars(毫巴) | 只读 | +| `SEA LEVEL PRESSURE` | Barometric pressure at sea level | Barometric压力在海液位 | Millibars(毫巴) | 只读 | +| `TOTAL AIR TEMPERATURE` | Total air temperature is the air temperature at the front of the aircraft where the ram pressure from the speed of the aircraft is taken into account. | 总空气温度为the空气温度在thefront的the飞机wheretheram压力来自the速度的the飞机为takenintoaccount. | Celsius(摄氏度) | 只读 | +| `WINDSHIELD RAIN EFFECT AVAILABLE` | Is visual effect available on this aircraft | 为visualeffect可用开此飞机 | Bool(布尔) | 只读 | +| `AMBIENT IN CLOUD` | True if the aircraft is in a cloud. | 当the飞机为在a云时为真 | Bool(布尔) | 只读 | +| `AMBIENT VISIBILITY` | Ambient visibility | 环境能见度 | Meters(米) | 只读 | +| `STANDARD ATM TEMPERATURE` | Outside temperature on the standard ATM scale | 标准大气温度 | Rankine(兰氏度) | 只读 | + +## 十一、Helicopter Specific Data(直升机) + +共 **18** 项(可写 0 / 只读 18) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `ROTOR BRAKE HANDLE POS` | Percent actuated | 旋翼制动手柄POS | Percent Over 100(百分比(超100)) | 只读 | +| `ROTOR BRAKE ACTIVE` | Active | 激活 | Bool(布尔) | 只读 | +| `ROTOR CLUTCH SWITCH POS` | Switch position | 开关位置 | Bool(布尔) | 只读 | +| `ROTOR CLUTCH ACTIVE` | Active | 激活 | Bool(布尔) | 只读 | +| `ROTOR TEMPERATURE` | Main rotor transmission temperature | 主旋翼传动温度 | Rankine(兰氏度) | 只读 | +| `ROTOR CHIP DETECTED` | Chip detection | 旋翼CHIP探测 | Bool(布尔) | 只读 | +| `ROTOR GOV SWITCH POS` | Switch position | 开关位置 | Bool(布尔) | 只读 | +| `ROTOR GOV ACTIVE` | Active | 激活 | Bool(布尔) | 只读 | +| `ROTOR LATERAL TRIM PCT` | Trim percent | 旋翼LATERAL配平百分比 | Percent Over 100(百分比(超100)) | 只读 | +| `ROTOR RPM PCT` | Percent max rated rpm | 旋翼转速百分比 | Percent Over 100(百分比(超100)) | 只读 | +| `ENG TURBINE TEMPERATURE` | Turbine temperature. Applies only to Bell helicopter. | 涡轮温度.Applies仅至Bell直升机. | Celsius(摄氏度) | 只读 | +| `ENG TORQUE PERCENT:index` | Torque. Returns main rotor torque for Bell helicopter, or the indexed rotor torque of other helicopters. | 扭矩.返回主旋翼扭矩用于Bell直升机,或theindexed旋翼扭矩的other直升机. | Percent scalar 16K (Ft/lbs * 16384) | 只读 | +| `ENG FUEL PRESSURE` | Fuel pressure. Applies only to Bell helicopter. | 燃油压力.Applies仅至Bell直升机. | PSI(磅/平方英寸) | 只读 | +| `ENG ELECTRICAL LOAD` | Electrical load. Applies only to Bell helicopter. | 电气负载.Applies仅至Bell直升机. | Percent(百分比) | 只读 | +| `ENG TRANSMISSION PRESSURE` | Transmission pressure. Applies only to Bell helicopter. | 传动压力.Applies仅至Bell直升机. | PSI(磅/平方英寸) | 只读 | +| `ENG TRANSMISSION TEMPERATURE` | Transmission temperature. Applies only to Bell helicopter. | 传动温度.Applies仅至Bell直升机. | Celsius(摄氏度) | 只读 | +| `ENG ROTOR RPM:index` | Rotor rpm. Returns main rotor rpm for Bell helicopter, or the indexed rotor rpm of other helicopters. | 旋翼转速.返回主旋翼转速用于Bell直升机,或theindexed旋翼转速的other直升机. | Percent scalar 16K (Max rpm * 16384) | 只读 | +| `COLLECTIVE POSITION` | The position of the helicopter's collective. 0 is fully up, 100 fully depressed. | The位置的the直升机's总距.0为完全收起,100完全depressed. | Percent over 100(百分比(超100)) | 只读 | + +## 十二、Slings and Hoists(吊索/绞车) + +共 **9** 项(可写 2 / 只读 7) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `NUM SLING CABLES` | The number of sling cables (not hoists) that are configured for the aircraft. Refer to the document Notes on Aircraft Systems. | The数量的吊索cables(nothoists)thatareconfigured用于the飞机.Refer至thedocumentNotes开飞机Systems. | Number(数值) | 只读 | +| `PAYLOAD STATION OBJECT:index` | Places the named object at the payload station identified by the index (starting from 1). The string is the Container name (refer to the title property of Simulation Object Configuration Files). | Placesthenamed对象在thepayload台站identified由the索引(starting来自1).The字符串为theContainer名称(refer至the标题property的模拟对象ConfigurationFiles). | String(字符串) | 只读 | +| `PAYLOAD STATION NUM SIMOBJECTS:index` | The number of objects at the payload station (indexed from 1). | The数量的objects在thepayload台站(indexed来自1). | Number(数值) | 只读 | +| `SLING OBJECT ATTACHED:index` | If units are set as boolean, returns True if a sling object is attached. If units are set as a string, returns the container title of the object. There can be multiple sling positions, indexed from 1. The sling positions are set in the Aircraft Configuration File. | 若单位are设定作为boolean,返回真若a吊索对象为已连接.若单位are设定作为a字符串,返回thecontainer标题的the对象.Therecanbemultiple吊索位置数,indexed来自1.The吊索位置数are设定在the飞机ConfigurationFile. | Bool/String | 只读 | +| `SLING CABLE BROKEN:index` | True if the cable is broken. | 当the索为断裂时为真 | Bool(布尔) | 只读 | +| `SLING CABLE EXTENDED LENGTH:index` | The length of the cable extending from the aircraft. | 吊索伸出长度(索引) | Feet(英尺) | **可写** | +| `SLING ACTIVE PAYLOAD STATION:index` | The payload station (identified by the parameter) where objects will be placed from the sling (identified by the index). | 吊索激活PAYLOAD台站(索引) | Number(数值) | **可写** | +| `SLING HOIST PERCENT DEPLOYED:index` | The percentage of the full length of the sling cable deployed. | The百分比的thefull长度的the吊索索放出. | Percent over 100(百分比(超100)) | 只读 | +| `IS ATTACHED TO SLING` | Set to true if this object is attached to a sling. | 设定至真若此对象为已连接至a吊索. | Bool(布尔) | 只读 | + +## 十三、Aircraft Miscellaneous Systems Data(杂项系统) + +共 **50** 项(可写 24 / 只读 26) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `SMOKE ENABLE` | Set to True to activate the smoke system, if one is available (for example, on the Extra). | 设定至真至activatethesmoke系统,若一为可用(用于example,开theExtra). | Bool(布尔) | **可写** | +| `SMOKESYSTEM AVAILABLE` | Smoke system available | Smoke系统可用 | Bool(布尔) | 只读 | +| `PITOT HEAT` | Pitot heat active | 皮托heat激活 | Bool(布尔) | 只读 | +| `FOLDING WING LEFT PERCENT` | Left folding wing position, 100 is fully folded | 左folding机翼位置,100为完全folded | Percent Over 100(百分比(超100)) | **可写** | +| `FOLDING WING RIGHT PERCENT` | Right folding wing position, 100 is fully folded | 右folding机翼位置,100为完全folded | Percent Over 100(百分比(超100)) | **可写** | +| `CANOPY OPEN` | Percent primary door/exit open | 百分比主door/出口打开 | Percent Over 100(百分比(超100)) | **可写** | +| `TAILHOOK POSITION` | Percent tail hook extended | 百分比tail钩伸出 | Percent Over 100(百分比(超100)) | **可写** | +| `EXIT OPEN:index` | Percent door/exit open | 百分比door/出口打开 | Percent Over 100(百分比(超100)) | **可写** | +| `STALL HORN AVAILABLE` | True if stall alarm available | 当stallalarm可用时为真 | Bool(布尔) | 只读 | +| `ENGINE MIXURE AVAILABLE` | True if engine mixture is available for prop engines. Obsolete value as mixture is always available. Spelling error in variable name. | 当发动机混合比为可用用于prop发动机.Obsoletevalue作为混合比为always可用.Spelling误差在variable名称时为真 | Bool(布尔) | 只读 | +| `CARB HEAT AVAILABLE` | True if carb heat available | 当carbheat可用时为真 | Bool(布尔) | 只读 | +| `SPOILER AVAILABLE` | True if spoiler system available | 当扰流板系统可用时为真 | Bool(布尔) | 只读 | +| `IS TAIL DRAGGER` | True if the aircraft is a taildragger | 当the飞机为ataildragger时为真 | Bool(布尔) | 只读 | +| `STROBES AVAILABLE` | True if strobe lights are available | 当频闪lightsare可用时为真 | Bool(布尔) | 只读 | +| `TOE BRAKES AVAILABLE` | True if toe brakes are available | 当toebrakesare可用时为真 | Bool(布尔) | 只读 | +| `PUSHBACK STATE` | Type of pushback :; 0 = Straight; 1 = Left; 2 = Right | 类型的pushback:;0=Straight;1=左;2=右 | Enum(枚举) | **可写** | +| `ELECTRICAL MASTER BATTERY` | Battery switch position | Battery开关位置 | Bool(布尔) | **可写** | +| `ELECTRICAL TOTAL LOAD AMPS` | Total load amps | 电气总负载AMPS | Amperes | **可写** | +| `ELECTRICAL BATTERY LOAD` | Battery load | 电气BATTERY负载 | Amperes | **可写** | +| `ELECTRICAL BATTERY VOLTAGE` | Battery voltage | 电气BATTERYVOLTAGE | Volts(伏特) | **可写** | +| `ELECTRICAL MAIN BUS VOLTAGE` | Main bus voltage | 电气主BUSVOLTAGE | Volts(伏特) | **可写** | +| `ELECTRICAL MAIN BUS AMPS` | Main bus current | 电气主BUSAMPS | Amperes | **可写** | +| `ELECTRICAL AVIONICS BUS VOLTAGE` | Avionics bus voltage | 电气航电BUSVOLTAGE | Volts(伏特) | **可写** | +| `ELECTRICAL AVIONICS BUS AMPS` | Avionics bus current | 电气航电BUSAMPS | Amperes | **可写** | +| `ELECTRICAL HOT BATTERY BUS VOLTAGE` | Voltage available when battery switch is turned off | 电压可用当battery开关为turned关 | Volts(伏特) | **可写** | +| `ELECTRICAL HOT BATTERY BUS AMPS` | Current available when battery switch is turned off | 当前可用当battery开关为turned关 | Amperes | **可写** | +| `ELECTRICAL BATTERY BUS VOLTAGE` | Battery bus voltage | 电气BATTERYBUSVOLTAGE | Volts(伏特) | **可写** | +| `ELECTRICAL BATTERY BUS AMPS` | Battery bus current | 电气BATTERYBUSAMPS | Amperes | **可写** | +| `ELECTRICAL GENALT BUS VOLTAGE:index` | Genalt bus voltage (takes engine index) | Genaltbus电压(takes发动机索引) | Volts(伏特) | **可写** | +| `ELECTRICAL GENALT BUS AMPS:index` | Genalt bus current (takes engine index) | Genaltbus当前(takes发动机索引) | Amperes | **可写** | +| `CIRCUIT GENERAL PANEL ON` | Is electrical power available to this circuit | 为电气动力可用至此circuit | Bool(布尔) | 只读 | +| `CIRCUIT FLAP MOTOR ON` | Is electrical power available to this circuit | 为电气动力可用至此circuit | Bool(布尔) | 只读 | +| `CIRCUIT GEAR MOTOR ON` | Is electrical power available to this circuit | 为电气动力可用至此circuit | Bool(布尔) | 只读 | +| `CIRCUIT AUTOPILOT ON` | Is electrical power available to this circuit | 为电气动力可用至此circuit | Bool(布尔) | 只读 | +| `CIRCUIT AVIONICS ON` | Is electrical power available to this circuit | 为电气动力可用至此circuit | Bool(布尔) | 只读 | +| `CIRCUIT PITOT HEAT ON` | Is electrical power available to this circuit | 为电气动力可用至此circuit | Bool(布尔) | 只读 | +| `CIRCUIT PROP SYNC ON` | Is electrical power available to this circuit | 为电气动力可用至此circuit | Bool(布尔) | 只读 | +| `CIRCUIT AUTO FEATHER ON` | Is electrical power available to this circuit | 为电气动力可用至此circuit | Bool(布尔) | 只读 | +| `CIRCUIT AUTO BRAKES ON` | Is electrical power available to this circuit | 为电气动力可用至此circuit | Bool(布尔) | 只读 | +| `CIRCUIT STANDY VACUUM ON` | Is electrical power available to this circuit | 为电气动力可用至此circuit | Bool(布尔) | 只读 | +| `CIRCUIT MARKER BEACON ON` | Is electrical power available to this circuit | 为电气动力可用至此circuit | Bool(布尔) | 只读 | +| `CIRCUIT GEAR WARNING ON` | Is electrical power available to this circuit | 为电气动力可用至此circuit | Bool(布尔) | 只读 | +| `CIRCUIT HYDRAULIC PUMP ON` | Is electrical power available to this circuit | 为电气动力可用至此circuit | Bool(布尔) | 只读 | +| `HYDRAULIC PRESSURE:index` | Hydraulic system pressure. Indexes start at 1. | 液压系统压力.Indexes开始在1. | Pound force per square foot | 只读 | +| `HYDRAULIC RESERVOIR PERCENT:index` | Hydraulic pressure changes will follow changes to this variable. Indexes start at 1. | 液压储液罐百分比(索引) | Percent Over 100(百分比(超100)) | **可写** | +| `HYDRAULIC SYSTEM INTEGRITY` | Percent system functional | 百分比系统functional | Percent Over 100(百分比(超100)) | 只读 | +| `STRUCTURAL DEICE SWITCH` | True if the aircraft structure deice switch is on | 当the飞机structure除冰开关为开时为真 | Bool(布尔) | 只读 | +| `APPLY HEAT TO SYSTEMS` | Used when too close to a fire. | 使用当tooclose至a火. | Bool(布尔) | **可写** | +| `DROPPABLE OBJECTS TYPE:index` | The type of droppable object at the station number identified by the index. | The类型的droppable对象在the台站数量identified由the索引. | String(字符串) | **可写** | +| `DROPPABLE OBJECTS COUNT:index` | The number of droppable objects at the station number identified by the index. | The数量的droppableobjects在the台站数量identified由the索引. | Number(数值) | 只读 | + +## 十四、Miscellaneous Data(杂项) + +共 **159** 项(可写 14 / 只读 145) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `TOTAL WEIGHT` | Total weight of the aircraft | 总重量的the飞机 | Pounds(磅) | 只读 | +| `MAX GROSS WEIGHT` | Maximum gross weight of the aircaft | 最大gross重量的theaircaft | Pounds(磅) | 只读 | +| `EMPTY WEIGHT` | Empty weight of the aircraft | Empty重量的the飞机 | Pounds(磅) | 只读 | +| `IS USER SIM` | Is this the user loaded aircraft | 为此the用户loaded飞机 | Bool(布尔) | 只读 | +| `SIM DISABLED` | Is sim disabled | 模拟器是否禁用 | Bool(布尔) | **可写** | +| `G FORCE` | Current g force | 当前gforce | GForce | **可写** | +| `ATC HEAVY` | Is this aircraft recognized by ATC as heavy | 为此飞机recognized由ATC作为heavy | Bool(布尔) | **可写** | +| `AUTO COORDINATION` | Is auto-coordination active | 为自动-coordination激活 | Bool(布尔) | **可写** | +| `REALISM` | General realism percent | 通用realism百分比 | Number(数值) | **可写** | +| `TRUE AIRSPEED SELECTED` | True if True Airspeed has been selected | 当真空速具有beenselected时为真 | Bool(布尔) | **可写** | +| `DESIGN SPEED VS0` | Design speed at VS0 | 设计速度在VS0 | Feet per second(英尺/秒) | 只读 | +| `DESIGN SPEED VS1` | Design speed at VS1 | 设计速度在VS1 | Feet per second(英尺/秒) | 只读 | +| `DESIGN SPEED VC` | Design speed at VC | 设计速度在VC | Feet per second(英尺/秒) | 只读 | +| `MIN DRAG VELOCITY` | Minimum drag velocity | 最小drag速度 | Feet per second(英尺/秒) | 只读 | +| `ESTIMATED CRUISE SPEED` | Estimated cruise speed | ESTIMATEDCRUISE速度 | Feet per second(英尺/秒) | 只读 | +| `CG PERCENT` | Longitudinal CG position as a percent of reference chord | Longitudinal重心位置作为a百分比的referencechord | Percent over 100(百分比(超100)) | 只读 | +| `CG PERCENT LATERAL` | Lateral CG position as a percent of reference chord | Lateral重心位置作为a百分比的referencechord | Percent over 100(百分比(超100)) | 只读 | +| `IS SLEW ACTIVE` | True if slew is active | 当slew为激活时为真 | Bool(布尔) | **可写** | +| `IS SLEW ALLOWED` | True if slew is enabled | 当slew为enabled时为真 | Bool(布尔) | **可写** | +| `ATC SUGGESTED MIN RWY TAKEOFF` | Suggested minimum runway length for takeoff. Used by ATC | Suggested最小runway长度用于takeoff.使用由ATC | Feet(英尺) | 只读 | +| `ATC SUGGESTED MIN RWY LANDING` | Suggested minimum runway length for landing. Used by ATC | Suggested最小runway长度用于着陆.使用由ATC | Feet(英尺) | 只读 | +| `PAYLOAD STATION WEIGHT:index` | Individual payload station weight | PAYLOAD台站重量(索引) | Pounds(磅) | **可写** | +| `PAYLOAD STATION COUNT` | Number of payload stations | 数量的payloadstations | Number(数值) | 只读 | +| `USER INPUT ENABLED` | Is input allowed from the user | 为inputallowed来自the用户 | Bool(布尔) | **可写** | +| `TYPICAL DESCENT RATE` | Normal descent rate | TYPICALDESCENT速率 | Feet per minute | 只读 | +| `VISUAL MODEL RADIUS` | Model radius | VISUALMODEL半径 | Meters(米) | 只读 | +| `SIGMA SQRT` | Sigma sqrt | 西格玛平方根 | Number(数值) | 只读 | +| `DYNAMIC PRESSURE` | Dynamic pressure | DYNAMIC压力 | foot pounds | 只读 | +| `TOTAL VELOCITY` | Velocity regardless of direction. For example, if a helicopter is ascending vertically at 100 fps, getting this variable will return 100. | 速度regardless的方向.用于example,若a直升机为ascendingvertically在100fps,getting此variablewill返回100. | Feet per second(英尺/秒) | 只读 | +| `AIRSPEED SELECT INDICATED OR TRUE` | The airspeed, whether true or indicated airspeed has been selected. | The空速,whether真或指示空速具有beenselected. | Knots(节) | 只读 | +| `VARIOMETER RATE` | Variometer rate | VARIOMETER速率 | Feet per second(英尺/秒) | 只读 | +| `VARIOMETER SWITCH` | True if the variometer switch is on | 当thevariometer开关为开时为真 | Bool(布尔) | 只读 | +| `PRESSURE ALTITUDE` | Altitude reading | 压力高度 | Meters(米) | 只读 | +| `MAGNETIC COMPASS` | Compass reading | 磁罗盘 | Degrees(度) | 只读 | +| `TURN INDICATOR RATE` | Turn indicator reading | TURN指示器速率 | Radians per second(弧度/秒) | 只读 | +| `TURN INDICATOR SWITCH` | True if turn indicator switch is on | 当转弯指示器开关为开时为真 | Bool(布尔) | 只读 | +| `YOKE Y INDICATOR` | Yoke position in vertical direction | 操纵杆位置在垂直方向 | Position(位置) | 只读 | +| `YOKE X INDICATOR` | Yoke position in horizontal direction | 操纵杆横向指示器 | Position(位置) | 只读 | +| `RUDDER PEDAL INDICATOR` | Rudder pedal position | 方向舵踏板指示器 | Position(位置) | 只读 | +| `BRAKE DEPENDENT HYDRAULIC PRESSURE` | Brake dependent hydraulic pressure reading | 制动DEPENDENT液压压力 | foot pounds | 只读 | +| `PANEL ANTI ICE SWITCH` | True if panel anti-ice switch is on | 当面板防-冰开关为开时为真 | Bool(布尔) | 只读 | +| `WING AREA` | Total wing area | 总机翼area | Square feet | 只读 | +| `WING SPAN` | Total wing span | 总机翼span | Feet(英尺) | 只读 | +| `BETA DOT` | Beta dot | 桨距角DOT | Radians per second(弧度/秒) | 只读 | +| `LINEAR CL ALPHA` | Linear CL alpha | 线性升力系数迎角 | Per radian(每弧度) | 只读 | +| `STALL ALPHA` | Stall alpha | STALL迎角 | Radians(弧度) | 只读 | +| `ZERO LIFT ALPHA` | Zero lift alpha | 零升力迎角 | Radians(弧度) | 只读 | +| `CG AFT LIMIT` | Aft limit of CG | Aft限制的重心 | Percent over 100(百分比(超100)) | 只读 | +| `CG FWD LIMIT` | Forward limit of CG | Forward限制的重心 | Percent over 100(百分比(超100)) | 只读 | +| `CG MAX MACH` | Max mach CG | 重心最大MACH | Machs | 只读 | +| `CG MIN MACH` | Min mach CG | 重心MINMACH | Machs | 只读 | +| `PAYLOAD STATION NAME` | Descriptive name for payload station | PAYLOAD台站名称 | String(字符串) | 只读 | +| `ELEVON DEFLECTION` | Elevon deflection | ELEVON偏转 | Radians(弧度) | 只读 | +| `EXIT TYPE` | One of:; 0: Main; 1: Cargo; 2: Emergency; 3: Unknown | 一的:;0:主;1:Cargo;2:紧急;3:Unknown | Enum(枚举) | 只读 | +| `EXIT POSX` | Position of exit relative to datum reference point | 位置的出口相对至datumreference点 | Feet(英尺) | 只读 | +| `EXIT POSY` | Position of exit relative to datum reference point | 位置的出口相对至datumreference点 | Feet(英尺) | 只读 | +| `EXIT POSZ` | Position of exit relative to datum reference point | 位置的出口相对至datumreference点 | Feet(英尺) | 只读 | +| `DECISION HEIGHT` | Design decision height | 决断高度 | Feet(英尺) | 只读 | +| `DECISION ALTITUDE MSL` | Design decision altitude above mean sea level | 设计决断高度高于mean海液位 | Feet(英尺) | 只读 | +| `EMPTY WEIGHT PITCH MOI` | Empty weight pitch moment of inertia | Empty重量俯仰moment的inertia | slug feet squared | 只读 | +| `EMPTY WEIGHT ROLL MOI` | Empty weight roll moment of inertia | Empty重量rollmoment的inertia | slug feet squared | 只读 | +| `EMPTY WEIGHT YAW MOI` | Empty weight yaw moment of inertia | Empty重量yawmoment的inertia | slug feet squared | 只读 | +| `EMPTY WEIGHT CROSS COUPLED MOI` | Empty weigth cross coupled moment of inertia | EMPTY重量串COUPLEDMOI | slug feet squared | 只读 | +| `TOTAL WEIGHT PITCH MOI` | Total weight pitch moment of inertia | 总重量俯仰moment的inertia | slug feet squared | 只读 | +| `TOTAL WEIGHT ROLL MOI` | Total weight roll moment of inertia | 总重量rollmoment的inertia | slug feet squared | 只读 | +| `TOTAL WEIGHT YAW MOI` | Total weight yaw moment of inertia | 总重量yawmoment的inertia | slug feet squared | 只读 | +| `TOTAL WEIGHT CROSS COUPLED MOI` | Total weight cross coupled moment of inertia | 总重量串coupledmoment的inertia | slug feet squared | 只读 | +| `WATER BALLAST VALVE` | True if water ballast valve is available | 当水ballast阀门为可用时为真 | Bool(布尔) | 只读 | +| `MAX RATED ENGINE RPM` | Maximum rated rpm | 最大RATED发动机转速 | Rpm(转/分) | 只读 | +| `FULL THROTTLE THRUST TO WEIGHT RATIO` | Full throttle thrust to weight ratio | FULL油门推力至重量比 | Number(数值) | 只读 | +| `PROP AUTO CRUISE ACTIVE` | True if prop auto cruise active | 当prop自动cruise激活时为真 | Bool(布尔) | 只读 | +| `PROP ROTATION ANGLE` | Prop rotation angle | PROP旋转角度 | Radians(弧度) | 只读 | +| `PROP BETA MAX` | Prop beta max | PROP桨距角最大 | Radians(弧度) | 只读 | +| `PROP BETA MIN` | Prop beta min | PROP桨距角MIN | Radians(弧度) | 只读 | +| `PROP BETA MIN REVERSE` | Prop beta min reverse | PROP桨距角MIN反推 | Radians(弧度) | 只读 | +| `FUEL SELECTED TRANSFER MODE` | One of:; -1: off; 0: auto; 1: forward; 2: aft; 3: manual | 燃油SELECTED传输模式 | Enum(枚举) | 只读 | +| `DROPPABLE OBJECTS UI NAME` | Descriptive name, used in User Interface dialogs, of a droppable object | DROPPABLEOBJECTSUI名称 | String(字符串) | 只读 | +| `MANUAL FUEL PUMP HANDLE` | Position of manual fuel pump handle. 100 is fully deployed. | 位置的manual燃油泵手柄.100为完全放出. | Percent over 100(百分比(超100)) | 只读 | +| `BLEED AIR SOURCE CONTROL` | One of:; 0: min; 1: auto; 2: off; 3: apu; 4: engines | 一的:;0:min;1:自动;2:关;3:APU;4:发动机 | Enum(枚举) | 只读 | +| `ELECTRICAL OLD CHARGING AMPS` | Legacy, use ELECTRICAL BATTERY LOAD | Legacy,use电气BATTERY负载 | Amps | 只读 | +| `HYDRAULIC SWITCH` | True if hydraulic switch is on | 当液压开关为开时为真 | Bool(布尔) | 只读 | +| `CONCORDE VISOR POSITION PERCENT` | 0 = up, 1.0 = extended/down | 0=收起,1.0=伸出/down | Percent over 100(百分比(超100)) | 只读 | +| `CONCORDE NOSE ANGLE` | 0 = up | 0=收起 | Radians(弧度) | 只读 | +| `REALISM CRASH WITH OTHERS` | True indicates crashing with other aircraft is possible. | 真indicatescrashing带other飞机为possible. | Bool(布尔) | 只读 | +| `REALISM CRASH DETECTION` | True indicates crash detection is turned on. | 真indicates坠毁detection为turned开. | Bool(布尔) | 只读 | +| `MANUAL INSTRUMENT LIGHTS` | True if instrument lights are set manually | 当instrumentlightsare设定manually时为真 | Bool(布尔) | 只读 | +| `PITOT ICE PCT` | Amount of pitot ice. 100 is fully iced. | 皮托冰百分比 | Percent over 100(百分比(超100)) | 只读 | +| `SEMIBODY LOADFACTOR Y` | Semibody loadfactor x and z are not supported. | 半机体载荷因子横向和垂向arenotsupported. | Number(数值) | 只读 | +| `SEMIBODY LOADFACTOR YDOT` | Semibody loadfactory ydot | 半机体法向载荷因子变化率 | Per second(每秒) | 只读 | +| `RAD INS SWITCH` | True if Rad INS switch on | 当RadINS开关开时为真 | Bool(布尔) | 只读 | +| `SIMULATED RADIUS` | Simulated radius | 模拟半径 | Feet(英尺) | 只读 | +| `STRUCTURAL ICE PCT` | Amount of ice on aircraft structure. 100 is fully iced. | Amount的冰开飞机structure.100为完全iced. | Percent over 100(百分比(超100)) | 只读 | +| `ARTIFICIAL GROUND ELEVATION` | In case scenery is not loaded for AI planes, this variable can be used to set a default surface elevation. | 在casescenery为notloaded用于AIplanes,此variablecanbe使用至设定adefaultsurfaceelevation. | Feet(英尺) | 只读 | +| `SURFACE INFO VALID` | True indicates SURFACE CONDITION is meaningful. | SURFACEINFO有效 | Bool(布尔) | 只读 | +| `SURFACE CONDITION` | One of:; 0: Normal; 1: Wet; 2: Icy; 3: Snow | 一的:;0:Normal;1:Wet;2:Icy;3:Snow | Enum(枚举) | 只读 | +| `PUSHBACK ANGLE` | Pushback angle (the heading of the tug) | Pushback角(the航向的thetug) | Radians(弧度) | 只读 | +| `PUSHBACK CONTACTX` | The towpoint position, relative to the aircrafts datum reference point. | Thetowpoint位置,相对至theaircraftsdatumreference点. | Feet(英尺) | 只读 | +| `PUSHBACK CONTACTY` | Pushback contact position in vertical direction | Pushbackcontact位置在垂直方向 | Feet(英尺) | 只读 | +| `PUSHBACK CONTACTZ` | Pushback contact position in fore/aft direction | Pushbackcontact位置在fore/aft方向 | Feet(英尺) | 只读 | +| `PUSHBACK WAIT` | True if waiting for pushback. | 当waiting用于pushback时为真 | Bool(布尔) | 只读 | +| `YAW STRING ANGLE` | The yaw string angle. Yaw strings are attached to gliders as visible indicators of the yaw angle. An animation of this is not implemented in ESP. | Theyaw字符串角.Yawstringsare已连接至gliders作为visibleindicators的theyaw角.An动画的此为notimplemented在ESP. | Radians(弧度) | 只读 | +| `YAW STRING PCT EXTENDED` | Yaw string angle as a percentage | Yaw字符串角作为a百分比 | Percent over 100(百分比(超100)) | 只读 | +| `INDUCTOR COMPASS PERCENT DEVIATION` | Inductor compass deviation reading | INDUCTOR罗盘百分比偏差 | Percent over 100(百分比(超100)) | 只读 | +| `INDUCTOR COMPASS HEADING REF` | Inductor compass heading | Inductor罗盘航向 | Radians(弧度) | 只读 | +| `ANEMOMETER PCT RPM` | Anemometer rpm as a percentage | Anemometer转速作为a百分比 | Percent over 100(百分比(超100)) | 只读 | +| `ROTOR ROTATION ANGLE` | Main rotor rotation angle (helicopters only) | 主旋翼旋转角(仅直升机) | Radians(弧度) | 只读 | +| `DISK PITCH ANGLE` | Main rotor pitch angle (helicopters only) | 旋翼盘俯仰角 | Radians(弧度) | 只读 | +| `DISK BANK ANGLE` | Main rotor bank angle (helicopters only) | 旋翼盘坡度角 | Radians(弧度) | 只读 | +| `DISK PITCH PCT` | Main rotor pitch percent (helicopters only) | 旋翼盘俯仰百分比 | Percent over 100(百分比(超100)) | 只读 | +| `DISK BANK PCT` | Main rotor bank percent (helicopters only) | 旋翼盘坡度百分比 | Percent over 100(百分比(超100)) | 只读 | +| `DISK CONING PCT` | Main rotor coning percent (helicopters only) | 旋翼盘锥度百分比 | Percent over 100(百分比(超100)) | 只读 | +| `STATIC CG TO GROUND` | Static CG to ground | 静重心离地高度 | Feet(英尺) | 只读 | +| `STATIC PITCH` | Static pitch | 静俯仰角 | Radians(弧度) | 只读 | +| `CRASH SEQUENCE` | One of:; 0: off; 1: complete; 3: reset; 4: pause; 11: start | 枚举:0=关闭;1=完成;3=重置;4=暂停;11=开始 | Enum(枚举) | 只读 | +| `CRASH FLAG` | One of:; 0: None; 2: Mountain; 4: General; 6: Building; 8: Splash; 10: Gear up; 12: Overstress; 14: Building; 16: Aircraft; 18: Fuel Truck | 枚举:0=无;2=撞山;4=一般;6=建筑物;8=落水;10=起落架收起;12=过载;14=建筑物;16=飞机;18=加油车 | Enum(枚举) | 只读 | +| `TOW RELEASE HANDLE` | Position of tow release handle. 100 is fully deployed. | 拖曳释放手柄位置,100为完全放出 | Percent over 100(百分比(超100)) | 只读 | +| `TOW CONNECTION` | True if a towline is connected to both tow plane and glider. | 拖缆同时连接拖机与滑翔机时为真 | Bool(布尔) | 只读 | +| `APU PCT RPM` | Auxiliary power unit rpm, as a percentage | APU 转速百分比 | Percent over 100(百分比(超100)) | 只读 | +| `APU PCT STARTER` | Auxiliary power unit starter, as a percentage | APU 起动机百分比 | Percent over 100(百分比(超100)) | 只读 | +| `APU VOLTS` | Auxiliary power unit voltage | APU 电压 | Volts(伏特) | 只读 | +| `APU GENERATOR SWITCH` | True if APU generator switch on | APU 发电机开关 | Bool(布尔) | 只读 | +| `APU GENERATOR ACTIVE` | True if APU generator active | APU 发电机激活 | Bool(布尔) | 只读 | +| `APU ON FIRE DETECTED` | True if APU on fire | APU 着火探测 | Bool(布尔) | 只读 | +| `PRESSURIZATION CABIN ALTITUDE` | The current altitude of the cabin pressurization.. | 客舱增压高度 | Feet(英尺) | 只读 | +| `PRESSURIZATION CABIN ALTITUDE GOAL` | The set altitude of the cabin pressurization. | 客舱增压目标高度 | Feet(英尺) | 只读 | +| `PRESSURIZATION CABIN ALTITUDE RATE` | The rate at which cabin pressurization changes. | 客舱增压高度变化率 | Feet per second(英尺/秒) | 只读 | +| `PRESSURIZATION PRESSURE DIFFERENTIAL` | The difference in pressure between the set altitude pressurization and the current pressurization. | 客舱增压压差 | foot pounds | 只读 | +| `PRESSURIZATION DUMP SWITCH` | True if the cabin pressurization dump switch is on. | 客舱增压泄压开关 | Bool(布尔) | 只读 | +| `FIRE BOTTLE SWITCH` | True if the fire bottle switch is on. | 灭火瓶开关 | Bool(布尔) | 只读 | +| `FIRE BOTTLE DISCHARGED` | True if the fire bottle is discharged. | 灭火瓶已释放 | Bool(布尔) | 只读 | +| `CABIN NO SMOKING ALERT SWITCH` | True if the No Smoking switch is on. | 客舱禁烟告警开关 | Bool(布尔) | **可写** | +| `CABIN SEATBELTS ALERT SWITCH` | True if the Seatbelts switch is on. | 客舱系安全带告警开关 | Bool(布尔) | **可写** | +| `GPWS WARNING` | True if Ground Proximity Warning System installed. | 近地警告系统已安装 | Bool(布尔) | 只读 | +| `GPWS SYSTEM ACTIVE` | True if the Ground Proximity Warning System is active | 近地警告系统激活 | Bool(布尔) | **可写** | +| `IS ALTITUDE FREEZE ON` | True if the altitude of the aircraft is frozen. | 高度冻结开启 | Bool(布尔) | 只读 | +| `IS ATTITUDE FREEZE ON` | True if the attitude (pitch, bank and heading) of the aircraft is frozen. | 姿态冻结开启 | Bool(布尔) | 只读 | +| `SLING HOOK IN PICKUP MODE:index` | | 吊索钩处于拾取模式(索引) | Bool(布尔) | 只读 | +| `AI TRAFFIC STATE` | | AI 交通状态 | String(字符串) | 只读 | +| `AI TRAFFIC ASSIGNED PARKING` | | AI 分配停机位 | String(字符串) | 只读 | +| `RECIP ENG FUEL TANKS USED:index` | | 往复式发动机使用油箱(索引) | Mask(掩码) | **可写** | +| `TURB ENG TANKS USED:index` | | 涡轮发动机使用油箱(索引) | Mask(掩码) | 只读 | +| `ENG TURBINE TEMPERATURE:index` | | 发动机涡轮温度(索引) | Celsius(摄氏度) | 只读 | +| `ENG ELECTRICAL LOAD:index` | | 发动机电气负载(索引) | Percent(百分比) | 只读 | +| `ENG TRANSMISSION PRESSURE:index` | | 发动机传动箱压力(索引) | PSI(磅/平方英寸) | 只读 | +| `ENG TRANSMISSION TEMPERATURE:index` | | 发动机传动箱温度(索引) | Celsius(摄氏度) | 只读 | +| `SURFACE TYPE` | | SURFACE类型 | Enum(枚举) | 只读 | +| `COM STATUS:index` | | 通讯STATUS(索引) | Enum(枚举) | 只读 | +| `NAV TOFROM:index` | | 导航TOFROM(索引) | Enum(枚举) | 只读 | +| `GPS APPROACH MODE` | | GPS进近模式 | Enum(枚举) | 只读 | +| `GPS APPROACH WP TYPE` | | GPS进近航路点类型 | Enum(枚举) | 只读 | +| `GPS APPROACH SEGMENT TYPE` | | GPS进近SEGMENT类型 | Enum(枚举) | 只读 | +| `GPS APPROACH APPROACH TYPE` | | GPS进近进近类型 | Enum(枚举) | 只读 | +| `AMBIENT PRECIP STATE` | | 降水状态 | Mask(掩码) | 只读 | +| `CATEGORY` | | 飞机类别 | String(字符串) | 只读 | +| `CONCORDE VISOR NOSE HANDLE` | | CONCORDEVISORNOSE手柄 | Enum(枚举) | 只读 | +| `IS LATITUDE LONGITUDE FREEZE ON` | | IS纬度经度冻结开 | Bool(布尔) | 只读 | +| `TIME OF DAY` | | 时段 | Enum(枚举) | 只读 | +| `SIMULATION RATE` | | 模拟速率 | Number(数值) | 只读 | +| `UNITS OF MEASURE` | | 单位制 | Enum(枚举) | 只读 | + +## 十五、Aircraft String Data(字符串) + +共 **12** 项(可写 3 / 只读 9) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `ATC TYPE` | Type used by ATC | 类型使用由ATC | String(字符串) | 只读 | +| `ATC MODEL` | Model used by ATC | Model使用由ATC | String(字符串) | 只读 | +| `ATC ID` | ID used by ATC | ID使用由ATC | String(字符串) | **可写** | +| `ATC AIRLINE` | Airline used by ATC | 航空公司使用由ATC | String(字符串) | **可写** | +| `ATC FLIGHT NUMBER` | Flight Number used by ATC | 飞行数量使用由ATC | String(字符串) | **可写** | +| `TITLE` | Title from aircraft.cfg | 标题来自飞机.cfg | String(字符串) | 只读 | +| `HSI STATION IDENT` | Tuned station identifier | HSI台站识别 | String(字符串) | 只读 | +| `GPS WP PREV ID` | ID of previous GPS waypoint | GPS航路点上一ID | String(字符串) | 只读 | +| `GPS WP NEXT ID` | ID of next GPS waypoint | GPS航路点下一ID | String(字符串) | 只读 | +| `GPS APPROACH AIRPORT ID` | ID of airport | GPS进近AIRPORTID | String(字符串) | 只读 | +| `GPS APPROACH APPROACH ID` | ID of approach | GPS进近进近ID | String(字符串) | 只读 | +| `GPS APPROACH TRANSITION ID` | ID of approach transition | ID的进近transition | String(字符串) | 只读 | + +## 十六、AI Controlled Aircraft(AI 飞机) + +共 **13** 项(可写 6 / 只读 7) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `AI DESIRED SPEED` | Desired speed of the AI object. | 目标速度的theAI对象. | Knots(节) | **可写** | +| `AI CURRENT WAYPOINT` | Current waypoint in the list | 当前waypoint在thelist | Number(数值) | **可写** | +| `AI DESIRED HEADING` | Desired heading of the AI object. | 目标航向的theAI对象. | Degrees(度) | **可写** | +| `AI GROUNDTURNTIME` | Time to make a 90 degree turn. | 90度转弯所需时间 | Seconds(秒) | **可写** | +| `AI GROUNDCRUISESPEED` | Cruising speed. | AI 地面巡航速度 | Knots(节) | **可写** | +| `AI GROUNDTURNSPEED` | Turning speed. | AI 地面转弯速度 | Knots(节) | **可写** | +| `AI TRAFFIC ISIFR` | Request whether this aircraft is IFR or VFR See Note 1. | 请求whether此飞机为IFR或VFRSee注意1. | Boolean | 只读 | +| `AI TRAFFIC CURRENT AIRPORT` | ICAO code of current airport. See Note 1. | AI交通当前AIRPORT | String(字符串) | 只读 | +| `AI TRAFFIC ASSIGNED RUNWAY` | Assigned runway name (for example: "32R"). See Note 1. | 分配runway名称(用于example:"32R").See注意1. | String(字符串) | 只读 | +| `AI TRAFFIC FROMAIRPORT` | ICAO code of the departure airport in the current schedule. See Note 2. | ICAOcode的thedepartureairport在the当前schedule.See注意2. | String(字符串) | 只读 | +| `AI TRAFFIC TOAIRPORT` | ICAO code of the destination airport in the current schedule. See Note 2. | ICAOcode的thedestinationairport在the当前schedule.See注意2. | String(字符串) | 只读 | +| `AI TRAFFIC ETD` | Estimated time of departure for the current schedule entry, given as the number of seconds difference from the current simulation time. This can be negative if ETD is earlier than the current simulation time. See Note 2. | Estimated时间的departure用于the当前scheduleentry,given作为the数量的秒差值来自the当前模拟时间.此canbenegative若ETD为earlier比the当前模拟时间.See注意2. | Seconds(秒) | 只读 | +| `AI TRAFFIC ETA` | Estimated time of arrival for the current schedule entry, given as the number of seconds difference from the current simulated time. This can be negative if ETA is earlier than the current simulated time. See Note 2. | Estimated时间的arrival用于the当前scheduleentry,given作为the数量的秒差值来自the当前半径时间.此canbenegative若ETA为earlier比the当前半径时间.See注意2. | Seconds(秒) | 只读 | + +## 十七、Carrier Operations(航母作业) + +共 **10** 项(可写 0 / 只读 10) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `LAUNCHBAR POSITION` | Installed on aircraft before takeoff from a carrier catapult. Note that gear cannot retract with this extended. 100 = fully extended. Refer to the document Notes on Aircraft Systems. | 已安装开飞机beforetakeoff来自a航母弹射.注意that起落架cannot收放带此伸出.100=完全伸出.Refer至thedocumentNotes开飞机Systems. | Percent over 100(百分比(超100)) | 只读 | +| `LAUNCHBAR SWITCH` | If this is set to True the launch bar switch has been engaged. | 若此为设定至真thelaunchbar开关具有beenengaged. | Bool(布尔) | 只读 | +| `LAUNCHBAR HELD EXTENDED` | This will be True if the launchbar is fully extended, and can be used, for example, to change the color of an instrument light. | 此willbe真若thelaunchbar为完全伸出,和canbe使用,用于example,至changethecolor的aninstrument灯光. | Bool(布尔) | 只读 | +| `NUMBER OF CATAPULTS` | Maximum of 4. A model can contain more than 4 catapults, but only the first four will be read and recognized by the simulation. | 最大的4.Amodelcancontainmore比4catapults,but仅thefirstfourwillberead和recognized由the模拟. | Number(数值) | 只读 | +| `CATAPULT STROKE POSITION:index` | Catapults are indexed from 1. This value will be 0 before the catapult fires, and then up to 100 as the aircraft is propelled down the catapult. The aircraft may takeoff before the value reaches 100 (depending on the aircraft weight, power applied, and other factors), in which case this value will not be further updated. This value could be used to drive a bogie animation. | Catapultsareindexed来自1.此valuewillbe0beforethe弹射fires,和then收起至100作为the飞机为propelleddownthe弹射.The飞机maytakeoffbeforethevaluereaches100(depending开the飞机重量,动力applied,和otherfactors),在whichcase此valuewillnotbefurtherupdated.此valuecouldbe使用至driveabogie动画. | Number(数值) | 只读 | +| `HOLDBACK BAR INSTALLED` | Holdback bars allow build up of thrust before takeoff from a catapult, and are installed by the deck crew of an aircraft carrier. | Holdbackbarsallowbuild收起的推力beforetakeoff来自a弹射,和are已安装由thedeckcrew的an飞机航母. | Bool(布尔) | 只读 | +| `BLAST SHIELD POSITION:index` | Indexed from 1, 100 is fully deployed, 0 flat on deck | Indexed来自1,100为完全放出,0flat开deck | Percent over 100(百分比(超100)) | 只读 | +| `CABLE CAUGHT BY TAILHOOK` | A number 1 through 4 for the cable number caught by the tailhook. Cable 1 is the one closest to the stern of the carrier. A value of 0 indicates no cable was caught. | A数量1through4用于the索数量caught由the尾钩.索1为the一closest至thestern的the航母.Avalue的0indicates禁索wascaught. | Number(数值) | 只读 | +| `TAILHOOK HANDLE` | True if the tailhook handle is engaged. | 当the尾钩手柄为engaged时为真 | Bool(布尔) | 只读 | +| `SURFACE RELATIVE GROUND SPEED` | The speed of the aircraft relative to the speed of the first surface directly underneath it. Use this to retrieve, for example, an aircraft's taxiing speed while it is moving on a moving carrier. It also applies to airborne aircraft, for example when a helicopter is successfully hovering above a moving ship, this value should be zero. The returned value will be the same as GROUND VELOCITY if the first surface beneath it is not moving. | The速度的the飞机相对至the速度的thefirstsurfacedirectlyunderneathit.Use此至retrieve,用于example,an飞机'staxiing速度whileit为moving开amoving航母.Italsoapplies至airborne飞机,用于example当a直升机为successfullyhovering高于amovingship,此valueshouldbe零.Thereturnedvaluewillbethesame作为地面速度若thefirstsurfacebeneathit为notmoving. | Feet per second(英尺/秒) | 只读 | + +## 十八、Racing(竞速) + +共 **10** 项(可写 4 / 只读 6) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `RECIP ENG DETONATING:index` | Indexed from 1. Set to True if the engine is detonating. | Indexed来自1.设定至真若the发动机为detonating. | Bool(布尔) | 只读 | +| `RECIP ENG CYLINDER HEALTH:index` | Index high 16 bits is engine number, low 16 cylinder number, both indexed from 1. | 索引high16bits为发动机数量,low16气缸数量,双方indexed来自1. | Percent over 100(百分比(超100)) | 只读 | +| `RECIP ENG NUM CYLINDERS` | Indexed from 1. The number of engine cylinders. | 往复式发动机数量CYLINDERS | Number(数值) | 只读 | +| `RECIP ENG NUM CYLINDERS FAILED` | Indexed from 1. The number of cylinders that have failed. | 往复式发动机数量CYLINDERS故障 | Number(数值) | 只读 | +| `RECIP ENG ANTIDETONATION TANK VALVE:index` | Indexed from 1, each engine can have one antidetonation tank. Installed on racing aircraft. Refer to the document Notes on Aircraft Systems. | Indexed来自1,each发动机can具有一antidetonation油箱.已安装开racing飞机.Refer至thedocumentNotes开飞机Systems. | Bool(布尔) | **可写** | +| `RECIP ENG ANTIDETONATION TANK QUANTITY:index` | Indexed from 1. Refer to the Mission Creation documentationfor the procedure for refilling tanks. | 往复式发动机ANTIDETONATION油箱数量(索引) | Gallons(加仑) | **可写** | +| `RECIP ENG ANTIDETONATION TANK MAX QUANTITY:index` | Indexed from 1. This value set in the Aircraft Configuration File. | 往复式发动机ANTIDETONATION油箱最大数量(索引) | Gallons(加仑) | 只读 | +| `RECIP ENG NITROUS TANK VALVE:index` | Indexed from 1. Each engine can have one Nitrous fuel tank installed. | Indexed来自1.Each发动机can具有一Nitrous燃油油箱已安装. | Bool(布尔) | **可写** | +| `RECIP ENG NITROUS TANK QUANTITY:index` | Indexed from 1. Refer to the Mission Creation documentationfor the procedure for refilling tanks. | 往复式发动机NITROUS油箱数量(索引) | Gallons(加仑) | **可写** | +| `RECIP ENG NITROUS TANK MAX QUANTITY:index` | Indexed from 1. This value set in the Aircraft Configuration File. | 往复式发动机NITROUS油箱最大数量(索引) | Gallons(加仑) | 只读 | + +## 十九、Environment Data(全局环境) + +共 **14** 项(可写 0 / 只读 14) + +| 变量名 | 英文说明 | 中文说明 | 单位 | 读写 | +|---|---|---|---|---| +| `ABSOLUTE TIME` | Time, as referenced from 12:00 AM January 1, 0000 | 时间,作为referenced来自12:00AMJanuary1,0000 | Seconds(秒) | 只读 | +| `ZULU TIME` | Greenwich Mean Time (GMT) | GreenwichMean时间(世界时) | Seconds(秒) | 只读 | +| `ZULU DAY OF WEEK` | GMT day of week | 世界时日的周 | Number(数值) | 只读 | +| `ZULU DAY OF MONTH` | GMT day of month | 世界时日的月 | Number(数值) | 只读 | +| `ZULU MONTH OF YEAR` | GMT month of year | 世界时月的年 | Number(数值) | 只读 | +| `ZULU DAY OF YEAR` | GMT day of year | 世界时年内天数 | Number(数值) | 只读 | +| `ZULU YEAR` | GMT year | 世界时年份 | Number(数值) | 只读 | +| `LOCAL TIME` | Local time | 本地时间 | Seconds(秒) | 只读 | +| `LOCAL DAY OF WEEK` | Local day of week | 本地日的周 | Number(数值) | 只读 | +| `LOCAL DAY OF MONTH` | Local day of month | 本地日的月 | Number(数值) | 只读 | +| `LOCAL MONTH OF YEAR` | Local month of year | 本地月的年 | Number(数值) | 只读 | +| `LOCAL DAY OF YEAR` | Local day of year | 本地日的年 | Number(数值) | 只读 | +| `LOCAL YEAR` | Local year | 本地年 | Number(数值) | 只读 | +| `TIME ZONE OFFSET` | Local time difference from GMT | 本地时间差值来自世界时 | Seconds(秒) | 只读 | diff --git "a/Docs/\351\241\271\347\233\256\345\216\237\347\220\206.md" "b/Docs/\351\241\271\347\233\256\345\216\237\347\220\206.md" new file mode 100644 index 00000000..bccb44cf --- /dev/null +++ "b/Docs/\351\241\271\347\233\256\345\216\237\347\220\206.md" @@ -0,0 +1,35 @@ +# 项目原理 + +## 架构概览 +- **核心库 `SimConnect/`**:使用 `ctypes` 调用官方 `SimConnect.dll`,将 C 接口映射为 Python 枚举、结构体与函数,从而完成事件发送、数据订阅与设施列表查询。 +- **请求/事件封装**:`RequestList.py` 定义数据请求与缓存策略,`EventList.py` 将海量事件名映射为可调用的 Python 对象,`FacilitiesList.py` 负责机场、航路点、导航台等设施列表订阅。 +- **示例应用**:`glass_server.py` 基于 Flask 提供 Web UI 与 REST API;`ESP32_Client_[WIP].py` 演示通过 ESP32 读取模拟量并调用 HTTP 接口;`local_example.py` 展示最小本地读写示例。 + +## 开发与交互流程 +1. **加载 DLL**:`SimConnect.SimConnectDll` 使用 `windll.LoadLibrary` 载入 `SimConnect.dll`,并为每个 SDK 函数声明 `argtypes`/`restype`,确保参数与返回值与 C 签名一致。 +2. **建立连接**:`SimConnect.connect()` 调用 `SimConnect_Open` 创建句柄,并订阅系统事件(启动、停止、暂停/恢复),随后启动后台线程 `_run()` 轮询 `CallDispatch`。 +3. **消息分发**:`my_dispatch_proc` 根据 `dwID` 将收到的数据分发到对应处理函数(系统状态、异常、设施列表、对象数据等),并将数据写回请求对象或环境变量。 +4. **数据请求/写入**:`request_data`、`get_data`、`set_data` 通过定义 ID 与请求 ID 将 Python 值转换为 C 指针,再交由 DLL 读取或写入模拟器变量;字符串数据特殊处理为 `c_char_p`。 +5. **事件发送**:`map_to_sim_event` 动态扩展事件枚举并调用 `TransmitClientEvent` 触发模拟器事件;`Event` 类则提供可调用对象的语法糖。 +6. **设施订阅**:`FacilitiesHelper.subscribe` 订阅机场/航路点等列表,回调里通过 `dump` 输出数据,便于快速调试或自定义处理。 + +## 部署要点 +- **平台**:SimConnect 仅支持 Windows 主机,需确保安装并可访问 `SimConnect.dll`(仓库已附带)。 +- **Python 版本**:必须使用 64 位 Python;32 位会触发 `WinError 193`。 +- **配置文件**:若远程连接,需在脚本目录创建 `SimConnect.cfg` 并在模拟器端 `SimConnect.xml` 中设置 `Address` 为 `0.0.0.0` 以允许网络访问。 +- **依赖安装**:执行 `pip install -r requirements.txt`(如使用开发工具再安装 `requirements_dev.txt`)。示例 Web 端依赖 Flask 等包。 + +## 运行示例 +- **Web/REST 示例**:`python glass_server.py`,浏览器访问 `http://localhost:5000` 查看 UI,或使用 REST 接口: + - `GET /dataset/` 拉取数据集 JSON。 + - `GET /datapoint//get` 读取单个变量。 + - `POST /datapoint//set` 设置变量。 + - `POST /event//trigger` 触发事件。 +- **本地示例**:`python local_example.py` 连接模拟器并演示变量读取与事件触发。 +- **ESP32 示例**:将 `ESP32_Client_[WIP].py` 烧录到板子,在脚本中配置 Wi-Fi 与服务器地址,即可周期性读取 ADC 并通过 HTTP 推送到模拟器。 + +## 扩展与调试建议 +- 使用 `logging` 控制日志级别,排查连接或事件映射问题。 +- 如需新增变量或事件,只需在 `RequestList.py`/`EventList.py` 中添加对应名称与说明,无需改动底层 DLL 映射。 +- 后台线程 `_run` 采用短睡眠轮询(2ms),若需降低 CPU,可适当调高休眠时间,但可能增加延迟。 + diff --git a/ESP32_Client_[WIP].py b/ESP32_Client_[WIP].py deleted file mode 100644 index 25ee059e..00000000 --- a/ESP32_Client_[WIP].py +++ /dev/null @@ -1,87 +0,0 @@ -# This file is executed on every boot (including wake-boot from deepsleep) -import esp -import webrepl -import network -import socket -import json -import machine -from machine import Pin, ADC -from time import sleep - -esp.osdebug(None) - -server_address = 'http://' -server_port = 5000 - - -def do_connect(): - wlan = network.WLAN(network.STA_IF) - wlan.active(True) - if not wlan.isconnected(): - print('connecting to network...') - wlan.connect('', '') - while not wlan.isconnected(): - pass - print('network config:', wlan.ifconfig()) - - -def http_get(url): - _, _, host, path = url.split('/', 3) - addr = socket.getaddrinfo(host, server_port)[0][-1] - s = socket.socket() - s.connect(addr) - s.send(bytes('GET /%s HTTP/1.0\r\nHost: %s\r\n\r\n' % (path, host), 'utf8')) - while True: - data = s.recv(100) - if data: - print(str(data, 'utf8'), end='') - else: - break - s.close() - - -def http_post(url, body): - _, _, host, path = url.split('/', 3) - addr = socket.getaddrinfo(host, server_port)[0][-1] - s = socket.socket() - s.connect(addr) - s.send(bytes('POST /%s HTTP/1.0\r\nHost: %s\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s' % ( - path, host, len(body), body), 'utf8') - ) - while True: - data = s.recv(100) - if not data: - break - s.close() - - -def scale(val, src, dst): - """ - Scale the given value from the scale of src to the scale of dst. - """ - return ((val - src[0]) / (src[1] - src[0])) * (dst[1] - dst[0]) + dst[0] - - -def set_throttle_lever(value, index=1): - http_post(server_address + '/datapoint/GENERAL_ENG_THROTTLE_LEVER_POSITION:index/set', '{"index":%d, "value_to_use": %d}' % (index, value)) - - -def get_scaled_adc(_pin): - adc = ADC(Pin(_pin)) # create ADC object on ADC pin - adc.atten(ADC.ATTN_11DB) # set 11dB input attenuation (voltage range roughly 0.0v - 3.6v) - adc.width(ADC.WIDTH_11BIT) # set 9 bit return values (returned range 0-511) - return scale(adc.read(), (0, 520), (0, 100)) # read value using the newly configured attenuation and width - - -do_connect() -webrepl.start() - -buff = 2 -o_te = 0 -pin_to_read = 32 -while True: - c_te = int(get_scaled_adc(pin_to_read)) - if c_te > (o_te + buff) or c_te < (o_te - buff): - set_throttle_lever(c_te) - o_te = c_te - sleep(1) diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 0ad25db4..00000000 --- a/LICENSE +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/Makefile b/Makefile deleted file mode 100644 index 7234f381..00000000 --- a/Makefile +++ /dev/null @@ -1,25 +0,0 @@ -build: - python setup.py build - -install: - pip install -r requirements.txt - -install-dev: - pip install -r requirements_dev.txt - -test-sim: - python example.py - -test: install install-dev - python -m pytest --cov=SimConnect --cov-report xml - -update-dependencies: - pipenv update - pipenv lock -r > requirements.txt - pipenv lock -r -d > requirements_dev.txt - -sdist: - python setup.py sdist - -clean: - rm -r dist src/*.egg-info build .coverage coverage.xml .pytest_cache \ No newline at end of file diff --git a/Pipfile b/Pipfile deleted file mode 100644 index d04858d7..00000000 --- a/Pipfile +++ /dev/null @@ -1,18 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - -[dev-packages] -pytest-cov = "*" -pylint = "*" -black = "*" - -[packages] -flask = "*" - -[requires] -python_version = "3.8" - -[pipenv] -allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 967c24e6..00000000 --- a/Pipfile.lock +++ /dev/null @@ -1,404 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "75da094b7fd42cf27c1385797b0df42d11341a540f6defb40d93eba46794181a" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.8" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "click": { - "hashes": [ - "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", - "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==7.1.2" - }, - "flask": { - "hashes": [ - "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", - "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" - ], - "index": "pypi", - "version": "==1.1.2" - }, - "itsdangerous": { - "hashes": [ - "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", - "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.1.0" - }, - "jinja2": { - "hashes": [ - "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", - "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.11.2" - }, - "markupsafe": { - "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", - "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.1.1" - }, - "werkzeug": { - "hashes": [ - "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", - "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.0.1" - } - }, - "develop": { - "appdirs": { - "hashes": [ - "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", - "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" - ], - "version": "==1.4.4" - }, - "astroid": { - "hashes": [ - "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", - "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" - ], - "markers": "python_version >= '3.5'", - "version": "==2.4.2" - }, - "atomicwrites": { - "hashes": [ - "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197", - "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a" - ], - "markers": "sys_platform == 'win32'", - "version": "==1.4.0" - }, - "attrs": { - "hashes": [ - "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", - "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.1.0" - }, - "black": { - "hashes": [ - "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea", - "sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b" - ], - "index": "pypi", - "version": "==20.8b1" - }, - "click": { - "hashes": [ - "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", - "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==7.1.2" - }, - "colorama": { - "hashes": [ - "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", - "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" - ], - "markers": "sys_platform == 'win32' and sys_platform == 'win32'", - "version": "==0.4.3" - }, - "coverage": { - "hashes": [ - "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb", - "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3", - "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716", - "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034", - "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3", - "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8", - "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0", - "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f", - "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4", - "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962", - "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d", - "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b", - "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4", - "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3", - "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258", - "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59", - "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01", - "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd", - "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b", - "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d", - "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89", - "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd", - "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b", - "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d", - "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46", - "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546", - "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082", - "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b", - "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4", - "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8", - "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811", - "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd", - "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651", - "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==5.2.1" - }, - "iniconfig": { - "hashes": [ - "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437", - "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69" - ], - "version": "==1.0.1" - }, - "isort": { - "hashes": [ - "sha256:92533892058de0306e51c88f22ece002a209dc8e80288aa3cec6d443060d584f", - "sha256:a200d47b7ee8b7f7d0a9646650160c4a51b6a91a9413fd31b1da2c4de789f5d3" - ], - "markers": "python_version >= '3.6' and python_version < '4.0'", - "version": "==5.5.1" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", - "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", - "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", - "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", - "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", - "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", - "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", - "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", - "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", - "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", - "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", - "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", - "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", - "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", - "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", - "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", - "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", - "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", - "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", - "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", - "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.4.3" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "more-itertools": { - "hashes": [ - "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20", - "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c" - ], - "markers": "python_version >= '3.5'", - "version": "==8.5.0" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" - }, - "packaging": { - "hashes": [ - "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", - "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.4" - }, - "pathspec": { - "hashes": [ - "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", - "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" - ], - "version": "==0.8.0" - }, - "pluggy": { - "hashes": [ - "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", - "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.13.1" - }, - "py": { - "hashes": [ - "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", - "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.9.0" - }, - "pylint": { - "hashes": [ - "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210", - "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f" - ], - "index": "pypi", - "version": "==2.6.0" - }, - "pyparsing": { - "hashes": [ - "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", - "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.7" - }, - "pytest": { - "hashes": [ - "sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4", - "sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad" - ], - "markers": "python_version >= '3.5'", - "version": "==6.0.1" - }, - "pytest-cov": { - "hashes": [ - "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191", - "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e" - ], - "index": "pypi", - "version": "==2.10.1" - }, - "regex": { - "hashes": [ - "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204", - "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162", - "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f", - "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb", - "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6", - "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7", - "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88", - "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99", - "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644", - "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a", - "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840", - "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067", - "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd", - "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4", - "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e", - "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89", - "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e", - "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc", - "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf", - "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341", - "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7" - ], - "version": "==2020.7.14" - }, - "six": { - "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.15.0" - }, - "toml": { - "hashes": [ - "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" - ], - "version": "==0.10.1" - }, - "typed-ast": { - "hashes": [ - "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", - "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", - "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", - "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", - "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", - "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", - "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", - "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", - "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", - "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", - "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", - "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", - "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", - "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", - "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", - "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", - "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", - "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", - "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", - "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", - "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" - ], - "version": "==1.4.1" - }, - "typing-extensions": { - "hashes": [ - "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", - "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", - "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" - ], - "version": "==3.7.4.3" - }, - "wrapt": { - "hashes": [ - "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" - ], - "version": "==1.12.1" - } - } -} diff --git a/Prompt.md b/Prompt.md new file mode 100644 index 00000000..95d4be8b --- /dev/null +++ b/Prompt.md @@ -0,0 +1,28 @@ +Always respond in 中文 +DO NOT GIVE ME HIGH LEVEL STUFF, IF I ASK FOR FIX OR EXPLANATION, I WANT ACTUAL CODE OR EXPLANATION!!! I DON'T WANT "Here's how you can blablabla" +- You are a expert full stack engineer. +- If commands need to be executed, please pause file creation and let me execute the commands first. +- Please think step by step according to my needs, and if there are UI elements, they must be sufficiently aesthetic and modern. +- Do not affect other functionalities and interface layout styles. +- Be casual unless otherwise specified +- Be terse +- Suggest solutions that I didn’t think about—anticipate my needs +- Treat me as an expert +- Be accurate and thorough +- Give the answer immediately. Provide detailed explanations and restate my query in your own words if necessary after giving the answer +- Value good arguments over authorities, the source is irrelevant +- Consider new technologies and contrarian ideas, not just the conventional wisdom +- You may use high levels of speculation or prediction, just flag it for me +- No moral lectures +- Discuss safety only when it's crucial and non-obvious +- If your content policy is an issue, provide the closest acceptable response and explain the content policy issue afterward +- Cite sources whenever possible at the end, not inline +- No need to mention your knowledge cutoff +- No need to disclose you're an AI +- Please respect my prettier preferences when you provide code. +- Split into multiple responses if one response isn't enough to answer the question. + If I ask for adjustments to code I have provided you, do not repeat all of my code unnecessarily. Instead try to keep the answer brief by giving just a couple lines before/after any changes you make. Multiple code blocks are ok. +- You are a helpful assistant, but you need to get my permission and confirmation before executing any code. +- All operations in the terminal should run in the foreground, allowing me to see the output during the process and the final result. +- When providing suggestions, you are not limited to the existing code structure within the working directory. You can consider a wide range of options, including but not limited to available libraries, modules, technology stacks, and implementation methods, to find the best way to meet my requirements. +- Unless I explicitly ask for it, you don't need to provide summary explanations for the results or suggestions you give. \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 6b566fa6..00000000 --- a/README.md +++ /dev/null @@ -1,194 +0,0 @@ -[![PyPI version](https://badge.fury.io/py/SimConnect.svg)](https://badge.fury.io/py/SimConnect) -# Python-SimConnect - -Python interface for Microsoft Flight Simulator 2020 (MSFS2020) using SimConnect - -This library allows Python scripts to read and set variables within MSFS2020 and trigger events within the simulation. - -It also includes, as an example, "Cockpit Companion", a flask mini http server which runs locally. It provides a web UI with a moving map and simulation variables. It also provides simulation data in JSON format in response to REST API requests. - -Full documentation for this example can be found at [https://msfs2020.cc](https://msfs2020.cc) and it is included in a standalone repo here on Github as [MSFS2020-cockpit-companion](https://github.com/hankhank10/MSFS2020-cockpit-companion). - - -## Mobiflight Simconnect events: - -Yes this supports the new [SimConnect commands that DocMoebiuz](https://forums.flightsimulator.com/t/full-g1000-control-now-with-mobiflight/348509) of [MobiFlight](https://www.mobiflight.com/en/index.html) developed. -A full list of [commands and install instructions](https://pastebin.com/fMdB7at2) - -At this time MobiFlight SimConnect commands are not include in the AircraftEvents class and as so the AircraftEvents.find() and AircraftEvents.get() will not work. You will need to pass the Event ID to a new Event class as the Example below shows. - - -```py -from SimConnect import * -# Create SimConnect link -sm = SimConnect() -# Creat a function to call the MobiFlight AS1000_MFD_SOFTKEYS_3 event. -Sk3 = Event(b'MobiFlight.AS1000_MFD_SOFTKEYS_3', sm) -# Call the Event. -Sk3() -sm.exit() -quit() -``` - -## Python interface example - -```py -from SimConnect import * - -# Create SimConnect link -sm = SimConnect() -# Note the default _time is 2000 to be refreshed every 2 seconds -aq = AircraftRequests(sm, _time=2000) -# Use _time=ms where ms is the time in milliseconds to cache the data. -# Setting ms to 0 will disable data caching and always pull new data from the sim. -# There is still a timeout of 4 tries with a 10ms delay between checks. -# If no data is received in 40ms the value will be set to None -# Each request can be fine tuned by setting the time param. - -# To find and set timeout of cached data to 200ms: -altitude = aq.find("PLANE_ALTITUDE") -altitude.time = 200 - -# Get the aircraft's current altitude -altitude = aq.get("PLANE_ALTITUDE") -altitude = altitude + 1000 - -# Set the aircraft's current altitude -aq.set("PLANE_ALTITUDE", altitude) - -ae = AircraftEvents(sm) -# Trigger a simple event -event_to_trigger = ae.find("AP_MASTER") # Toggles autopilot on or off -event_to_trigger() - -# Trigger an event while passing a variable -target_altitude = 15000 -event_to_trigger = ae.find("AP_ALT_VAR_SET_ENGLISH") # Sets AP autopilot hold level -event_to_trigger(target_altitude) -sm.exit() -quit() -``` - -## HTTP interface example - -Run `glass_server.py` using Python 3. - -#### `http://localhost:5000` -Method: GET - -Variables: None - -Output: Web interface with moving map and aircraft information - -#### `http://localhost:5000/dataset/` -Method: GET - -Arguments to pass: - -|Argument|Location|Description| -|---|---|---| -|dataset_name|in path|can be navigation, airspeed compass, vertical_speed, fuel, flaps, throttle, gear, trim, autopilot, cabin| - -Description: Returns set of variables from simulator in JSON format - - -#### `http://localhost:5000/datapoint//get` -Method: GET - -Arguments to pass: - -|Argument|Location|Description| -|---|---|---| -|datapoint_name|in path|any variable name from MS SimConnect documentation| - -Description: Returns individual variable from simulator in JSON format - - -#### `http://localhost:5000/datapoint//set` -Method: POST - -Arguments to pass: - -|Argument|Location|Description| -|---|---|---| -|datapoint_name|in path|any variable name from MS SimConnect documentation| -|index (optional)|form or json|the relevant index if required (eg engine number) - if not passed defaults to None| -|value_to_use (optional)|value to set variable to - if not passed defaults to 0| - -Description: Sets datapoint in the simulator - -#### `http://localhost:5000/event//trigger` -Method: POST - -Arguments to pass: - -|Argument|Location|Description| -|---|---|---| -|event_name|in path|any event name from MS SimConnect documentation| -|value_to_use (optional)|value to pass to the event| - -Description: Triggers an event in the simulator - -## Running SimConnect on a separate system. - -#### Note: At this time SimConnect can only run on Windows hosts. - -Create a file called SimConnect.cfg in the same folder as your script. -#### Sample SimConnect.cfg: -```ini -; Example SimConnect client configurations -[SimConnect] -Protocol=IPv4 -Address= -Port=500 -``` -To enable the host running the sim to share over network, - -add \0.0.0.0\ - -under the \500\ in SimConnect.xml - -SimConnect.xml can be located at -#### `%AppData%\Microsoft Flight Simulator\SimConnect.xml` - -#### Sample SimConnect.xml: -```xml - - - - SimConnect Server Configuration - SimConnect.xml - - Static IP4 port - IPv4 - local - 500 -
0.0.0.0
- 64 - 41088 -
-... -``` -## Notes: - -Python 64-bit is needed. You may see this Error if running 32-bit python: - -```OSError: [WinError 193] %1 is not a valid Win32 application``` - -Per mracko on COM_RADIO_SET: - - MSFS uses the European COM frequency spacing of 8.33kHz for all default aircraft. - This means that in practice, you increment the frequency by 0.005 MHz and - skip x.x20, x.x45, x.x70, and x.x95 MHz frequencies. - Have a look here http://g3asr.co.uk/calculators/833kHz.htm - - -## Events and Variables - -Below are links to the Microsoft documentation - -[Function](https://docs.microsoft.com/en-us/previous-versions/microsoft-esp/cc526983(v=msdn.10)) - -[Event IDs](https://docs.microsoft.com/en-us/previous-versions/microsoft-esp/cc526980(v=msdn.10)) - -[Simulation Variables](https://docs.flightsimulator.com/html/Programming_Tools/SimVars/Simulation_Variables.htm) diff --git a/SimConnect/Attributes.py b/SimConnect/Attributes.py index 4e084aff..4ff948a7 100644 --- a/SimConnect/Attributes.py +++ b/SimConnect/Attributes.py @@ -3,6 +3,9 @@ from ctypes import * from ctypes.wintypes import * +# 本文件负责从 SimConnect DLL 暴露的 C 函数中创建 Python 端的接口定义, +# 通过 ctypes 声明参数与返回值,便于上层调用。 + class SimConnectDll(object): diff --git a/SimConnect/Constants.py b/SimConnect/Constants.py index d41bb1b3..d988be8d 100644 --- a/SimConnect/Constants.py +++ b/SimConnect/Constants.py @@ -4,6 +4,8 @@ LOGGER = logging.getLogger(__name__) +# 常量定义文件:对 SimConnect SDK 的关键数值进行 Python 端映射,方便重用。 + # //---------------------------------------------------------------------------- # // Constants diff --git a/SimConnect/Enum.py b/SimConnect/Enum.py index 8b42cad9..78637a99 100644 --- a/SimConnect/Enum.py +++ b/SimConnect/Enum.py @@ -3,6 +3,8 @@ from ctypes import * from .Constants import * +# 定义 SimConnect 需要的枚举与结构体,确保 ctypes 与 SDK 一致。 + import logging LOGGER = logging.getLogger(__name__) @@ -323,6 +325,14 @@ class SIMCONNECT_CLIENT_EVENT_ID(AutoName): # client-defined client event ID EVENT_SIM_STOP = auto() EVENT_SIM_PAUSED = auto() EVENT_SIM_UNPAUSED = auto() + EVENT_RACE_LAP = auto() + EVENT_RACE_END = auto() + EVENT_CUSTOM_MISSION_ACTION = auto() + EVENT_FLIGHT_LOADED = auto() + EVENT_MISSION_COMPLETED = auto() + EVENT_MP_SERVER_STARTED = auto() + EVENT_MP_CLIENT_STARTED = auto() + EVENT_MP_SESSION_ENDED = auto() pass @@ -425,39 +435,40 @@ class SIMCONNECT_RECV_EVENT_MULTIPLAYER_SESSION_ENDED(SIMCONNECT_RECV_EVENT): pass -# SIMCONNECT_DATA_RACE_RESULT +# SIMCONNECT_DATA_RACE_RESULT(SDK 使用 1 字节对齐) class SIMCONNECT_DATA_RACE_RESULT(Structure): + _pack_ = 1 _fields_ = [ - ("dwNumberOfRacers", DWORD), # The total number of racers - ("szPlayerName", c_char * MAX_PATH), # The name of the player - ( - "szSessionType", - c_char * MAX_PATH, - ), # The type of the multiplayer session: "LAN", "GAMESPY") - ("szAircraft", c_char * MAX_PATH), # The aircraft type - ("szPlayerRole", c_char * MAX_PATH), # The player role in the mission - ("fTotalTime", c_double), # Total time in seconds, 0 means DNF - ("fPenaltyTime", c_double), # Total penalty time in seconds - ( - "MissionGUID", - DWORD, - ), # The name of the mission to execute, NULL if no mission - ("dwIsDisqualified", c_double), # non 0 - disqualified, 0 - not disqualified + ("dwNumberOfRacers", DWORD), + ("MissionGUID", c_ubyte * 16), + ("szPlayerName", c_char * MAX_PATH), + ("szSessionType", c_char * MAX_PATH), + ("szAircraft", c_char * MAX_PATH), + ("szPlayerRole", c_char * MAX_PATH), + ("fTotalTime", c_double), + ("fPenaltyTime", c_double), + ("dwIsDisqualified", DWORD), ] class SIMCONNECT_RECV_EVENT_RACE_END( SIMCONNECT_RECV_EVENT ): # when dwID == SIMCONNECT_RECV_ID_EVENT_RACE_END - RacerData = SIMCONNECT_DATA_RACE_RESULT - _fields_ = [("dwRacerNumber", DWORD)] # The index of the racer the results are for + _pack_ = 1 + _fields_ = [ + ("dwRacerNumber", DWORD), + ("RacerData", SIMCONNECT_DATA_RACE_RESULT), + ] class SIMCONNECT_RECV_EVENT_RACE_LAP( SIMCONNECT_RECV_EVENT ): # when dwID == SIMCONNECT_RECV_ID_EVENT_RACE_LAP - RacerData = SIMCONNECT_DATA_RACE_RESULT - _fields_ = [("dwLapIndex", DWORD)] # The index of the lap the results are for + _pack_ = 1 + _fields_ = [ + ("dwLapIndex", DWORD), + ("RacerData", SIMCONNECT_DATA_RACE_RESULT), + ] class SIMCONNECT_RECV_SIMOBJECT_DATA(SIMCONNECT_RECV): @@ -535,12 +546,9 @@ class SIMCONNECT_RECV_SYSTEM_STATE( class SIMCONNECT_RECV_CUSTOM_ACTION(SIMCONNECT_RECV_EVENT): # _fields_ = [ - ("guidInstanceId", DWORD), # Instance id of the action that executed - ("dwWaitForCompletion", DWORD), # Wait for completion flag on the action - ( - "szPayLoad", - c_char, - ), # Variable length string payload associated with the mission action. + ("guidInstanceId", c_ubyte * 16), + ("dwWaitForCompletion", DWORD), + # 其后为变长 PayloadString,通过原始指针偏移读取 ] diff --git a/SimConnect/EventList.py b/SimConnect/EventList.py index bec37d14..dbe53719 100644 --- a/SimConnect/EventList.py +++ b/SimConnect/EventList.py @@ -1,5 +1,7 @@ from SimConnect import * +# 本文件列举大量可触发的 SimConnect 事件,并提供 EventHelper 以动态创建事件实例。 + class Event(object): diff --git a/SimConnect/FacilitiesList.py b/SimConnect/FacilitiesList.py index bbc0d918..1ce799f0 100644 --- a/SimConnect/FacilitiesList.py +++ b/SimConnect/FacilitiesList.py @@ -2,6 +2,8 @@ from .Enum import * from .Constants import * +# 设施请求工具:封装机场、导航点等设施列表的订阅与打印。 + class Facilitie(object): def __init__(self): diff --git a/SimConnect/RequestList.py b/SimConnect/RequestList.py index ccf16f59..90324965 100644 --- a/SimConnect/RequestList.py +++ b/SimConnect/RequestList.py @@ -2,6 +2,8 @@ from .Enum import * from .Constants import * +# 数据请求工具:封装航空器变量的定义、缓存与写入逻辑。 + class Request(object): diff --git a/SimConnect/SimConnect.dll b/SimConnect/SimConnect.dll index da40b739..a2bf93ed 100644 Binary files a/SimConnect/SimConnect.dll and b/SimConnect/SimConnect.dll differ diff --git a/SimConnect/SimConnect.py b/SimConnect/SimConnect.py index 5bb44652..cec41c4f 100644 --- a/SimConnect/SimConnect.py +++ b/SimConnect/SimConnect.py @@ -8,6 +8,8 @@ import os import threading +# 该模块封装与 SimConnect DLL 的交互,负责建立连接、事件分发、数据读写与模拟器状态管理。 + _library_path = os.path.splitext(os.path.abspath(__file__))[0] + '.dll' LOGGER = logging.getLogger(__name__) @@ -24,23 +26,39 @@ def IsHR(self, hr, value): return ctypes.c_ulong(_hr.value).value == value def handle_id_event(self, event): - uEventID = event.uEventID - if uEventID == self.dll.EventID.EVENT_SIM_START: + # 处理模拟器启动/停止/暂停类事件并同步内部状态 + uEventID = int(event.uEventID) + event_id = self.dll.EventID + if uEventID == int(event_id.EVENT_SIM_START.value): LOGGER.info("SIM START") self.running = True - if uEventID == self.dll.EventID.EVENT_SIM_STOP: + if uEventID == int(event_id.EVENT_SIM_STOP.value): LOGGER.info("SIM Stop") self.running = False - # Unknow whay not reciving - if uEventID == self.dll.EventID.EVENT_SIM_PAUSED: + if uEventID == int(event_id.EVENT_SIM_PAUSED.value): LOGGER.info("SIM Paused") self.paused = True - if uEventID == self.dll.EventID.EVENT_SIM_UNPAUSED: + if uEventID == int(event_id.EVENT_SIM_UNPAUSED.value): LOGGER.info("SIM Unpaused") self.paused = False def handle_simobject_event(self, ObjData): + # 处理对象数据回调,将结果写回已注册的请求 dwRequestID = ObjData.dwRequestID + dwObjectID = getattr(ObjData, "dwObjectID", None) + enum_state = getattr(self, "_aircraft_enum", None) + if enum_state and int(dwRequestID) == int(enum_state["request_id"]): + if dwObjectID is not None: + oid = int(dwObjectID) + if oid not in enum_state["ids"]: + enum_state["ids"].append(oid) + # dwoutof 表示本批总数;收齐后标记完成 + try: + if int(ObjData.dwentrynumber) >= int(ObjData.dwoutof): + enum_state["done"].set() + except Exception: + enum_state["done"].set() + if dwRequestID in self.Requests: _request = self.Requests[dwRequestID] rtype = _request.definitions[0][1].decode() @@ -52,9 +70,11 @@ def handle_simobject_event(self, ObjData): ObjData.dwData, POINTER(c_double * len(_request.definitions)) ).contents[0] else: - LOGGER.warn("Event ID: %d Not Handled." % (dwRequestID)) + if not (enum_state and int(dwRequestID) == int(enum_state["request_id"])): + LOGGER.warn("Event ID: %d Not Handled." % (dwRequestID)) def handle_exception_event(self, exc): + # 捕获并打印 SimConnect 抛出的异常,便于定位请求问题 _exception = SIMCONNECT_EXCEPTION(exc.dwException).name _unsendid = exc.UNKNOWN_SENDID _sendid = exc.dwSendID @@ -65,25 +85,146 @@ def handle_exception_event(self, exc): for _reqin in self.Requests: _request = self.Requests[_reqin] if _request.LastID == _unsendid: - LOGGER.warn("%s: in %s" % (_exception, _request.definitions[0])) + var_name = _request.definitions[0] + if _exception == "SIMCONNECT_EXCEPTION_DATA_ERROR": + self._log_exception( + "%s: 变量 %s 数据不可写入或值无效(Program Data 请用事件而非 SetData)" + % (_exception, var_name) + ) + else: + self._log_exception("%s: in %s" % (_exception, var_name)) return - LOGGER.warn(_exception) + op = self._consume_send_op(_unsendid) or self._consume_send_op(_sendid) + if op: + if _exception == "SIMCONNECT_EXCEPTION_INVALID_DATA_SIZE" and op == "AI_WAYPOINT_LIST": + self.waypoint_broken = True + self._log_exception("%s: in %s" % (_exception, op)) + return + + if _exception == "SIMCONNECT_EXCEPTION_DATA_ERROR": + self._log_exception("%s: SimConnect 数据写入被拒绝" % (_exception,)) + else: + self._log_exception(_exception) + + def _log_exception(self, message: str) -> None: + """同类异常限流,避免回放线程刷屏。""" + now = time.monotonic() + last = self._exc_log_ts.get(message, 0.0) + if now - last < 2.0: + self._exc_suppressed += 1 + return + extra = "" + if self._exc_suppressed: + extra = " (已抑制 %d 条重复)" % self._exc_suppressed + self._exc_suppressed = 0 + self._exc_log_ts[message] = now + LOGGER.warning("%s%s", message, extra) + + def _note_send_op(self, label: str) -> None: + temp = DWORD(0) + try: + self.dll.GetLastSentPacketID(self.hSimConnect, temp) + except Exception: + return + self._send_ops[int(temp.value)] = label + + def _consume_send_op(self, send_id): + try: + key = int(send_id) + except (TypeError, ValueError): + return None + return self._send_ops.pop(key, None) def handle_state_event(self, pData): - print("I:", pData.dwInteger, "F:", pData.fFloat, "S:", pData.szString) + req_id = int(pData.dwRequestID) + if req_id == int(self.dll.EventID.EVENT_SIM_PAUSED.value): + paused = int(pData.dwInteger) != 0 + LOGGER.debug("SIM pause state: %s", paused) + self.paused = paused + waiter = getattr(self, "_pause_state_waiter", None) + if waiter is not None: + try: + waiter(paused) + except Exception: + LOGGER.exception("pause state waiter failed") + return + LOGGER.debug( + "System state req=%s I=%s F=%s S=%s", + req_id, + pData.dwInteger, + pData.fFloat, + pData.szString, + ) # TODO: update callbackfunction to expand functions. def my_dispatch_proc(self, pData, cbData, pContext): - # print("my_dispatch_proc") + # 统一的分发函数,被 DLL 回调以分流不同类型的消息 dwID = pData.contents.dwID if dwID == SIMCONNECT_RECV_ID.SIMCONNECT_RECV_ID_EVENT: evt = cast(pData, POINTER(SIMCONNECT_RECV_EVENT)).contents - self.handle_id_event(evt) + if evt.uEventID == self.dll.EventID.EVENT_MISSION_COMPLETED.value: + result = int(evt.dwData) + LOGGER.info("MISSION COMPLETED: result=%s", result) + for handler in list(self._mission_completed_handlers): + try: + handler(result) + except Exception: + LOGGER.exception("mission completed handler failed") + else: + self.handle_id_event(evt) + + elif dwID == SIMCONNECT_RECV_ID.SIMCONNECT_RECV_ID_EVENT_FILENAME: + # ctypes 子类会追加字段而非覆盖 dwData,此处按 SDK 布局手动解析 + base = cast(pData, c_void_p).value + if base: + u_event_id = cast(base + 16, POINTER(DWORD)).contents.value + if u_event_id == self.dll.EventID.EVENT_FLIGHT_LOADED.value: + path = string_at(base + 20, MAX_PATH).decode("utf-8", errors="ignore").strip("\x00") + LOGGER.info("FLIGHT LOADED: %s", path) + for handler in list(self._flight_loaded_handlers): + try: + handler(path) + except Exception: + LOGGER.exception("flight loaded handler failed") elif dwID == SIMCONNECT_RECV_ID.SIMCONNECT_RECV_ID_SYSTEM_STATE: state = cast(pData, POINTER(SIMCONNECT_RECV_SYSTEM_STATE)).contents - self.handle_state_event(state) + if state.dwRequestID == self.dll.EventID.EVENT_FLIGHT_LOADED.value: + path = state.szString.decode("utf-8", errors="ignore").strip("\x00") + if path: + LOGGER.info("FLIGHT LOADED (state): %s", path) + for handler in list(self._flight_loaded_handlers): + try: + handler(path) + except Exception: + LOGGER.exception("flight loaded handler failed") + else: + self.handle_state_event(state) + + elif dwID == SIMCONNECT_RECV_ID.SIMCONNECT_RECV_ID_EVENT_MULTIPLAYER_SERVER_STARTED: + LOGGER.info("MULTIPLAYER SERVER STARTED") + for handler in list(self._mp_server_started_handlers): + try: + handler() + except Exception: + LOGGER.exception("mp server started handler failed") + + elif dwID == SIMCONNECT_RECV_ID.SIMCONNECT_RECV_ID_EVENT_MULTIPLAYER_CLIENT_STARTED: + LOGGER.info("MULTIPLAYER CLIENT STARTED") + for handler in list(self._mp_client_started_handlers): + try: + handler() + except Exception: + LOGGER.exception("mp client started handler failed") + + elif dwID == SIMCONNECT_RECV_ID.SIMCONNECT_RECV_ID_EVENT_MULTIPLAYER_SESSION_ENDED: + LOGGER.info("MULTIPLAYER SESSION ENDED") + for handler in list(self._mp_session_ended_handlers): + try: + handler() + except Exception: + LOGGER.exception("mp session ended handler failed") elif dwID == SIMCONNECT_RECV_ID.SIMCONNECT_RECV_ID_SIMOBJECT_DATA_BYTYPE: pObjData = cast( @@ -91,6 +232,12 @@ def my_dispatch_proc(self, pData, cbData, pContext): ).contents self.handle_simobject_event(pObjData) + elif dwID == SIMCONNECT_RECV_ID.SIMCONNECT_RECV_ID_SIMOBJECT_DATA: + pObjData = cast( + pData, POINTER(SIMCONNECT_RECV_SIMOBJECT_DATA) + ).contents + self.handle_simobject_event(pObjData) + elif dwID == SIMCONNECT_RECV_ID.SIMCONNECT_RECV_ID_OPEN: LOGGER.info("SIM OPEN") self.ok = True @@ -104,7 +251,14 @@ def my_dispatch_proc(self, pData, cbData, pContext): pData, POINTER(SIMCONNECT_RECV_ASSIGNED_OBJECT_ID) ).contents objectId = pObjData.dwObjectID + requestId = pObjData.dwRequestID os.environ["SIMCONNECT_OBJECT_ID"] = str(objectId) + waiter = self._object_id_waiters.pop(int(requestId), None) + if waiter is not None: + try: + waiter(int(requestId), int(objectId)) + except Exception: + LOGGER.exception("assigned object handler failed") elif (dwID == SIMCONNECT_RECV_ID.SIMCONNECT_RECV_ID_AIRPORT_LIST) or ( dwID == SIMCONNECT_RECV_ID.SIMCONNECT_RECV_ID_WAYPOINT_LIST) or ( @@ -119,6 +273,67 @@ def my_dispatch_proc(self, pData, cbData, pContext): _facilitie.parent.dump(pData) _facilitie.dump(pData) + elif dwID == SIMCONNECT_RECV_ID.SIMCONNECT_RECV_ID_EVENT_RACE_LAP: + evt = cast(pData, POINTER(SIMCONNECT_RECV_EVENT_RACE_LAP)).contents + lap_index = int(evt.dwLapIndex) + lap_time_s = float(evt.RacerData.fTotalTime) + penalty_s = float(evt.RacerData.fPenaltyTime) + LOGGER.info( + "RACE LAP: %s time=%.3fs penalty=%.3fs", + lap_index, + lap_time_s, + penalty_s, + ) + for handler in list(self._race_lap_handlers): + try: + handler(lap_index, lap_time_s, penalty_s) + except TypeError: + # 兼容旧签名 handler(lap_index) + try: + handler(lap_index) + except Exception: + LOGGER.exception("race lap handler failed") + except Exception: + LOGGER.exception("race lap handler failed") + + elif dwID == SIMCONNECT_RECV_ID.SIMCONNECT_RECV_ID_EVENT_RACE_END: + evt = cast(pData, POINTER(SIMCONNECT_RECV_EVENT_RACE_END)).contents + racer_number = int(evt.dwRacerNumber) + total_time_s = float(evt.RacerData.fTotalTime) + penalty_s = float(evt.RacerData.fPenaltyTime) + LOGGER.info( + "RACE END: racer %s total=%.3fs penalty=%.3fs", + racer_number, + total_time_s, + penalty_s, + ) + for handler in list(self._race_end_handlers): + try: + handler(racer_number, total_time_s, penalty_s) + except TypeError: + try: + handler(racer_number) + except Exception: + LOGGER.exception("race end handler failed") + except Exception: + LOGGER.exception("race end handler failed") + + elif dwID == SIMCONNECT_RECV_ID.SIMCONNECT_RECV_ID_CUSTOM_ACTION: + # RECV(12) + EVENT(12) + GUID(16) + Wait(4) = 44 + base = cast(pData, c_void_p).value + payload = "" + if base and cbData > 44: + try: + payload = string_at(base + 44).decode("utf-8", errors="ignore").strip() + except Exception: + payload = "" + LOGGER.info("CUSTOM ACTION payload: %r", payload) + for handler in list(self._custom_action_handlers): + try: + handler(payload) + except Exception: + LOGGER.exception("custom action handler failed") + elif dwID == SIMCONNECT_RECV_ID.SIMCONNECT_RECV_ID_QUIT: self.quit = 1 else: @@ -137,40 +352,379 @@ def __init__(self, auto_connect=True, library_path=_library_path): self.paused = False self.DEFINITION_POS = None self.DEFINITION_WAYPOINT = None + self.waypoint_broken = False + self._send_ops = {} + self._exc_log_ts = {} + self._exc_suppressed = 0 + self._race_lap_handlers = [] + self._race_end_handlers = [] + self._custom_action_handlers = [] + self._flight_loaded_handlers = [] + self._mission_completed_handlers = [] + self._mp_server_started_handlers = [] + self._mp_client_started_handlers = [] + self._mp_session_ended_handlers = [] + self._object_id_waiters = {} + self._aircraft_enum = None + self._pause_state_waiter = None + self._api_lock = threading.RLock() self.my_dispatch_proc_rd = self.dll.DispatchProc(self.my_dispatch_proc) if auto_connect: self.connect() - def connect(self): - try: - err = self.dll.Open( - byref(self.hSimConnect), LPCSTR(b"Request Data"), None, 0, 0, 0 + def add_race_lap_handler(self, handler): + """注册 Acceleration RaceLap 回调:handler(lap_index, lap_time_s, penalty_s)。""" + self._race_lap_handlers.append(handler) + + def add_race_end_handler(self, handler): + """注册 Acceleration RaceEnd 回调:handler(racer_number, total_time_s, penalty_s)。""" + self._race_end_handlers.append(handler) + + def add_custom_action_handler(self, handler): + """注册 Mission CustomAction 回调:handler(payload:str)。""" + self._custom_action_handlers.append(handler) + + def add_flight_loaded_handler(self, handler): + """注册 FlightLoaded 回调:handler(path:str)。""" + self._flight_loaded_handlers.append(handler) + + def add_mission_completed_handler(self, handler): + """注册 MissionCompleted 回调:handler(result_code:int)。""" + self._mission_completed_handlers.append(handler) + + def add_mp_server_started_handler(self, handler): + self._mp_server_started_handlers.append(handler) + + def add_mp_client_started_handler(self, handler): + self._mp_client_started_handlers.append(handler) + + def add_mp_session_ended_handler(self, handler): + self._mp_session_ended_handlers.append(handler) + + def request_flight_loaded(self) -> None: + """查询当前已加载的 .flt 路径。""" + with self._api_lock: + self.dll.RequestSystemState( + self.hSimConnect, + self.dll.EventID.EVENT_FLIGHT_LOADED, + b"FlightLoaded", ) - if self.IsHR(err, 0): - LOGGER.debug("Connected to Flight Simulator!") - # Request an event when the simulation starts - # The user is in control of the aircraft + def register_object_id_waiter(self, request_id, callback): + """注册一次性回调:AI 创建完成后 callback(request_id, object_id)。""" + key = int(request_id.value if hasattr(request_id, "value") else request_id) + self._object_id_waiters[key] = callback + + def remove_sim_object(self, object_id): + """移除模拟器中的 AI / 物体。""" + with self._api_lock: + req = self.new_request_id() + err = self.dll.AIRemoveObject( + self.hSimConnect, + DWORD(int(object_id)), + req.value, + ) + return self.IsHR(err, 0) + + def release_ai_control(self, object_id): + """释放 AI 自动控制,便于客户端接管航路点/位置。""" + with self._api_lock: + req = self.new_request_id() + err = self.dll.AIReleaseControl( + self.hSimConnect, + DWORD(int(object_id)), + req.value, + ) + return self.IsHR(err, 0) + + def set_init_position_on_object( + self, + object_id, + lat, + lon, + alt, + hdg=0, + pitch=0, + bank=0, + gnd=0, + speed=0, + ): + """对指定 ObjectID 写入 Initial Position。""" + with self._api_lock: + init_pos = SIMCONNECT_DATA_INITPOSITION() + init_pos.Altitude = float(alt) + init_pos.Latitude = float(lat) + init_pos.Longitude = float(lon) + init_pos.Pitch = float(pitch) + init_pos.Bank = float(bank) + init_pos.Heading = float(hdg) + init_pos.OnGround = 1 if gnd else 0 + init_pos.Airspeed = int(max(0, speed)) + if self.DEFINITION_POS is None: + self.DEFINITION_POS = self.new_def_id() + err = self.dll.AddToDataDefinition( + self.hSimConnect, + self.DEFINITION_POS.value, + b"Initial Position", + b"", + SIMCONNECT_DATATYPE.SIMCONNECT_DATATYPE_INITPOSITION, + 0, + SIMCONNECT_UNUSED, + ) + if not self.IsHR(err, 0): + self.DEFINITION_POS = None + return False + hr = self.dll.SetDataOnSimObject( + self.hSimConnect, + self.DEFINITION_POS.value, + DWORD(int(object_id)), + 0, + 0, + sizeof(SIMCONNECT_DATA_INITPOSITION), + byref(init_pos), + ) + if self.IsHR(hr, 0): + self._note_send_op("Initial Position") + return True + return False + + def set_double_on_object(self, object_id, simvar_name, unit_name, value): + """向指定 ObjectID 写入单个 FLOAT64 仿真变量。""" + with self._api_lock: + name = simvar_name if isinstance(simvar_name, (bytes, bytearray)) else str(simvar_name).encode() + unit = unit_name if isinstance(unit_name, (bytes, bytearray)) else str(unit_name).encode() + cache_key = (name, unit) + if not hasattr(self, "_object_var_defs"): + self._object_var_defs = {} + def_id = self._object_var_defs.get(cache_key) + if def_id is None: + def_id = self.new_def_id() + err = self.dll.AddToDataDefinition( + self.hSimConnect, + def_id.value, + name, + unit, + SIMCONNECT_DATATYPE.SIMCONNECT_DATATYPE_FLOAT64, + 0, + SIMCONNECT_UNUSED, + ) + if not self.IsHR(err, 0): + return False + self._object_var_defs[cache_key] = def_id + arr = (ctypes.c_double * 1)(float(value)) + hr = self.dll.SetDataOnSimObject( + self.hSimConnect, + def_id.value, + DWORD(int(object_id)), + 0, + 0, + sizeof(ctypes.c_double), + cast(arr, c_void_p), + ) + if self.IsHR(hr, 0): + self._note_send_op(name.decode(errors="ignore") if isinstance(name, (bytes, bytearray)) else str(name)) + return True + return False + + def create_non_atc_aircraft( + self, + container_title, + lat, + lon, + rqst, + tail_number="GHOST1", + hdg=0, + gnd=0, + alt=0, + pitch=0, + bank=0, + speed=0, + ): + """创建非 ATC 控制的 AI 飞机(适合航路点跟飞)。""" + with self._api_lock: + init_pos = SIMCONNECT_DATA_INITPOSITION() + init_pos.Altitude = float(alt) + init_pos.Latitude = float(lat) + init_pos.Longitude = float(lon) + init_pos.Pitch = float(pitch) + init_pos.Bank = float(bank) + init_pos.Heading = float(hdg) + init_pos.OnGround = 1 if gnd else 0 + init_pos.Airspeed = int(max(0, speed)) + title = container_title.encode() if isinstance(container_title, str) else container_title + tail = tail_number.encode() if isinstance(tail_number, str) else tail_number + err = self.dll.AICreateNonATCAircraft( + self.hSimConnect, + title, + tail, + init_pos, + rqst.value, + ) + return self.IsHR(err, 0) + + def set_ai_waypoints(self, object_id, waypoint_list): + """为指定 AI 对象下发航路点列表。""" + if not waypoint_list: + return False + if getattr(self, "waypoint_broken", False): + return False + with self._api_lock: + if self.DEFINITION_WAYPOINT is None: + self.DEFINITION_WAYPOINT = self.new_def_id() + err = self.dll.AddToDataDefinition( + self.hSimConnect, + self.DEFINITION_WAYPOINT.value, + b'AI WAYPOINT LIST', + b'', + SIMCONNECT_DATATYPE.SIMCONNECT_DATATYPE_WAYPOINT, + 0, + SIMCONNECT_UNUSED, + ) + if not self.IsHR(err, 0): + self.DEFINITION_WAYPOINT = None + return False + count = len(waypoint_list) + array_type = SIMCONNECT_DATA_WAYPOINT * count + arr = array_type() + for i, wp in enumerate(waypoint_list): + arr[i] = wp + hr = self.dll.SetDataOnSimObject( + self.hSimConnect, + self.DEFINITION_WAYPOINT.value, + DWORD(int(object_id)), + 0, + count, + sizeof(SIMCONNECT_DATA_WAYPOINT), + byref(arr), + ) + if self.IsHR(hr, 0): + self._note_send_op("AI_WAYPOINT_LIST") + return True + return False + + def enumerate_aircraft_ids(self, radius_m=200000, timeout=4.0): + """枚举当前范围内飞机 ObjectID(含用户机)。""" + collected = [] + done = threading.Event() + with self._api_lock: + def_id = self.new_def_id() + req_id = self.new_request_id() + err = self.dll.AddToDataDefinition( + self.hSimConnect, + def_id.value, + b"PLANE LATITUDE", + b"degrees", + SIMCONNECT_DATATYPE.SIMCONNECT_DATATYPE_FLOAT64, + 0, + SIMCONNECT_UNUSED, + ) + if not self.IsHR(err, 0): + return [] + + class _EnumReq: + def __init__(self): + self.DATA_REQUEST_ID = req_id + self.DATA_DEFINITION_ID = def_id + self.definitions = [(b"PLANE LATITUDE", b"degrees")] + self.outData = None + self.LastID = 0 + self.attemps = 1 + + def note_object(self, object_id): + oid = int(object_id) + if oid not in collected: + collected.append(oid) + + enum_req = _EnumReq() + self.Requests[req_id.value] = enum_req + self._aircraft_enum = { + "request_id": int(req_id.value), + "ids": collected, + "done": done, + } + self.dll.RequestDataOnSimObjectType( + self.hSimConnect, + req_id.value, + def_id.value, + int(radius_m), + SIMCONNECT_SIMOBJECT_TYPE.SIMCONNECT_SIMOBJECT_TYPE_AIRCRAFT, + ) + deadline = time.time() + timeout + while time.time() < deadline: + if done.is_set() and collected: + break + time.sleep(0.05) + with self._api_lock: + self.Requests.pop(req_id.value, None) + self._aircraft_enum = None + return list(collected) + + def connect(self, timeout=5.0): + try: + with self._api_lock: + err = self.dll.Open( + byref(self.hSimConnect), LPCSTR(b"Request Data"), None, 0, 0, 0 + ) + if not self.IsHR(err, 0): + raise ConnectionError("Did not find Flight Simulator running.") + LOGGER.debug("Connected to Flight Simulator!") self.dll.SubscribeToSystemEvent( self.hSimConnect, self.dll.EventID.EVENT_SIM_START, b"SimStart" ) - # The user is navigating the UI. self.dll.SubscribeToSystemEvent( self.hSimConnect, self.dll.EventID.EVENT_SIM_STOP, b"SimStop" ) - # Request a notification when the flight is paused self.dll.SubscribeToSystemEvent( self.hSimConnect, self.dll.EventID.EVENT_SIM_PAUSED, b"Paused" ) - # Request a notification when the flight is un-paused. self.dll.SubscribeToSystemEvent( self.hSimConnect, self.dll.EventID.EVENT_SIM_UNPAUSED, b"Unpaused" ) + self.dll.SubscribeToSystemEvent( + self.hSimConnect, self.dll.EventID.EVENT_RACE_LAP, b"RaceLap" + ) + self.dll.SubscribeToSystemEvent( + self.hSimConnect, self.dll.EventID.EVENT_RACE_END, b"RaceEnd" + ) + self.dll.SubscribeToSystemEvent( + self.hSimConnect, + self.dll.EventID.EVENT_CUSTOM_MISSION_ACTION, + b"CustomMissionActionExecuted", + ) + self.dll.SubscribeToSystemEvent( + self.hSimConnect, + self.dll.EventID.EVENT_FLIGHT_LOADED, + b"FlightLoaded", + ) + self.dll.SubscribeToSystemEvent( + self.hSimConnect, + self.dll.EventID.EVENT_MISSION_COMPLETED, + b"MissionCompleted", + ) + self.dll.SubscribeToSystemEvent( + self.hSimConnect, + self.dll.EventID.EVENT_MP_SERVER_STARTED, + b"MultiplayerServerStarted", + ) + self.dll.SubscribeToSystemEvent( + self.hSimConnect, + self.dll.EventID.EVENT_MP_CLIENT_STARTED, + b"MultiplayerClientStarted", + ) + self.dll.SubscribeToSystemEvent( + self.hSimConnect, + self.dll.EventID.EVENT_MP_SESSION_ENDED, + b"MultiplayerSessionEnded", + ) self.timerThread = threading.Thread(target=self._run) self.timerThread.daemon = True self.timerThread.start() - while self.ok is False: - pass + deadline = time.time() + timeout + while self.ok is False: + if time.time() > deadline: + self.exit() + raise ConnectionError("SimConnect connection timed out.") + time.sleep(0.01) except OSError: LOGGER.debug("Did not find Flight Simulator running.") raise ConnectionError("Did not find Flight Simulator running.") @@ -181,80 +735,95 @@ def _run(self): self.dll.CallDispatch(self.hSimConnect, self.my_dispatch_proc_rd, None) time.sleep(.002) except OSError as err: - print("OS error: {0}".format(err)) + LOGGER.debug("SimConnect dispatch error: %s", err) + self.quit = 1 def exit(self): self.quit = 1 - self.timerThread.join() - self.dll.Close(self.hSimConnect) + if hasattr(self, "timerThread") and self.timerThread.is_alive(): + self.timerThread.join(timeout=2.0) + with self._api_lock: + try: + self.dll.Close(self.hSimConnect) + except Exception: + pass def map_to_sim_event(self, name): - for m in self.dll.EventID: - if name.decode() == m.name: - LOGGER.debug("Already have event: ", m) - return m - - names = [m.name for m in self.dll.EventID] + [name.decode()] - self.dll.EventID = Enum(self.dll.EventID.__name__, names) - evnt = list(self.dll.EventID)[-1] - err = self.dll.MapClientEventToSimEvent(self.hSimConnect, evnt.value, name) - if self.IsHR(err, 0): - return evnt - else: + with self._api_lock: + for m in self.dll.EventID: + if name.decode() == m.name: + LOGGER.debug("Already have event: ", m) + return m + + names = [m.name for m in self.dll.EventID] + [name.decode()] + self.dll.EventID = Enum(self.dll.EventID.__name__, names) + evnt = list(self.dll.EventID)[-1] + err = self.dll.MapClientEventToSimEvent(self.hSimConnect, evnt.value, name) + if self.IsHR(err, 0): + return evnt LOGGER.error("Error: MapToSimEvent") return None def add_to_notification_group(self, group, evnt, bMaskable=False): - self.dll.AddClientEventToNotificationGroup( - self.hSimConnect, group, evnt, bMaskable - ) + with self._api_lock: + self.dll.AddClientEventToNotificationGroup( + self.hSimConnect, group, evnt, bMaskable + ) def request_data(self, _Request): - _Request.outData = None - self.dll.RequestDataOnSimObjectType( - self.hSimConnect, - _Request.DATA_REQUEST_ID.value, - _Request.DATA_DEFINITION_ID.value, - 0, - SIMCONNECT_SIMOBJECT_TYPE.SIMCONNECT_SIMOBJECT_TYPE_USER, - ) - temp = DWORD(0) - self.dll.GetLastSentPacketID(self.hSimConnect, temp) - _Request.LastID = temp.value + try: + with self._api_lock: + if self.quit: + return False + _Request.outData = None + self.dll.RequestDataOnSimObjectType( + self.hSimConnect, + _Request.DATA_REQUEST_ID.value, + _Request.DATA_DEFINITION_ID.value, + 0, + SIMCONNECT_SIMOBJECT_TYPE.SIMCONNECT_SIMOBJECT_TYPE_USER, + ) + temp = DWORD(0) + self.dll.GetLastSentPacketID(self.hSimConnect, temp) + _Request.LastID = temp.value + return True + except OSError: + self.quit = 1 + return False def set_data(self, _Request): - rtype = _Request.definitions[0][1].decode() - if 'string' in rtype.lower(): - pyarr = bytearray(_Request.outData) - dataarray = (ctypes.c_char * len(pyarr))(*pyarr) - else: - pyarr = list([_Request.outData]) - dataarray = (ctypes.c_double * len(pyarr))(*pyarr) + with self._api_lock: + rtype = _Request.definitions[0][1].decode() + if 'string' in rtype.lower(): + pyarr = bytearray(_Request.outData) + dataarray = (ctypes.c_char * len(pyarr))(*pyarr) + else: + pyarr = list([_Request.outData]) + dataarray = (ctypes.c_double * len(pyarr))(*pyarr) - pObjData = cast( - dataarray, c_void_p - ) - err = self.dll.SetDataOnSimObject( - self.hSimConnect, - _Request.DATA_DEFINITION_ID.value, - SIMCONNECT_SIMOBJECT_TYPE.SIMCONNECT_SIMOBJECT_TYPE_USER, - 0, - 0, - sizeof(ctypes.c_double) * len(pyarr), - pObjData - ) - if self.IsHR(err, 0): - # LOGGER.debug("Request Sent") - return True - else: + pObjData = cast( + dataarray, c_void_p + ) + err = self.dll.SetDataOnSimObject( + self.hSimConnect, + _Request.DATA_DEFINITION_ID.value, + SIMCONNECT_SIMOBJECT_TYPE.SIMCONNECT_SIMOBJECT_TYPE_USER, + 0, + 0, + sizeof(ctypes.c_double) * len(pyarr), + pObjData + ) + if self.IsHR(err, 0): + return True return False def get_data(self, _Request): - self.request_data(_Request) - # self.run() + if not self.request_data(_Request): + return False attemps = 0 while _Request.outData is None and attemps < _Request.attemps: - # self.run() + if self.quit: + return False time.sleep(.01) attemps += 1 if _Request.outData is None: @@ -263,71 +832,88 @@ def get_data(self, _Request): return True def send_event(self, evnt, data=DWORD(0)): - err = self.dll.TransmitClientEvent( - self.hSimConnect, - SIMCONNECT_OBJECT_ID_USER, - evnt.value, - data, - SIMCONNECT_GROUP_PRIORITY_HIGHEST, - DWORD(16), - ) + with self._api_lock: + err = self.dll.TransmitClientEvent( + self.hSimConnect, + SIMCONNECT_OBJECT_ID_USER, + evnt.value, + data, + SIMCONNECT_GROUP_PRIORITY_HIGHEST, + DWORD(16), + ) - if self.IsHR(err, 0): - # LOGGER.debug("Event Sent") - return True - else: + if self.IsHR(err, 0): + return True return False + def send_event_to_object(self, object_id, event_name, data=0): + """向指定 ObjectID 发送客户端事件(如 FREEZE_*)。""" + with self._api_lock: + name = event_name if isinstance(event_name, (bytes, bytearray)) else str(event_name).encode() + evnt = self.map_to_sim_event(name) + if evnt is None: + return False + err = self.dll.TransmitClientEvent( + self.hSimConnect, + DWORD(int(object_id)), + evnt.value, + DWORD(int(data)), + SIMCONNECT_GROUP_PRIORITY_HIGHEST, + DWORD(16), + ) + return self.IsHR(err, 0) + def new_def_id(self): - _name = "Definition" + str(len(list(self.dll.DATA_DEFINITION_ID))) - names = [m.name for m in self.dll.DATA_DEFINITION_ID] + [_name] + with self._api_lock: + _name = "Definition" + str(len(list(self.dll.DATA_DEFINITION_ID))) + names = [m.name for m in self.dll.DATA_DEFINITION_ID] + [_name] - self.dll.DATA_DEFINITION_ID = Enum(self.dll.DATA_DEFINITION_ID.__name__, names) - DEFINITION_ID = list(self.dll.DATA_DEFINITION_ID)[-1] - return DEFINITION_ID + self.dll.DATA_DEFINITION_ID = Enum(self.dll.DATA_DEFINITION_ID.__name__, names) + DEFINITION_ID = list(self.dll.DATA_DEFINITION_ID)[-1] + return DEFINITION_ID def new_request_id(self): - name = "Request" + str(len(self.dll.DATA_REQUEST_ID)) - names = [m.name for m in self.dll.DATA_REQUEST_ID] + [name] - self.dll.DATA_REQUEST_ID = Enum(self.dll.DATA_REQUEST_ID.__name__, names) - REQUEST_ID = list(self.dll.DATA_REQUEST_ID)[-1] + with self._api_lock: + name = "Request" + str(len(self.dll.DATA_REQUEST_ID)) + names = [m.name for m in self.dll.DATA_REQUEST_ID] + [name] + self.dll.DATA_REQUEST_ID = Enum(self.dll.DATA_REQUEST_ID.__name__, names) + REQUEST_ID = list(self.dll.DATA_REQUEST_ID)[-1] - return REQUEST_ID + return REQUEST_ID def add_waypoints(self, _waypointlist): - if self.DEFINITION_WAYPOINT is None: - self.DEFINITION_WAYPOINT = self.new_def_id() - err = self.dll.AddToDataDefinition( + with self._api_lock: + if self.DEFINITION_WAYPOINT is None: + self.DEFINITION_WAYPOINT = self.new_def_id() + err = self.dll.AddToDataDefinition( + self.hSimConnect, + self.DEFINITION_WAYPOINT.value, + b'AI WAYPOINT LIST', + b'number', + SIMCONNECT_DATATYPE.SIMCONNECT_DATATYPE_WAYPOINT, + 0, + SIMCONNECT_UNUSED, + ) + pyarr = [] + for waypt in _waypointlist: + for e in waypt._fields_: + pyarr.append(getattr(waypt, e[0])) + dataarray = (ctypes.c_double * len(pyarr))(*pyarr) + pObjData = cast( + dataarray, c_void_p + ) + sx = int(sizeof(ctypes.c_double) * (len(pyarr) / len(_waypointlist))) + hr = self.dll.SetDataOnSimObject( self.hSimConnect, self.DEFINITION_WAYPOINT.value, - b'AI WAYPOINT LIST', - b'number', - SIMCONNECT_DATATYPE.SIMCONNECT_DATATYPE_WAYPOINT, + SIMCONNECT_OBJECT_ID_USER, 0, - SIMCONNECT_UNUSED, + len(_waypointlist), + sx, + pObjData ) - pyarr = [] - for waypt in _waypointlist: - for e in waypt._fields_: - pyarr.append(getattr(waypt, e[0])) - dataarray = (ctypes.c_double * len(pyarr))(*pyarr) - pObjData = cast( - dataarray, c_void_p - ) - sx = int(sizeof(ctypes.c_double) * (len(pyarr) / len(_waypointlist))) - return - hr = self.dll.SetDataOnSimObject( - self.hSimConnect, - self.DEFINITION_WAYPOINT.value, - SIMCONNECT_OBJECT_ID_USER, - 0, - len(_waypointlist), - sx, - pObjData - ) - if self.IsHR(err, 0): - return True - else: + if self.IsHR(err, 0): + return True return False def set_pos( @@ -341,54 +927,54 @@ def set_pos( _Heading=0, _OnGround=0, ): - Init = SIMCONNECT_DATA_INITPOSITION() - Init.Altitude = _Altitude - Init.Latitude = _Latitude - Init.Longitude = _Longitude - Init.Pitch = _Pitch - Init.Bank = _Bank - Init.Heading = _Heading - Init.OnGround = _OnGround - Init.Airspeed = _Airspeed - - if self.DEFINITION_POS is None: - self.DEFINITION_POS = self.new_def_id() - err = self.dll.AddToDataDefinition( + with self._api_lock: + Init = SIMCONNECT_DATA_INITPOSITION() + Init.Altitude = _Altitude + Init.Latitude = _Latitude + Init.Longitude = _Longitude + Init.Pitch = _Pitch + Init.Bank = _Bank + Init.Heading = _Heading + Init.OnGround = _OnGround + Init.Airspeed = _Airspeed + + if self.DEFINITION_POS is None: + self.DEFINITION_POS = self.new_def_id() + err = self.dll.AddToDataDefinition( + self.hSimConnect, + self.DEFINITION_POS.value, + b'Initial Position', + b'', + SIMCONNECT_DATATYPE.SIMCONNECT_DATATYPE_INITPOSITION, + 0, + SIMCONNECT_UNUSED, + ) + + hr = self.dll.SetDataOnSimObject( self.hSimConnect, self.DEFINITION_POS.value, - b'Initial Position', - b'', - SIMCONNECT_DATATYPE.SIMCONNECT_DATATYPE_INITPOSITION, + SIMCONNECT_OBJECT_ID_USER, 0, - SIMCONNECT_UNUSED, + 0, + sizeof(Init), + pointer(Init) ) - - hr = self.dll.SetDataOnSimObject( - self.hSimConnect, - self.DEFINITION_POS.value, - SIMCONNECT_OBJECT_ID_USER, - 0, - 0, - sizeof(Init), - pointer(Init) - ) - if self.IsHR(hr, 0): - return True - else: + if self.IsHR(hr, 0): + return True return False def load_flight(self, flt_path): - hr = self.dll.FlightLoad(self.hSimConnect, flt_path.encode()) - if self.IsHR(hr, 0): - return True - else: + with self._api_lock: + hr = self.dll.FlightLoad(self.hSimConnect, flt_path.encode()) + if self.IsHR(hr, 0): + return True return False def load_flight_plan(self, pln_path): - hr = self.dll.FlightPlanLoad(self.hSimConnect, pln_path.encode()) - if self.IsHR(hr, 0): - return True - else: + with self._api_lock: + hr = self.dll.FlightPlanLoad(self.hSimConnect, pln_path.encode()) + if self.IsHR(hr, 0): + return True return False def save_flight( @@ -400,10 +986,10 @@ def save_flight( flt_mission_location='Custom departure', flt_original_flight='', flt_flight_type='NORMAL'): - hr = self.dll.FlightSave(self.hSimConnect, flt_path.encode(), flt_title.encode(), flt_description.encode(), 0) - if not self.IsHR(hr, 0): - return False - + with self._api_lock: + hr = self.dll.FlightSave(self.hSimConnect, flt_path.encode(), flt_title.encode(), flt_description.encode(), 0) + if not self.IsHR(hr, 0): + return False dicp = self.flight_to_dic(flt_path) if 'MissionType' not in dicp['Main']: dicp['Main']['MissionType'] = flt_mission_type @@ -420,12 +1006,42 @@ def save_flight( return False + def refresh_pause_state(self, timeout: float = 0.15): + """向模拟器查询当前暂停状态,并同步 ``self.paused``。""" + if self.quit: + return None + done = threading.Event() + result = {"paused": None} + + def _on_result(paused: bool) -> None: + result["paused"] = bool(paused) + done.set() + + with self._api_lock: + self._pause_state_waiter = _on_result + try: + hr = self.dll.RequestSystemState( + self.hSimConnect, + self.dll.EventID.EVENT_SIM_PAUSED.value, + b"Sim", + ) + except Exception: + self._pause_state_waiter = None + return None + if not self.IsHR(hr, 0): + self._pause_state_waiter = None + return None + if done.wait(max(0.05, float(timeout))): + self._pause_state_waiter = None + paused = result["paused"] + if paused is not None: + self.paused = paused + return paused + self._pause_state_waiter = None + return None + def get_paused(self): - hr = self.dll.RequestSystemState( - self.hSimConnect, - self.dll.EventID.EVENT_SIM_PAUSED, - b"Sim" - ) + return self.refresh_pause_state() def dic_to_flight(self, dic, fpath): with open(fpath, "w") as tempfile: @@ -452,31 +1068,33 @@ def flight_to_dic(self, fpath): return dic def sendText(self, text, timeSeconds=5, TEXT_TYPE=SIMCONNECT_TEXT_TYPE.SIMCONNECT_TEXT_TYPE_PRINT_WHITE): - pyarr = bytearray(text.encode()) - dataarray = (ctypes.c_char * len(pyarr))(*pyarr) - pObjData = cast(dataarray, c_void_p) - self.dll.Text( - self.hSimConnect, - TEXT_TYPE, - timeSeconds, - 0, - sizeof(ctypes.c_double) * len(pyarr), - pObjData - ) + with self._api_lock: + pyarr = bytearray(text.encode()) + dataarray = (ctypes.c_char * len(pyarr))(*pyarr) + pObjData = cast(dataarray, c_void_p) + self.dll.Text( + self.hSimConnect, + TEXT_TYPE, + timeSeconds, + 0, + sizeof(ctypes.c_double) * len(pyarr), + pObjData + ) def createSimulatedObject(self, name, lat, lon, rqst, hdg=0, gnd=1, alt=0, pitch=0, bank=0, speed=0): - simInitPos = SIMCONNECT_DATA_INITPOSITION() - simInitPos.Altitude = alt - simInitPos.Latitude = lat - simInitPos.Longitude = lon - simInitPos.Pitch = pitch - simInitPos.Bank = bank - simInitPos.Heading = hdg - simInitPos.OnGround = gnd - simInitPos.Airspeed = speed - self.dll.AICreateSimulatedObject( - self.hSimConnect, - name.encode(), - simInitPos, - rqst.value - ) + with self._api_lock: + simInitPos = SIMCONNECT_DATA_INITPOSITION() + simInitPos.Altitude = alt + simInitPos.Latitude = lat + simInitPos.Longitude = lon + simInitPos.Pitch = pitch + simInitPos.Bank = bank + simInitPos.Heading = hdg + simInitPos.OnGround = gnd + simInitPos.Airspeed = speed + self.dll.AICreateSimulatedObject( + self.hSimConnect, + name.encode(), + simInitPos, + rqst.value + ) diff --git a/SimConnect/__init__.py b/SimConnect/__init__.py index ea8fd2ac..60da27ef 100644 --- a/SimConnect/__init__.py +++ b/SimConnect/__init__.py @@ -3,6 +3,8 @@ from .EventList import AircraftEvents, Event from .FacilitiesList import FacilitiesRequests, Facilitie +# 对外暴露主要类,便于用户通过 from SimConnect import * 快速获取接口。 + def int_or_str(value): try: diff --git a/app_controller.py b/app_controller.py new file mode 100644 index 00000000..39dc2f5f --- /dev/null +++ b/app_controller.py @@ -0,0 +1,1582 @@ +# -*- coding: utf-8 -*- +"""应用编排:自检 → 登录 → 训练助手(SimConnect / 鉴权 / 轮询)。""" + +from __future__ import annotations + +import threading +import time +from typing import Optional + +from PySide2.QtCore import QEvent, QObject, Qt, QTimer, Signal +from PySide2.QtWidgets import QApplication +from SimConnect import Event + +from app_frame import ( + LAP_TABLE_ROWS, + format_lap_time, + show_start_monitoring, + show_sim_not_found, + show_trajectory_save_result, +) +from models.runtime_watch import RuntimeWatch, aircraft_has_crashed +from models.shadow_plane import ( + SHADOW_LEAD_DEFAULT_S, + TrajectoryRecorder, + default_trajectory_dir, + launch_shadow_follow, +) +from models.chart import ChartHistoryBuffer, TRAINING_CHART +from models.hardware_monitor import poll_stick, reload_stick, stick_record_fields +from models.lap_reference import LAP_COUNT, laps_from_session +from models.track_map import points_from_trajectory_samples +from services.auth import ( + AuthFailed, + check_auth_server_online, + check_network_online, + try_auth_login, +) +from models.data_bridge import extract, read_aq_snapshot +from services.simulator import ( + CHT_VAR, + CHECK_ITEM_MIN_S, + COLD_CABIN_DELAY_MS, + DAMAGE_VAR, + DATA_POLL_MS, + is_plane_moved, + read_simulation_rate, + chart_elapsed_time_s, + SIM_RATE_MAX_STEPS, + SIM_RATE_STEP_MS, + TARGET_CHT_F, + TARGET_SIM_RATE, + TORQUE_VAR, + c_to_f, + f_to_c, + parse_flt_mission_type, + parse_mission_title, + safe_disconnect_simulator, + try_connect_simulator, +) +from ui.auth_page import AuthPage +from ui.sim_check_page import SimCheckPage +from ui.training_assist_page import TrainingAssistPage + + +class _UiBridge(QObject): + check_item = Signal(int, str, str) + check_action = Signal(str, bool) + check_step_failed = Signal(int, str) + check_all_passed = Signal(object) + login_finished = Signal(object) + sim_connected = Signal(object, object, object, object) + sim_not_found = Signal() + shadow_follow_done = Signal(bool, str) + race_lap = Signal(int, float, float) + race_end = Signal(int, float, float) + flight_loaded = Signal(str) + mission_completed = Signal(int) + mp_server_started = Signal() + mp_client_started = Signal() + mp_session_ended = Signal() + custom_action = Signal(str) + + +class AppController(QObject): + """驱动 AppShell 三阶段流程与训练业务逻辑。""" + + def __init__(self, shell): + super().__init__() + self.shell = shell + self._bridge = _UiBridge() + self._closing = False + self._phase = "sim_check" + + self._sim_connection = None + self._detecting_check = False + self._check_ready = False + self._check_closing = False + + self._logging_in = False + self._login_result = None + + self.sm = None + self.aq = None + self.sim_rate_incr_event = None + self.sim_rate_decr_event = None + self.pause_on_event = None + self.situation_reset_event = None + + self.history = ChartHistoryBuffer(TRAINING_CHART) + self._speed_band_source = "airspeed" + self._training_wired = False + self._sim_dialog_shown = False + self._detecting_sim = False + self._preconnected = False + self._runtime = RuntimeWatch() + self._baseline_lat = None + self._baseline_lon = None + self._lap_times = [] + self._lap_count = 0 + self._race_finished = False + self._race_events_bound = False + self._session_events_bound = False + self._session_mode = "freeflight" + self._mp_active = False + self._mission_title = "" + self._last_flt_path = "" + self._live_readouts_enabled = False + self._trajectory_recorder = TrajectoryRecorder(default_trajectory_dir()) + self._shadow_controller = None + self._shadow_follow_busy = False + self._shadow_lead_s = SHADOW_LEAD_DEFAULT_S + self._shadow_smoke_enabled = True + self._shadow_trajectory_path = "" + self._shadow_cold_cabin_enabled = False + self._monitor_cold_cabin_enabled = False + self._monitor_shadow_follow_enabled = False + self._suspend_sim_poll = False + self._chart_time_origin = None + + self._sim_rate_target = None + self._sim_rate_steps_left = 0 + + self._poll_timer = QTimer(self) + self._poll_timer.timeout.connect(self._poll) + self._metrics_timer = QTimer(self) + self._metrics_timer.setTimerType(Qt.PreciseTimer) + self._metrics_timer.timeout.connect(self._poll_metrics) + self._stick_timer = QTimer(self) + self._stick_timer.setInterval(DATA_POLL_MS) + self._stick_timer.timeout.connect(self._poll_stick) + self._sim_rate_timer = QTimer(self) + self._sim_rate_timer.timeout.connect(self._sim_rate_step) + self._login_poll_timer = QTimer(self) + self._login_poll_timer.timeout.connect(self._poll_login) + + self._wire_bridge() + self._wire_pages() + + def _wire_bridge(self) -> None: + b = self._bridge + b.check_item.connect(self._on_check_item) + b.check_action.connect(self._on_check_action) + b.check_step_failed.connect(self._on_check_step_failed) + b.check_all_passed.connect(self._on_check_all_passed) + b.login_finished.connect(self._on_login_finished) + b.sim_connected.connect(self._on_sim_connected) + b.sim_not_found.connect(self._on_sim_not_found) + b.shadow_follow_done.connect(self._on_shadow_follow_done) + b.race_lap.connect(self._apply_race_lap) + b.race_end.connect(self._apply_race_end) + b.flight_loaded.connect(self._apply_flight_loaded) + b.mission_completed.connect(self._apply_mission_completed) + b.mp_server_started.connect(self._apply_mp_server_started) + b.mp_client_started.connect(self._apply_mp_client_started) + b.mp_session_ended.connect(self._apply_mp_session_ended) + b.custom_action.connect(self._apply_custom_action) + + def _wire_pages(self) -> None: + sim = self._sim_page() + if sim is not None: + sim.actionClicked.connect(self._on_sim_check_action) + auth = self._auth_page() + if auth is not None: + auth.loginRequested.connect(self._on_login_requested) + self._wire_training_page() + self.shell.installEventFilter(self) + + def _wire_training_page(self) -> None: + if self._training_wired: + return + train = self._train_page() + if train is None: + return + train.startMonitorClicked.connect(self._on_start_monitor) + train.rateClicked.connect(self._on_sim_rate) + train.monitorClicked.connect(self._on_monitor_action) + if hasattr(train, "speedSourceChanged"): + train.speedSourceChanged.connect(self._on_speed_band_source_changed) + self._speed_band_source = train.speed_band_source() + self._training_wired = True + + def _ensure_training_page(self) -> None: + """自检窗已显示后再构建训练页,避免 Charts/pygame 抢先闪顶层窗。""" + from ui import register_training_feature + + register_training_feature(self.shell) + self._wire_training_page() + + def _on_speed_band_source_changed(self, source: str) -> None: + self._speed_band_source = ( + "groundspeed" if source == "groundspeed" else "airspeed" + ) + self._push_aux_charts() + + def eventFilter(self, obj, event): + if obj is self.shell and event.type() == QEvent.Close: + self._on_close() + return super().eventFilter(obj, event) + + def _sim_page(self) -> Optional[SimCheckPage]: + page = self.shell.registry.get("flow.sim_check") + return page if isinstance(page, SimCheckPage) else None + + def _auth_page(self) -> Optional[AuthPage]: + page = self.shell.registry.get("flow.auth") + return page if isinstance(page, AuthPage) else None + + def _train_page(self) -> Optional[TrainingAssistPage]: + page = self.shell.registry.get("training_assist") + return page if isinstance(page, TrainingAssistPage) else None + + def start(self) -> None: + self._phase = "sim_check" + self.shell.show_feature("flow.sim_check") + self.shell.show() + # 训练页(Charts/pygame)不要在自检阶段预热,否则会在初始化页再闪一窗 + QTimer.singleShot(180, self._start_check) + + # ---- SimCheck ---- + + def _start_check(self) -> None: + if self._detecting_check or self._check_closing or self._closing: + return + self._detecting_check = True + self._check_ready = False + if self._sim_connection: + safe_disconnect_simulator(self._sim_connection[0]) + self._sim_connection = None + page = self._sim_page() + if page is not None: + page.set_action("START", False) + page.set_busy(True) + for i in range(3): + page.set_item(i, "idle", "等待") + threading.Thread(target=self._check_worker, daemon=True).start() + + def _check_worker(self) -> None: + conn = None + steps = ( + (0, "检测中", "OK", "无法连接外网", check_network_online), + (1, "检测中", "OK", "服务器不可达", check_auth_server_online), + ) + for index, running, ok_text, fail_text, probe in steps: + if self._check_closing: + return + self._bridge.check_item.emit(index, "run", running) + started = time.monotonic() + ok = False + try: + ok = bool(probe()) + except Exception: + ok = False + remain = CHECK_ITEM_MIN_S - (time.monotonic() - started) + if remain > 0: + time.sleep(remain) + if self._check_closing: + return + if not ok: + self._bridge.check_step_failed.emit(index, fail_text) + return + self._bridge.check_item.emit(index, "ok", ok_text) + + if self._check_closing: + return + self._bridge.check_item.emit(2, "run", "检测中") + started = time.monotonic() + try: + conn = try_connect_simulator() + ok = True + except Exception: + ok = False + remain = CHECK_ITEM_MIN_S - (time.monotonic() - started) + if remain > 0: + time.sleep(remain) + if self._check_closing: + if conn: + safe_disconnect_simulator(conn[0]) + return + if not ok: + self._bridge.check_step_failed.emit(2, "未侦测到运行中的FSX程序") + return + self._bridge.check_all_passed.emit(conn) + + def _on_check_item(self, index: int, state: str, detail: str) -> None: + page = self._sim_page() + if page is not None: + page.set_item(index, state, detail) + + def _on_check_action(self, text: str, enabled: bool) -> None: + page = self._sim_page() + if page is not None: + page.set_action(text, enabled) + page.set_busy(False) + + def _on_check_step_failed(self, index: int, detail: str) -> None: + self._detecting_check = False + if self._check_closing: + return + page = self._sim_page() + if page is not None: + page.set_item(index, "fail", detail) + for later in range(index + 1, 3): + page.set_item(later, "idle", "未检测") + page.set_action("重试", True) + page.set_busy(False) + + def _on_check_all_passed(self, conn) -> None: + self._detecting_check = False + if self._check_closing: + safe_disconnect_simulator(conn[0]) + return + self._sim_connection = conn + self._check_ready = True + page = self._sim_page() + if page is not None: + page.set_item(2, "ok", "OK") + page.set_action("START", True) + page.set_busy(False) + + def _on_sim_check_action(self) -> None: + if self._detecting_check: + return + if self._check_ready: + self._on_sim_check_start() + return + self._start_check() + + def _on_sim_check_start(self) -> None: + if not self._check_ready or not self._sim_connection: + return + self._phase = "auth" + self.shell.show_feature("flow.auth") + + # ---- Auth ---- + + def _on_login_requested(self, username: str, password: str) -> None: + if self._logging_in: + return + auth = self._auth_page() + username = (username or "").strip() + if not username: + if auth is not None: + auth.set_status("请输入用户名", "error") + return + if not password: + if auth is not None: + auth.set_status("请输入密码", "error") + return + self._logging_in = True + self._login_result = None + if auth is not None: + auth.set_login_enabled(False) + auth.set_status("正在登录…") + threading.Thread(target=self._login_worker, args=(username, password), daemon=True).start() + self._login_poll_timer.start(50) + + def _login_worker(self, username: str, password: str) -> None: + try: + try_auth_login(username, password) + self._login_result = ("ok", None) + except Exception as exc: + self._login_result = ("fail", exc) + + def _poll_login(self) -> None: + if self._login_result is None: + return + self._login_poll_timer.stop() + status, payload = self._login_result + self._login_result = None + self._bridge.login_finished.emit((status, payload)) + + def _on_login_finished(self, result) -> None: + status, payload = result + if status == "ok": + self._on_login_ok() + return + self._on_login_failed(payload) + + def _format_login_error(self, exc) -> str: + if isinstance(exc, AuthFailed): + return "用户名或密码错误" + msg = str(exc).strip() + low = msg.lower() + if "1045" in msg or "access denied" in low: + return "鉴权服务账号无效" + if "1049" in msg or "unknown database" in low: + return "鉴权库不存在" + if "1146" in msg or "doesn't exist" in low: + return "鉴权用户表不存在" + if "1054" in msg or "unknown column" in low: + return "鉴权表字段不匹配" + if "2003" in msg or "can't connect" in low or "timed out" in low or "timeout" in low: + return "无法连接鉴权服务器" + if "passlib" in low: + return "缺少 passlib,请先安装" + if "pymysql" in low: + return "缺少 pymysql,请先安装" + if "caching_sha2_password" in low or "auth plugin" in low: + return "鉴权库认证插件不兼容" + return msg or f"登录失败:{type(exc).__name__}" + + def _on_login_failed(self, exc) -> None: + self._logging_in = False + auth = self._auth_page() + if auth is not None: + auth.set_login_enabled(True) + auth.set_status(self._format_login_error(exc), "error") + + def _on_login_ok(self) -> None: + self._logging_in = False + conn = self._sim_connection + self._sim_connection = None + if conn: + self.sm, self.aq, self.sim_rate_incr_event, self.sim_rate_decr_event = conn + self._preconnected = True + self._phase = "training" + self._ensure_training_page() + self.shell.show_feature("training_assist") + self._start_training() + + def _start_training(self) -> None: + train = self._train_page() + if self.sm: + self._init_sim_events() + self._bind_race_events(self.sm) + self._bind_session_events(self.sm) + reload_stick() + if not self._stick_timer.isActive(): + self._stick_timer.start() + if self._preconnected: + QTimer.singleShot(100, self._on_preconnected) + else: + QTimer.singleShot(100, self._detect_simulator) + + # ---- Training ---- + + def _on_preconnected(self) -> None: + if self._closing: + return + if self.sm and self.sm.quit: + self._handle_sim_lost() + return + train = self._train_page() + if train is None: + return + self._sync_rate_button() + train.set_status("已连接模拟器", "ok") + self._sync_session_mode_ui() + self._sync_start_monitor_button() + + def _detect_simulator(self) -> None: + if self._detecting_sim or self._closing: + return + self._detecting_sim = True + train = self._train_page() + if train is not None: + train.set_status("正在检测模拟器...", "muted") + threading.Thread(target=self._detect_simulator_worker, daemon=True).start() + + def _detect_simulator_worker(self) -> None: + try: + conn = try_connect_simulator() + except (ConnectionError, OSError): + self._bridge.sim_not_found.emit() + return + self._bridge.sim_connected.emit(*conn) + + def _on_sim_connected(self, sm, aq, sim_rate_incr, sim_rate_decr) -> None: + self._detecting_sim = False + if self._closing: + safe_disconnect_simulator(sm) + return + self.sm = sm + self.aq = aq + self.sim_rate_incr_event = sim_rate_incr + self.sim_rate_decr_event = sim_rate_decr + self._init_sim_events() + self._bind_race_events(sm) + self._bind_session_events(sm) + train = self._train_page() + if train is None: + return + self._sync_rate_button() + train.set_status("已连接模拟器", "ok") + self._sync_session_mode_ui() + self._sync_start_monitor_button() + + def _on_sim_not_found(self) -> None: + self._detecting_sim = False + if self._closing: + return + train = self._train_page() + if train is not None: + train.set_status("未连接模拟器", "error") + self._show_sim_not_found_dialog() + + def _show_sim_not_found_dialog(self) -> None: + if self._sim_dialog_shown or self._closing: + return + self._sim_dialog_shown = True + show_sim_not_found(self.shell) + self._quit_app() + + def _chart_has_data(self) -> bool: + return len(self.history) > 0 + + def _is_shadow_active(self) -> bool: + ctrl = self._shadow_controller + return ctrl is not None and ctrl.object_id is not None + + def _refresh_monitor_button(self) -> None: + train = self._train_page() + if train is None: + return + if self._runtime.is_recording(): + train.set_monitor_button("stop") + elif self._runtime.is_waiting(): + train.set_monitor_button("pending") + elif ( + not self._runtime.is_active() + and self._chart_has_data() + and self._trajectory_recorder.has_pending_samples + ): + train.set_monitor_button("save") + else: + train.set_monitor_button(None) + self._sync_start_monitor_button() + + def _sync_start_monitor_button(self) -> None: + train = self._train_page() + if train is None: + return + ready = bool((self._mission_title or "").strip()) + train.set_start_monitor_mission_ready(ready) + + def _on_monitor_action(self) -> None: + train = self._train_page() + if self._runtime.is_active(): + had_shadow = ( + self._shadow_controller is not None + and self._shadow_controller.object_id is not None + ) + self._stop_monitoring(save=False) + if train is not None: + if had_shadow: + train.set_status("监测已停止,跟飞已结束", "warn") + else: + train.set_status("监测已停止", "warn") + self._refresh_monitor_button() + return + if self._trajectory_recorder.has_pending_samples: + self._save_pending_trajectory() + + def _save_pending_trajectory(self) -> None: + total_s = self._total_flight_time_s() + path = self._trajectory_recorder.save( + total_s=total_s, + laps_s=laps_from_session(self._lap_times), + ) + ok = path is not None + show_trajectory_save_result( + self.shell, + ok=ok, + path=path, + message="保存成功" if ok else "无有效轨迹数据,未写入文件", + ) + train = self._train_page() + if train is not None: + if ok: + train.set_status(f"轨迹已保存:{path.name}", "ok") + else: + train.set_status("轨迹保存失败:无有效数据", "error") + self._refresh_monitor_button() + + def _handle_sim_lost(self) -> None: + if self._closing: + return + self._runtime.reset() + self._poll_timer.stop() + self._metrics_timer.stop() + self._baseline_lat = None + self._baseline_lon = None + self._chart_time_origin = None + try: + self._trajectory_recorder.stop() + except Exception: + pass + self._mp_active = False + self._session_mode = "freeflight" + self._mission_title = "" + self._last_flt_path = "" + self._live_readouts_enabled = False + self._race_events_bound = False + self._session_events_bound = False + safe_disconnect_simulator(self.sm) + self.sm = None + self.aq = None + train = self._train_page() + if train is not None: + train.set_monitor_button(None) + train.set_session_mode("freeflight") + train.set_mission_title(None) + self._clear_live_readouts() + train.set_start_monitor_mission_ready(False) + self._show_sim_not_found_dialog() + + def _start_monitor_waiting(self) -> None: + if not self.aq or self._closing: + return + if self._runtime.is_recording(): + self._trajectory_recorder.stop() + self._runtime.enter_waiting() + self._baseline_lat = None + self._baseline_lon = None + self._chart_time_origin = None + self._reset_mission_tracking(update_labels=False) + self._request_sim_pause() + _, self._baseline_lat, self._baseline_lon = is_plane_moved( + self.aq, None, None + ) + train = self._train_page() + if train is not None: + train.set_status("等待解除暂停状态后开始监测...", "warn") + self._refresh_monitor_button() + self._refresh_session_info() + self._poll() + self._refresh_runtime_timers() + + def _stop_monitoring(self, save: bool = False) -> None: + was_recording = self._runtime.is_recording() + self._runtime.return_to_idle() + self._baseline_lat = None + self._baseline_lon = None + self._chart_time_origin = None + self._poll_timer.stop() + if was_recording: + self._trajectory_recorder.stop() + if save and was_recording: + self._trajectory_recorder.save( + total_s=self._total_flight_time_s(), + laps_s=laps_from_session(self._lap_times), + ) + self._stop_shadow_soft() + train = self._train_page() + if train is not None: + self._refresh_monitor_button() + self._refresh_runtime_timers() + + def _apply_shadow_pause(self, paused: bool) -> None: + ctrl = self._shadow_controller + if ctrl is None or ctrl.object_id is None: + return + try: + ctrl.set_replay_paused(paused) + except Exception: + pass + + def _refresh_metrics(self) -> None: + if not self._runtime.should_refresh_metrics(self._live_readouts_enabled): + return + cht_f, torque, health = self._read_live_readouts() + self._apply_live_readouts(cht_f, torque, health) + + def _refresh_runtime_timers(self) -> None: + if self._closing or not self.aq: + self._poll_timer.stop() + self._metrics_timer.stop() + return + if self._runtime.should_poll(self._is_shadow_active(), self._live_readouts_enabled): + self._poll_timer.start(DATA_POLL_MS) + else: + self._poll_timer.stop() + if self._runtime.should_refresh_metrics(self._live_readouts_enabled): + if not self._metrics_timer.isActive(): + self._metrics_timer.start(DATA_POLL_MS) + else: + self._metrics_timer.stop() + + def _reset_mission_tracking(self, update_labels=True) -> None: + self._lap_count = 0 + self._lap_times = [] + self._race_finished = False + if update_labels: + self._refresh_mission_labels() + + def _lap_entry_by_number(self, lap_number): + for item in self._lap_times: + if int(item["lap"]) == int(lap_number): + return item + return None + + def _lap_time_by_number(self, lap_number): + entry = self._lap_entry_by_number(lap_number) + return entry["time_s"] if entry else None + + def _lap_penalty_delta_by_number(self, lap_number): + entry = self._lap_entry_by_number(lap_number) + if entry is None: + return None + if "penalty_delta_s" in entry: + delta = float(entry.get("penalty_delta_s") or 0.0) + return delta if delta > 0 else None + cumulative = float(entry.get("penalty_s") or 0.0) + prev_cumulative = 0.0 + for item in self._lap_times: + if int(item["lap"]) >= int(lap_number): + continue + prev_cumulative = float(item.get("penalty_s") or 0.0) + delta = cumulative - prev_cumulative + return delta if delta > 0 else None + + def _total_penalty_s(self): + if not self._lap_times: + return None + latest = max(self._lap_times, key=lambda x: int(x["lap"])) + penalty = float(latest.get("penalty_s") or 0.0) + return penalty if penalty > 0 else None + + def _total_flight_time_s(self): + total = 0.0 + has_any = False + for item in self._lap_times: + t = item.get("time_s") + if t is None: + continue + try: + value = float(t) + except (TypeError, ValueError): + continue + if value > 0 and value == value: + total += value + has_any = True + return total if has_any else None + + def _refresh_mission_labels(self) -> None: + train = self._train_page() + if train is None: + return + times = [] + penalties = [] + for i in range(LAP_TABLE_ROWS): + lap_no = i + 1 + times.append(self._lap_time_by_number(lap_no)) + penalties.append(self._lap_penalty_delta_by_number(lap_no)) + train.set_laps( + times, + self._total_flight_time_s(), + lap_penalties=penalties, + total_penalty=self._total_penalty_s(), + ) + self._sync_session_mode_ui() + + def _bind_race_events(self, sm) -> None: + if not sm or self._race_events_bound: + return + sm.add_race_lap_handler(self._on_race_lap_event) + sm.add_race_end_handler(self._on_race_end_event) + self._race_events_bound = True + + def _bind_session_events(self, sm) -> None: + if not sm or self._session_events_bound: + return + sm.add_flight_loaded_handler(self._on_flight_loaded_event) + sm.add_mission_completed_handler(self._on_mission_completed_event) + sm.add_mp_server_started_handler(self._on_mp_server_started_event) + sm.add_mp_client_started_handler(self._on_mp_client_started_event) + sm.add_mp_session_ended_handler(self._on_mp_session_ended_event) + sm.add_custom_action_handler(self._on_custom_action_event) + sm.request_flight_loaded() + self._session_events_bound = True + + def _set_session_mode(self, mode: str) -> None: + if mode not in ("freeflight", "mission_sp", "mission_mp"): + return + self._session_mode = mode + train = self._train_page() + if train is not None: + train.set_session_mode(mode) + train.set_mission_title(self._mission_title or None) + + def _set_mission_title(self, title: Optional[str]) -> None: + text = (title or "").strip() + self._mission_title = text + train = self._train_page() + if train is not None: + train.set_mission_title(text or None) + if text: + self._enable_live_readouts() + elif not self._runtime.is_active(): + self._disable_live_readouts() + self._sync_start_monitor_button() + + def _clear_live_readouts(self) -> None: + train = self._train_page() + if train is None: + return + train.set_cht(None) + train.set_torque(None) + train.set_health(None) + + def _enable_live_readouts(self) -> None: + if self._live_readouts_enabled or self._closing or not self.aq: + return + self._live_readouts_enabled = True + self._poll_metrics() + self._refresh_runtime_timers() + + def _disable_live_readouts(self) -> None: + self._live_readouts_enabled = False + self._refresh_runtime_timers() + self._clear_live_readouts() + + def _sync_session_mode_ui(self) -> None: + train = self._train_page() + if train is not None: + train.set_session_mode(self._session_mode) + train.set_mission_title(self._mission_title or None) + + def _mode_from_flt_path(self, path: str) -> str: + mission_type = parse_flt_mission_type(path) + if mission_type.lower() == "freeflight": + return "freeflight" + return "mission_mp" if self._mp_active else "mission_sp" + + def _update_session_info_from_flt(self, path: str) -> None: + if not path: + return + mode = self._mode_from_flt_path(path) + if mode == "freeflight" and self._mp_active: + return + if mode == "freeflight" and self._runtime.is_active(): + mode = "mission_mp" if self._mp_active else "mission_sp" + title = parse_mission_title(path) + if title: + self._set_mission_title(title) + elif mode == "freeflight": + self._set_mission_title(None) + self._set_session_mode(mode) + + def _refresh_session_info(self) -> None: + if self.sm: + try: + self.sm.request_flight_loaded() + except Exception: + pass + path = (self._last_flt_path or "").strip() + if path: + self._update_session_info_from_flt(path) + elif self._runtime.is_active(): + self._set_session_mode("mission_mp" if self._mp_active else "mission_sp") + + def _on_flight_loaded_event(self, path: str) -> None: + if self._closing: + return + self._bridge.flight_loaded.emit(path) + + def _on_mission_completed_event(self, result_code: int) -> None: + if self._closing: + return + self._bridge.mission_completed.emit(int(result_code)) + + def _on_mp_server_started_event(self) -> None: + if self._closing: + return + self._bridge.mp_server_started.emit() + + def _on_mp_client_started_event(self) -> None: + if self._closing: + return + self._bridge.mp_client_started.emit() + + def _on_mp_session_ended_event(self) -> None: + if self._closing: + return + self._bridge.mp_session_ended.emit() + + def _on_custom_action_event(self, payload: str) -> None: + if self._closing: + return + self._bridge.custom_action.emit(payload or "") + + def _apply_flight_loaded(self, path: str) -> None: + if not path: + return + self._last_flt_path = path + self._update_session_info_from_flt(path) + + def _apply_mission_completed(self, _result_code: int) -> None: + if self._mp_active: + return + self._set_mission_title(None) + self._set_session_mode("freeflight") + + def _apply_mp_server_started(self) -> None: + self._mp_active = True + if self._session_mode == "mission_sp": + self._set_session_mode("mission_mp") + + def _apply_mp_client_started(self) -> None: + self._mp_active = True + if self._session_mode in ("mission_sp", "mission_mp"): + self._set_session_mode("mission_mp") + + def _apply_mp_session_ended(self) -> None: + self._mp_active = False + if self._session_mode == "mission_mp": + self._set_session_mode("mission_sp") + + def _apply_custom_action(self, _payload: str) -> None: + self._apply_mission_session_mode() + + def _apply_mission_session_mode(self) -> None: + """按是否多人会话切换 mission_mp / mission_sp。""" + self._set_session_mode("mission_mp" if self._mp_active else "mission_sp") + + def _on_race_lap_event(self, lap_index, lap_time_s=0.0, penalty_s=0.0): + if self._closing: + return + self._bridge.race_lap.emit(int(lap_index), float(lap_time_s), float(penalty_s)) + + def _on_race_end_event(self, racer_number, total_time_s=0.0, penalty_s=0.0): + if self._closing: + return + self._bridge.race_end.emit(int(racer_number), float(total_time_s), float(penalty_s)) + + def _apply_race_lap(self, lap_index, lap_time_s=0.0, penalty_s=0.0) -> None: + self._apply_mission_session_mode() + completed_lap = int(lap_index) + 1 + time_s = float(lap_time_s) + train = self._train_page() + if time_s <= 0 or time_s != time_s: + if train is not None: + train.set_status(f"第 {completed_lap} 圈完成,但未读到有效圈时", "warn") + self._lap_count = completed_lap + 1 + self._refresh_mission_labels() + return + self._lap_times = [x for x in self._lap_times if int(x["lap"]) != completed_lap] + prev_cumulative = max( + (float(x.get("penalty_s") or 0.0) for x in self._lap_times if int(x["lap"]) < completed_lap), + default=0.0, + ) + penalty_cumulative = float(penalty_s or 0.0) + penalty_delta = max(0.0, penalty_cumulative - prev_cumulative) + self._lap_times.append( + { + "lap": completed_lap, + "time_s": time_s, + "penalty_s": penalty_cumulative, + "penalty_delta_s": penalty_delta, + } + ) + self._lap_times.sort(key=lambda x: int(x["lap"])) + self._lap_count = completed_lap + 1 + self._race_finished = False + self._refresh_mission_labels() + four_done = self._four_laps_complete() + hint = f"第 {completed_lap} 圈 {format_lap_time(time_s)}" + if penalty_delta > 0: + hint += f"(本圈罚时 {format_lap_time(penalty_delta)})" + elif penalty_cumulative > 0: + hint += f"(累计罚时 {format_lap_time(penalty_cumulative)})" + if train is not None and self._runtime.is_recording(): + chart_t = self._current_chart_time_s() + if chart_t is not None: + train.add_lap_marker(chart_t, completed_lap) + self._push_aux_charts(train) + train.set_status(hint, "ok") + elif train is not None: + train.set_status(hint, "ok") + if four_done: + self._pause_all_monitoring_after_four_laps(hint) + + def _pause_all_monitoring_after_four_laps(self, last_hint: str = "") -> None: + """四圈成绩齐全:停止抓取/写入/刷图等全部监测(保留当前画面与待存轨迹)。""" + if not self._runtime.is_recording() and not self._runtime.is_waiting(): + return + self._stop_monitoring(save=False) + train = self._train_page() + if train is not None: + text = "四圈完成,监测已全部暂停" + if last_hint: + text = f"{last_hint}|{text}" + train.set_status(text, "ok") + self._refresh_monitor_button() + + def _apply_race_end(self, racer_number, total_time_s=0.0, penalty_s=0.0) -> None: + self._apply_mission_session_mode() + self._race_finished = True + self._refresh_mission_labels() + train = self._train_page() + if train is None: + return + flight = self._total_flight_time_s() + text = f"竞赛结束(选手 #{racer_number + 1})" + if flight is not None: + text += f" 总飞行 {format_lap_time(flight)}" + elif total_time_s and total_time_s == total_time_s and total_time_s > 0: + text += f" 接口总时 {format_lap_time(total_time_s)}" + if penalty_s and penalty_s == penalty_s and penalty_s > 0: + text += f" 罚时 {format_lap_time(penalty_s)}" + train.set_status(text, "ok") + + def _begin_race_tracking(self) -> None: + self._lap_count = 1 + self._lap_times = [] + self._race_finished = False + self._refresh_mission_labels() + train = self._train_page() + if train is not None: + self._push_aux_charts(train) + + def _four_laps_complete(self) -> bool: + have = {int(x["lap"]) for x in self._lap_times if float(x.get("time_s") or 0) > 0} + return all(i in have for i in range(1, LAP_COUNT + 1)) + + def _current_chart_time_s(self) -> Optional[float]: + """图表 X 轴:模拟器 ABSOLUTE TIME 相对监测起点的飞行时间(暂停时不推进)。""" + if not self.aq: + return None + elapsed, origin = chart_elapsed_time_s(self.aq, self._chart_time_origin) + if origin is not None: + self._chart_time_origin = origin + return elapsed + + def _push_aux_charts(self, train=None) -> None: + if train is None: + train = self._train_page() + if train is None: + return + train.set_track_points( + points_from_trajectory_samples(self._trajectory_recorder.samples_snapshot()) + ) + self._push_stick(train) + + def _push_stick(self, train=None) -> None: + if train is None: + train = self._train_page() + if train is None: + return + train.set_stick(poll_stick()) + + def _poll_stick(self) -> None: + if self._closing: + return + self._push_stick() + + def _start_recording(self) -> None: + self._refresh_session_info() + self._runtime.enter_recording() + self._chart_time_origin = None + self.history.clear() + train = self._train_page() + if train is not None: + train.clear_lap_markers() + train.reset_track_filters() + train.resume_live_follow() + train.update_chart(self.history) + title = self.aq.get("TITLE") if self.aq else None + if isinstance(title, bytes): + title = title.decode(errors="ignore") + self._trajectory_recorder.start(title) + self._begin_race_tracking() + if train is not None: + train.set_status("监测中...", "ok") + self._refresh_monitor_button() + self._push_stick(train) + + def _poll(self) -> None: + if self._closing or not self.aq: + return + if self._handle_detected_crash(): + return + paused = self._runtime.pause_detector.refresh(self.aq, self.sm) + self._apply_shadow_pause(paused) + if not self._runtime.is_active(): + if not self._suspend_sim_poll: + if self.sm and self.sm.quit: + self._handle_sim_lost() + return + self._refresh_runtime_timers() + return + interval = DATA_POLL_MS + if self._suspend_sim_poll: + self._poll_timer.start(interval) + return + if self.sm and self.sm.quit: + self._handle_sim_lost() + return + + if self._runtime.is_waiting(): + train = self._train_page() + if paused: + if train is not None: + train.set_status("等待解除暂停状态后开始监测...", "warn") + self._poll_timer.start(interval) + return + if train is not None: + train.set_status("请移动飞机以开始监测", "warn") + self._start_recording() + + if paused: + if self._trajectory_recorder.active and not self._trajectory_recorder.paused: + self._trajectory_recorder.set_paused(True) + train = self._train_page() + if train is not None: + train.set_status("模拟器已暂停,监测同步暂停", "warn") + self._poll_timer.start(interval) + return + + if self._trajectory_recorder.paused: + self._trajectory_recorder.set_paused(False) + train = self._train_page() + if train is not None: + train.set_status("监测中...", "ok") + + # 四圈已齐则不再抓取/写入/刷图(兜底,正常应在过圈时已停) + if self._four_laps_complete(): + self._pause_all_monitoring_after_four_laps() + return + + flight_t = self._current_chart_time_s() + if flight_t is None: + self._poll_timer.start(interval) + return + _, torque, health = self._read_live_readouts() + snap = read_aq_snapshot(self.aq, torque=torque, health_ratio=health) + series = extract(snap) + self.history.append(flight_t, **series) + if self._runtime.is_recording(): + active_lap = max(1, min(LAP_COUNT, int(self._lap_count) if self._lap_count else 1)) + try: + self._trajectory_recorder.sample_from_aq( + self.aq, + torque=torque, + health_ratio=health, + lap=active_lap, + timestamp_s=flight_t, + snapshot=snap, + **stick_record_fields(poll_stick()), + ) + except OSError: + self._trajectory_recorder.stop() + train = self._train_page() + if train is not None: + train.update_chart(self.history) + self._push_aux_charts(train) + self._refresh_monitor_button() + if self._runtime.is_active(): + self._refresh_runtime_timers() + + def _read_live_readouts(self): + cht_c = self.aq.get(CHT_VAR) + cht_f = c_to_f(cht_c) if cht_c is not None else None + + torque_raw = self.aq.get(TORQUE_VAR) + torque = float(torque_raw) if torque_raw is not None else None + + health = None + damage = self.aq.get(DAMAGE_VAR) + if damage is not None: + health = 1.0 - float(damage) / 100.0 + return cht_f, torque, health + + def _apply_live_readouts(self, cht_f, torque, health) -> None: + train = self._train_page() + if train is None: + return + train.set_cht(cht_f) + train.set_torque(torque) + if health is not None: + train.set_health(health * 100.0) + else: + train.set_health(None) + + def _poll_metrics(self) -> None: + if self._closing or not self.aq: + return + self._refresh_metrics() + + def _stop_shadow_soft(self, ctrl=None, stop_recorder: bool = False) -> None: + if ctrl is None: + ctrl = self._shadow_controller + self._shadow_controller = None + if ctrl is not None: + try: + ctrl.signal_stop() + except Exception: + pass + + def _bg_join(): + try: + ctrl.stop_replay(join_timeout=0.3) + except Exception: + pass + + threading.Thread(target=_bg_join, daemon=True).start() + if stop_recorder: + try: + self._trajectory_recorder.stop_record_thread() + except Exception: + pass + self._refresh_runtime_timers() + + def _handle_detected_crash(self) -> bool: + """检测到坠毁时停止监测/跟飞;返回是否已处理。""" + if self._runtime.crash_latched or not aircraft_has_crashed(self.aq): + return False + session_active = self._runtime.is_active() + ctrl = self._shadow_controller + had_shadow = ctrl is not None and ctrl.object_id is not None + if not session_active and not had_shadow: + return False + self._runtime.crash_latched = True + if session_active: + self._stop_monitoring(save=False) + elif had_shadow: + self._shadow_controller = None + self._stop_shadow_soft(ctrl, stop_recorder=True) + train = self._train_page() + if train is not None: + if had_shadow and session_active: + train.set_status("检测到坠毁,已停止跟飞及监测", "warn") + elif had_shadow: + train.set_status("检测到坠毁,已停止跟飞", "warn") + else: + train.set_status("检测到坠毁,已停止监测", "warn") + self._refresh_runtime_timers() + return True + + def _init_sim_events(self) -> None: + if not self.sm: + return + self.pause_on_event = Event(b"PAUSE_ON", self.sm) + self.situation_reset_event = Event(b"SITUATION_RESET", self.sm) + if not self.sim_rate_incr_event: + self.sim_rate_incr_event = Event(b"SIM_RATE_INCR", self.sm) + if not self.sim_rate_decr_event: + self.sim_rate_decr_event = Event(b"SIM_RATE_DECR", self.sm) + + def _request_sim_pause(self) -> None: + self._init_sim_events() + if self.pause_on_event: + try: + self.pause_on_event() + except Exception: + pass + + def _read_sim_rate(self): + return read_simulation_rate(self.aq) + + def _sync_rate_button(self, rate=None) -> None: + train = self._train_page() + if train is None: + return + if rate is None: + rate = self._read_sim_rate() + if rate is not None and rate >= TARGET_SIM_RATE - 0.05: + train.set_rate_button("关闭倍速") + else: + train.set_rate_button("开启倍速") + + def _reset_task_and_request_pause(self) -> None: + if self.situation_reset_event: + self.situation_reset_event() + self._request_sim_pause() + + def _apply_cold_cabin(self) -> bool: + target_c = f_to_c(TARGET_CHT_F) + return bool(self.aq.set(CHT_VAR, target_c)) + + def _on_start_monitor(self) -> None: + if not self.aq: + train = self._train_page() + if train is not None: + train.set_status("未连接模拟器", "error") + return + if not (self._mission_title or "").strip(): + train = self._train_page() + if train is not None: + train.set_status("请先选择飞行任务", "warn") + return + self._refresh_session_info() + settings = show_start_monitoring( + self.shell, + session_mode=self._session_mode, + cold_cabin_enabled=self._monitor_cold_cabin_enabled, + shadow_follow_enabled=self._monitor_shadow_follow_enabled, + lead_s=self._shadow_lead_s, + smoke_enabled=self._shadow_smoke_enabled, + trajectory_path=self._shadow_trajectory_path or None, + ) + if settings is None: + return + reload_stick() + self._monitor_cold_cabin_enabled = bool(settings.get("cold_cabin")) + self._monitor_shadow_follow_enabled = bool(settings.get("shadow_follow")) + self._shadow_lead_s = float(settings.get("lead", self._shadow_lead_s)) + self._shadow_smoke_enabled = bool(settings.get("smoke", self._shadow_smoke_enabled)) + if settings.get("trajectory_path"): + self._shadow_trajectory_path = str(settings["trajectory_path"]) + task = settings.get("task", "current") + if settings.get("shadow_follow"): + self._begin_shadow_follow_from_monitor(task, settings) + return + if task == "reset": + if self._monitor_cold_cabin_enabled: + self._cold_cabin_reset_task() + else: + self._start_monitor_reset_task() + elif self._monitor_cold_cabin_enabled: + self._cold_cabin_current_task() + else: + self._request_sim_pause() + train = self._train_page() + if train is not None: + train.set_status("准备开始监测...", "muted") + self._start_monitor_waiting() + + def _begin_shadow_follow_from_monitor(self, task: str, settings: dict) -> None: + if not self.aq or not self.sm or self._shadow_follow_busy: + train = self._train_page() + if train is not None: + train.set_status("未连接模拟器", "error") + return + path = (settings.get("trajectory_path") or "").strip() + if not path: + train = self._train_page() + if train is not None: + train.set_status("请选择跟飞轨迹文件", "warn") + return + + self._stop_shadow_soft() + if self._runtime.is_active(): + self._stop_monitoring() + reset_applied = False + if task == "reset": + try: + self._reset_task_and_request_pause() + reset_applied = True + except Exception as exc: + train = self._train_page() + if train is not None: + train.set_status(f"任务重置失败:{exc}", "error") + return + else: + self._request_sim_pause() + + lead = float(settings.get("lead", self._shadow_lead_s)) + enable_smoke = bool(settings.get("smoke", self._shadow_smoke_enabled)) + enable_cold_cabin = bool(settings.get("cold_cabin")) + self._shadow_follow_busy = True + train = self._train_page() + if train is not None: + train.set_status("跟飞准备中...", "muted") + threading.Thread( + target=self._shadow_follow_worker, + args=(path, lead, enable_smoke, enable_cold_cabin, task == "current", reset_applied), + daemon=True, + ).start() + + def _start_monitor_reset_task(self) -> None: + self._reset_task_and_request_pause() + train = self._train_page() + if train is not None: + train.set_status("任务已重置,准备开始监测...", "muted") + self._start_monitor_waiting() + + def _cold_cabin_current_task(self) -> None: + QTimer.singleShot(COLD_CABIN_DELAY_MS, self._finish_cold_cabin_current) + + def _finish_cold_cabin_current(self) -> None: + if self._closing or not self.aq: + return + train = self._train_page() + if self._apply_cold_cabin(): + if train is not None: + train.set_status("冷舱启动成功", "ok") + self._start_monitor_waiting() + elif train is not None: + train.set_status("冷舱启动失败", "error") + + def _cold_cabin_reset_task(self) -> None: + self._reset_task_and_request_pause() + QTimer.singleShot(COLD_CABIN_DELAY_MS, self._finish_cold_cabin_reset) + + def _finish_cold_cabin_reset(self) -> None: + if self._closing or not self.aq: + return + ok = self._apply_cold_cabin() + self._request_sim_pause() + train = self._train_page() + if ok: + if train is not None: + train.set_status("冷舱启动成功", "ok") + self._start_monitor_waiting() + elif train is not None: + train.set_status("冷舱启动失败", "error") + + def _on_sim_rate(self) -> None: + if not self.aq or not self.sm: + train = self._train_page() + if train is not None: + train.set_rate_status("未连接模拟器", "error") + return + self._init_sim_events() + current = self._read_sim_rate() + if current is None: + current = 1.0 + if current >= TARGET_SIM_RATE - 0.05: + self._sim_rate_target = 1.0 + action = "关闭" + else: + self._sim_rate_target = TARGET_SIM_RATE + action = "开启" + self._sim_rate_steps_left = SIM_RATE_MAX_STEPS + self._sim_rate_timer.stop() + train = self._train_page() + if train is not None: + train.set_rate_status(f"倍速{action}中…", "muted") + self._sim_rate_step() + + def _sim_rate_step(self) -> None: + if self._closing or not self.aq: + return + current = self._read_sim_rate() + train = self._train_page() + if current is None: + if train is not None: + train.set_rate_status("倍速调节失败:无法读取 SIMULATION_RATE", "error") + return + target = self._sim_rate_target + if target is None: + self._sync_rate_button(current) + return + if abs(current - target) <= 0.05: + self._sync_rate_button(current) + if train is not None: + if target >= TARGET_SIM_RATE - 0.05: + train.set_rate_status(f"开启倍速:{current:.1f}x", "ok") + else: + train.set_rate_status(f"关闭倍速:{current:.1f}x", "ok") + return + if self._sim_rate_steps_left <= 0: + self._sync_rate_button(current) + if train is not None: + train.set_rate_status(f"倍速调节未到位,当前 {current:.1f}x", "warn") + return + self._sim_rate_steps_left -= 1 + if current < target - 0.05: + self.sim_rate_incr_event() + else: + self.sim_rate_decr_event() + self._sim_rate_timer.start(SIM_RATE_STEP_MS) + + def _shadow_follow_worker( + self, + path, + lead_s=SHADOW_LEAD_DEFAULT_S, + enable_smoke=True, + enable_cold_cabin=False, + use_user_reference=False, + reset_applied=False, + ) -> None: + err_text = None + try: + old = self._shadow_controller + self._shadow_controller = None + if old is not None: + try: + old.signal_stop() + old.stop_replay(join_timeout=0.3) + except Exception: + pass + if not self.sm or getattr(self.sm, "quit", 0) or not getattr(self.sm, "ok", False): + raise RuntimeError("模拟器连接异常,请重新连接后再试") + if enable_cold_cabin: + time.sleep(COLD_CABIN_DELAY_MS / 1000.0) + if not self._apply_cold_cabin(): + raise RuntimeError("冷舱启动失败") + self._request_sim_pause() + result = launch_shadow_follow( + self.sm, + self.aq, + path, + situation_reset_event=None, + reset_already_applied=reset_applied, + replace_mission_ai=False, + lead_s=lead_s, + enable_smoke=enable_smoke, + use_user_reference=use_user_reference, + ) + self._shadow_controller = result["controller"] + duration = float(result.get("duration_s") or 0.0) + lead = float(result.get("lead_s") or 0.0) + msg = ( + f"影子机已启动:{result['aircraft_title']}," + f"{result['sample_count']} 点 / {duration:.1f}s" + f"(Shadow Plane 偏移 {lead:.1f}s,{int(result.get('replay_hz') or 60)}Hz" + f"{',拉烟' if result.get('smoke') else ''}" + f"{',冷舱' if enable_cold_cabin else ''})" + ) + self._request_sim_pause() + self._bridge.shadow_follow_done.emit(True, msg) + except Exception as exc: + err_text = str(exc) or exc.__class__.__name__ + self._bridge.shadow_follow_done.emit(False, err_text) + + def _on_shadow_follow_done(self, ok: bool, message: str) -> None: + self._shadow_follow_busy = False + if self._closing: + return + train = self._train_page() + if train is not None: + if ok: + self._apply_shadow_pause(self._runtime.pause_detector.refresh(self.aq, self.sm)) + self._start_monitor_waiting() + else: + train.set_status(f"跟飞失败:{message}", "error") + + def _on_close(self) -> None: + if self._closing: + return + self._closing = True + self._check_closing = True + self._poll_timer.stop() + self._metrics_timer.stop() + self._stick_timer.stop() + self._sim_rate_timer.stop() + self._login_poll_timer.stop() + self._runtime.reset() + try: + self._trajectory_recorder.stop() + except Exception: + pass + self._trajectory_recorder.stop_and_save( + total_s=self._total_flight_time_s(), + laps_s=laps_from_session(self._lap_times), + ) + if self._shadow_controller is not None: + try: + self._shadow_controller.signal_stop() + except Exception: + pass + self._shadow_controller = None + train = self._train_page() + if train is not None: + train.metrics.stop_health_flash() + conn_sm = None + if self.sm: + conn_sm = self.sm + elif self._sim_connection: + conn_sm = self._sim_connection[0] + safe_disconnect_simulator(conn_sm) + self.sm = None + self.aq = None + self._sim_connection = None + + def _quit_app(self) -> None: + self._on_close() + app = QApplication.instance() + if app is not None: + app.quit() diff --git a/app_frame.py b/app_frame.py new file mode 100644 index 00000000..3c45cdf2 --- /dev/null +++ b/app_frame.py @@ -0,0 +1,2597 @@ +# -*- coding: utf-8 -*- +"""可复用 UI 框架:主题 / Capability / AppShell / WidgetKit。 + +功能页请放在 ui/ 下组合本模块导出的积木,勿在此文件写具体业务布局。 +图表组件见 charts 包。 +""" + +from __future__ import annotations + +import math +import os +import sys +import time +from pathlib import Path +from typing import Callable, Iterable, Optional + + +def _configure_qt_plugins() -> None: + """conda / pip 安装的 Qt 常找不到 platforms 插件,需显式设置路径。""" + try: + import PySide2 + except ImportError: + return + root = Path(PySide2.__file__).resolve().parent + plugins = root / "plugins" + platforms = plugins / "platforms" + if not platforms.is_dir(): + return + os.environ["QT_PLUGIN_PATH"] = str(plugins) + os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = str(platforms) + os.environ["QT_QPA_PLATFORM"] = "windows" + + +_configure_qt_plugins() + +from PySide2.QtCore import Qt, QTimer, Signal +from PySide2.QtGui import ( + QColor, + QFont, + QFontDatabase, + QIcon, + QMouseEvent, + QPainter, + QPen, + QPixmap, +) +from PySide2.QtWidgets import ( + QApplication, + QDialog, + QFileDialog, + QFrame, + QGridLayout, + QHBoxLayout, + QLabel, + QLineEdit, + QMainWindow, + QPushButton, + QSizePolicy, + QStackedWidget, + QVBoxLayout, + QWidget, +) + +from models.chart import ( + ChartHistoryBuffer, + health_band, + TRAINING_CHART, + TORQUE_YMAX, + TORQUE_YMIN, + HEALTH_YMAX, + HEALTH_YMIN, +) + +# --------------------------------------------------------------------------- +# 展示常量(与现主程序语义对齐) +# --------------------------------------------------------------------------- + +WINDOW_ALPHA = 0.85 +LAP_TABLE_ROWS = 4 +C_CHT = 120.0 +D_TORQUE = 500.0 +E_TORQUE = 550.0 +X_AXIS_MAJOR_SEC = TRAINING_CHART.x_axis_major_sec +METRIC_VALUE_DECIMALS = 1 +METRIC_VALUE_INDENT = " " + +# 预置 Capability 键(可扩展) +CAP_METRICS = "metrics" +CAP_LAPS = "laps" +CAP_CHART = "chart" +CAP_CHART_SAVE = "chart_save" +CAP_COLD_CABIN = "cold_cabin" +CAP_SIM_RATE = "sim_rate" +CAP_SHADOW = "shadow" + +HEALTH_FLASH_MS = 200 + +DEFAULT_CAPABILITIES = frozenset( + { + CAP_METRICS, + CAP_LAPS, + CAP_CHART, + CAP_CHART_SAVE, + CAP_COLD_CABIN, + CAP_SIM_RATE, + CAP_SHADOW, + } +) + +# 各功能页设计尺寸 (design_w, design_h) +FEATURE_WINDOW_SPECS: dict[str, tuple[int, int, int, int]] = { + "flow.sim_check": (460, 400, 460, 400), + "flow.auth": (360, 400, 360, 400), + "training_assist": (680, 820, 680, 820), +} + + +def format_lap_time(seconds): + if seconds is None: + return "--" + try: + total = float(seconds) + except (TypeError, ValueError): + return "--" + if total < 0 or total != total: + return "--" + minutes = int(total // 60) + secs = total - minutes * 60 + if minutes > 0: + return f"{minutes}:{secs:04.1f}" + return f"{secs:.1f}s" + + +def format_lap_time_with_penalty(time_s, penalty_s=None) -> str: + """净圈时;本圈罚时(若有)在括号内。""" + text = format_lap_time(time_s) + if penalty_s is None: + return text + try: + penalty = float(penalty_s) + except (TypeError, ValueError): + return text + if penalty <= 0 or penalty != penalty: + return text + return f"{text} ({format_lap_time(penalty)})" + + +def format_total_time_display(net_s, cumulative_penalty_s=None) -> str: + """净总时(累计罚时)含罚总时。""" + net_text = format_lap_time(net_s) + if net_text == "--": + return "--" + if cumulative_penalty_s is None: + return net_text + try: + penalty = float(cumulative_penalty_s) + except (TypeError, ValueError): + return net_text + if penalty <= 0 or penalty != penalty: + return net_text + try: + net = float(net_s) + except (TypeError, ValueError): + return net_text + if net < 0 or net != net: + return net_text + gross = net + penalty + return f"{net_text} ({format_lap_time(penalty)}) {format_lap_time(gross)}" + + +def format_metric_value(value, decimals=METRIC_VALUE_DECIMALS): + if value is None: + text = "--" + else: + try: + number = float(value) + except (TypeError, ValueError): + text = "--" + else: + if number != number: + text = "--" + else: + text = f"{number:.{decimals}f}" + return METRIC_VALUE_INDENT + text + + +def cht_color(cht_f): + if cht_f is None: + return UITheme.FG + return UITheme.FLASH_BLUE if float(cht_f) < C_CHT else UITheme.ERROR + + +def torque_color(torque): + if torque is None: + return UITheme.FG + tq = float(torque) + if tq > E_TORQUE: + return UITheme.FLASH_BLUE + if D_TORQUE <= tq <= E_TORQUE: + return UITheme.WARNING + return UITheme.ERROR + + +def health_color(health_pct): + band = health_band(health_pct) + if band is None: + return UITheme.FG + if band == "flash": + return UITheme.FLASH_BLUE + if band == "green": + return UITheme.SUCCESS + if band == "orange": + return UITheme.WARNING + return UITheme.ERROR + + +def status_kind_color(kind: str) -> str: + return { + "ok": UITheme.SUCCESS, + "warn": UITheme.WARNING, + "error": UITheme.ERROR, + "muted": UITheme.FG_MUTED, + }.get(kind, UITheme.FG_MUTED) + + +# --------------------------------------------------------------------------- +# UITheme +# --------------------------------------------------------------------------- + + +class UITheme: + BLUE_DEEP = "#0B1420" + BLUE_MID = "#152A40" + BLUE_LIGHT = "#2E4F73" + BG = BLUE_DEEP + SURFACE = BLUE_MID + # SURFACE(#152A40) 明度×0.8 预计算,供图表井区/对比控件等嵌套底 + CHART_WELL = "#112233" + METRIC_SURFACE = "#101E2E" + METRIC_BORDER = "#243D58" + FG = "#C5D6E8" + FG_MUTED = "#7A9BB8" + FG_TITLE = "#8AABC8" + FG_ON_ACCENT = "#E8F1F8" + ACCENT = BLUE_LIGHT + ACCENT_HOVER = "#3A638F" + BORDER = BLUE_LIGHT + TOGGLE_TRACK_OFF = "#122436" + TOGGLE_TRACK_ON = BLUE_LIGHT + TOGGLE_TRACK_BORDER = "#3A5F82" + TOGGLE_KNOB_OFF = "#8FAEC8" + TOGGLE_KNOB_ON = FG_ON_ACCENT + COLLAPSIBLE_TITLE_BG = BLUE_DEEP + COLLAPSIBLE_BODY_BG = BLUE_MID + SUCCESS = "#2ecc71" + WARNING = "#f39c12" + ERROR = "#e74c3c" + FLASH_BLUE = "#1e90ff" + CHART_TORQUE = "#4a90d9" + CHART_HEALTH = "#e67e22" + CHART_AIRSPEED = "#2ecc71" + CHART_GROUNDSPEED = "#1abc9c" + CHART_PITCH = "#f1c40f" + CHART_BANK = "#e74c3c" + CHART_REFERENCE = "#9b59b6" + # LAP1–LAP4 固定色(柱状堆叠 / 图例) + CHART_LAP_COLORS = ("#4a90d9", "#e67e22", "#2ecc71", "#e74c3c") + CHART_HEALTH_RED_FILL = "#e74c3c" + CHART_HEALTH_ORANGE_FILL = "#f39c12" + CHART_HEALTH_BLUE_FILL = "#1e90ff" + CHART_HEALTH_SPAN_ALPHA = 0.18 + SIGNAL_IDLE = "#4A6278" + SIGNAL_RUN = "#5E7F9A" + SIGNAL_OK = "#5F8F75" + SIGNAL_FAIL = "#946868" + + FONT_FAMILY = "Noto Sans SC" + FONT_FAMILY_TECH = "Orbitron" + FONT_XS = 8 + FONT_SM = 9 + FONT_MD = 10 + FONT_LG = 11 + FONT_METRIC_TITLE = 12 + FONT_TITLE = 20 + FONT_CHT = 36 + FONT_LAP_TOTAL = 20 + FONT_UNIT = 14 + FONT_HINT = 8 + + _fonts_loaded = False + _scale = 1.0 + + @classmethod + def resource_root(cls) -> Path: + if getattr(sys, "frozen", False): + return Path(sys._MEIPASS) + return Path(__file__).resolve().parent + + @classmethod + def fonts_dir(cls) -> Path: + return cls.resource_root() / "resources" / "fonts" + + @classmethod + def icons_dir(cls) -> Path: + return cls.resource_root() / "resources" / "icons" + + @classmethod + def combo_arrow_url(cls) -> str: + """QSS url() 用下拉箭头资源路径(正斜杠)。""" + return (cls.icons_dir() / "combo_down_arrow.png").as_posix() + + @classmethod + def load_fonts(cls) -> None: + if cls._fonts_loaded: + return + font_dir = cls.fonts_dir() + for name in ( + "NotoSansSC-Regular.ttf", + "NotoSansSC-Bold.ttf", + "Orbitron-Medium.ttf", + "Orbitron-Bold.ttf", + ): + path = font_dir / name + if path.is_file(): + QFontDatabase.addApplicationFont(str(path)) + families = QFontDatabase().families() + if "Noto Sans SC" in families: + cls.FONT_FAMILY = "Noto Sans SC" + if "Orbitron" in families: + cls.FONT_FAMILY_TECH = "Orbitron" + cls._fonts_loaded = True + + @classmethod + def apply_scale(cls, window_w, window_h, design_w, design_h) -> None: + if design_w <= 0 or design_h <= 0: + cls._scale = 1.0 + return + cls._scale = min(window_w / float(design_w), window_h / float(design_h)) + + @classmethod + def scaled(cls, value, minimum=0) -> int: + scaled = int(round(value * cls._scale)) + return max(minimum, scaled) if minimum else scaled + + @classmethod + def font(cls, size: int, bold: bool = False) -> QFont: + cls.load_fonts() + f = QFont(cls.FONT_FAMILY, size) + f.setBold(bold) + return f + + @classmethod + def font_tech(cls, size: int, bold: bool = False) -> QFont: + cls.load_fonts() + f = QFont(cls.FONT_FAMILY_TECH, size) + f.setBold(bold) + return f + + @classmethod + def qss(cls) -> str: + cls.load_fonts() + return f""" + QWidget {{ + background-color: {cls.BG}; + color: {cls.FG}; + font-family: "{cls.FONT_FAMILY}"; + font-size: {cls.FONT_MD}pt; + }} + QLabel {{ + background: transparent; + }} + QMainWindow, QDialog {{ + background-color: {cls.BG}; + }} + QFrame#Card, QFrame#Surface {{ + background-color: {cls.SURFACE}; + border: 1px solid {cls.BORDER}; + border-radius: 4px; + }} + QFrame#MetricCard {{ + background-color: {cls.METRIC_SURFACE}; + border: 2px solid {cls.METRIC_BORDER}; + border-radius: 8px; + }} + QFrame#MetricCard QLabel#MetricCardTitle {{ + color: {cls.FG_TITLE}; + font-size: {cls.FONT_METRIC_TITLE}pt; + font-weight: bold; + background: transparent; + }} + QWidget#ActionBar, QWidget#ActionBarRow, QWidget#MonitorSlot {{ + background: transparent; + }} + QFrame#CollapsibleBox {{ + background: transparent; + border: 1px solid {cls.BORDER}; + border-radius: 4px; + }} + QFrame#CollapsibleBox[collapsed="true"] {{ + border-radius: 4px; + }} + QFrame#CollapsibleTitle {{ + background-color: {cls.COLLAPSIBLE_BODY_BG}; + border: none; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + }} + QFrame#CollapsibleBox[collapsed="true"] QFrame#CollapsibleTitle {{ + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + }} + QFrame#CollapsibleBody {{ + background-color: {cls.COLLAPSIBLE_TITLE_BG}; + border: none; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + }} + QPushButton#CollapsibleHeader {{ + background: transparent; + color: {cls.FG}; + border: none; + padding: 0 8px; + text-align: left; + font-size: {cls.FONT_MD}pt; + }} + QPushButton#CollapsibleHeader:hover:enabled {{ + color: {cls.ACCENT_HOVER}; + }} + QPushButton#CollapsibleHeader:disabled {{ + color: {cls.FG_MUTED}; + }} + QLabel#DialogTitle {{ + color: {cls.FG_TITLE}; + font-size: {cls.FONT_LG}pt; + background: transparent; + }} + QFrame#SurfaceFlat {{ + background-color: {cls.SURFACE}; + border: none; + }} + QFrame#CheckListCard {{ + background-color: {cls.SURFACE}; + border: 1px solid {cls.BORDER}; + border-radius: 8px; + }} + QLabel#Title {{ + color: {cls.FG_TITLE}; + font-size: {cls.FONT_TITLE}pt; + font-weight: bold; + background: transparent; + }} + QLabel#Subtitle {{ + color: {cls.FG_MUTED}; + font-size: {cls.FONT_LG}pt; + background: transparent; + }} + QLabel#Muted, QLabel#ChartHint {{ + color: {cls.FG_MUTED}; + font-size: {cls.FONT_SM}pt; + background: transparent; + }} + QLabel#MetricTitle {{ + color: {cls.FG_TITLE}; + font-size: {cls.FONT_MD}pt; + background: transparent; + }} + QLabel#MetricValue, QLabel#CHTValue, QLabel#InfoValue {{ + color: {cls.FG}; + font-family: "{cls.FONT_FAMILY_TECH}"; + font-size: {cls.FONT_CHT}pt; + background: transparent; + }} + QLabel#InfoValue {{ + font-size: {cls.FONT_LG}pt; + }} + QLabel#SessionModeLabel {{ + font-family: "{cls.FONT_FAMILY_TECH}"; + font-size: {cls.FONT_LG}pt; + background: transparent; + }} + QLabel#SessionMissionLabel {{ + font-family: "{cls.FONT_FAMILY}"; + font-size: {cls.FONT_MD}pt; + background: transparent; + color: {cls.FG}; + }} + QLabel#LapTotalValue {{ + color: {cls.FG}; + font-family: "{cls.FONT_FAMILY_TECH}"; + font-size: {cls.FONT_LAP_TOTAL}pt; + font-weight: bold; + background: transparent; + }} + QLabel#MetricUnit {{ + color: {cls.FG_MUTED}; + font-size: {cls.FONT_UNIT}pt; + background: transparent; + }} + QWidget#StatusBar {{ + background: transparent; + }} + QLabel#StatusEvent, QLabel#StatusRate {{ + font-size: {cls.FONT_SM}pt; + background: transparent; + }} + QLabel#CheckWait {{ + color: {cls.FG_MUTED}; + font-size: {cls.FONT_LG}pt; + background: transparent; + }} + QLabel#CheckRun {{ color: {cls.SIGNAL_RUN}; font-size: {cls.FONT_LG}pt; background: transparent; }} + QLabel#CheckOk {{ color: {cls.SIGNAL_OK}; font-size: {cls.FONT_LG}pt; background: transparent; }} + QLabel#CheckFail {{ color: {cls.SIGNAL_FAIL}; font-size: {cls.FONT_LG}pt; background: transparent; }} + QLabel#CheckItemDetail {{ + color: {cls.FG_MUTED}; + font-size: {cls.FONT_MD}pt; + background: transparent; + }} + QLineEdit {{ + background-color: {cls.BG}; + color: {cls.FG}; + border: 1px solid {cls.BORDER}; + border-radius: 3px; + padding: 6px 8px; + selection-background-color: {cls.ACCENT}; + }} + QLineEdit:focus {{ + border: 1px solid {cls.ACCENT_HOVER}; + }} + QPushButton#FlatButton, QPushButton#ChartTool, QPushButton#IconSm {{ + background-color: {cls.ACCENT}; + color: {cls.FG_ON_ACCENT}; + border: none; + border-radius: 3px; + padding: 8px 18px; + font-size: {cls.FONT_LG}pt; + }} + QPushButton#FlatButton:hover, QPushButton#ChartTool:hover, QPushButton#IconSm:hover {{ + background-color: {cls.ACCENT_HOVER}; + }} + QPushButton#FlatButton:checked {{ + background-color: {cls.ACCENT}; + color: {cls.FG_ON_ACCENT}; + }} + QPushButton#FlatButton:checked:hover {{ + background-color: {cls.ACCENT_HOVER}; + }} + QPushButton#FlatButton:disabled, QPushButton#ChartTool:disabled, QPushButton#IconSm:disabled {{ + background-color: {cls.BLUE_MID}; + color: {cls.FG_MUTED}; + }} + /* 双状态按钮:两态均保持高对比,避免在 CHART_WELL 上隐没 */ + QPushButton#DualStateButton {{ + background-color: {cls.ACCENT}; + color: {cls.FG_ON_ACCENT}; + border: none; + border-radius: 3px; + padding: 8px 18px; + font-size: {cls.FONT_LG}pt; + }} + QPushButton#DualStateButton:hover {{ + background-color: {cls.ACCENT_HOVER}; + }} + QPushButton#DualStateButton:checked {{ + background-color: {cls.ACCENT}; + color: {cls.FG_ON_ACCENT}; + }} + QPushButton#DualStateButton:checked:hover {{ + background-color: {cls.ACCENT_HOVER}; + }} + QPushButton#DualStateButton:!checked {{ + background-color: {cls.FLASH_BLUE}; + color: {cls.FG_ON_ACCENT}; + }} + QPushButton#DualStateButton:!checked:hover {{ + background-color: {cls.ACCENT_HOVER}; + }} + QPushButton#IconSm, QPushButton#IconSmAccent {{ + padding: 4px 10px; + font-size: {cls.FONT_MD}pt; + min-width: 28px; + }} + QPushButton#IconSmAccent {{ + background-color: {cls.FLASH_BLUE}; + color: {cls.FG_ON_ACCENT}; + border: none; + border-radius: 3px; + }} + QPushButton#ChartTool {{ + padding: 4px 12px; + font-size: {cls.FONT_SM}pt; + }} + QPushButton#MonitorButton {{ + border: none; + border-radius: 3px; + padding: 8px 18px; + font-size: {cls.FONT_LG}pt; + color: {cls.FG_ON_ACCENT}; + }} + QPushButton#MonitorButton[monitorKind="stop"] {{ + background-color: {cls.WARNING}; + }} + QPushButton#MonitorButton[monitorKind="stop"]:hover {{ + background-color: #e67e22; + }} + QPushButton#MonitorButton[monitorKind="save"] {{ + background-color: {cls.ERROR}; + }} + QPushButton#MonitorButton[monitorKind="save"]:hover {{ + background-color: #c0392b; + }} + QChartView {{ + background-color: {cls.SURFACE}; + border: none; + }} + /* 全局下拉:显式箭头,避免自定义底色吞掉原生指示 */ + QComboBox {{ + background-color: {cls.SURFACE}; + color: {cls.FG}; + border: 1px solid {cls.BORDER}; + border-radius: 3px; + padding: 2px 22px 2px 6px; + min-height: 20px; + }} + QComboBox::drop-down {{ + subcontrol-origin: padding; + subcontrol-position: top right; + width: 18px; + border: none; + background: transparent; + }} + QComboBox::down-arrow {{ + image: url("{cls.combo_arrow_url()}"); + width: 10px; + height: 6px; + }} + QComboBox QAbstractItemView {{ + background-color: {cls.SURFACE}; + color: {cls.FG}; + border: 1px solid {cls.BORDER}; + selection-background-color: {cls.ACCENT}; + }} + /* 图表嵌套控件:相对 SURFACE 的预计算井区色 + 边框 */ + QComboBox#StickDeviceCombo, + QWidget#ChartToolbar QComboBox {{ + background-color: {cls.CHART_WELL}; + color: {cls.FG}; + border: 1px solid {cls.BORDER}; + border-radius: 3px; + padding: 2px 22px 2px 6px; + min-height: 20px; + }} + QWidget#CompareStylePanel QComboBox {{ + background-color: {cls.SURFACE}; + color: {cls.FG}; + border: 1px solid {cls.BORDER}; + border-radius: 3px; + padding: 2px 22px 2px 6px; + min-height: 20px; + }} + QComboBox#StickDeviceCombo QAbstractItemView, + QWidget#ChartToolbar QComboBox QAbstractItemView {{ + background-color: {cls.CHART_WELL}; + color: {cls.FG}; + border: 1px solid {cls.BORDER}; + selection-background-color: {cls.ACCENT}; + }} + QWidget#CompareStylePanel QComboBox QAbstractItemView {{ + background-color: {cls.SURFACE}; + color: {cls.FG}; + border: 1px solid {cls.BORDER}; + selection-background-color: {cls.ACCENT}; + }} + /* 全局复选框:选中为亮橙色 */ + QCheckBox::indicator {{ + width: 14px; + height: 14px; + background-color: {cls.CHART_WELL}; + border: 1px solid {cls.BORDER}; + border-radius: 2px; + }} + QCheckBox::indicator:checked {{ + background-color: {cls.WARNING}; + border: 1px solid {cls.WARNING}; + }} + QWidget#ChartToolbar {{ + background-color: {cls.SURFACE}; + border: none; + }} + QWidget#ChartToolbarCell {{ + background-color: {cls.SURFACE}; + border: none; + }} + QWidget#ChartToolbar QLabel#ChartHint {{ + background-color: {cls.SURFACE}; + color: {cls.FG_MUTED}; + }} + QWidget#ChartToolbar QCheckBox {{ + background-color: {cls.SURFACE}; + color: {cls.FG}; + border: none; + }} + QWidget#StickInputPane {{ + background-color: {cls.SURFACE}; + border: none; + }} + QWidget#CompareStylePanel {{ + background-color: {cls.CHART_WELL}; + border: 1px solid {cls.BORDER}; + border-radius: 3px; + padding: 4px 6px; + }} + """ + + +# --------------------------------------------------------------------------- +# 窗口几何 +# --------------------------------------------------------------------------- + + +class WindowGeom: + @classmethod + def screen_size(cls) -> tuple[int, int]: + screen = QApplication.primaryScreen() + if screen is None: + return 1920, 1080 + geo = screen.availableGeometry() + return geo.width(), geo.height() + + @classmethod + def main_window_size(cls) -> tuple[int, int]: + sw, sh = cls.screen_size() + return sw // 3, sh * 3 // 5 + 180 + + @classmethod + def auth_window_size(cls) -> tuple[int, int]: + return cls.scale_dialog_size(360, 400) + + @classmethod + def scale_dialog_size(cls, base_w: int, base_h: int) -> tuple[int, int]: + """对话框尺寸不超过屏幕比例范围,并按需缩小(对齐原 Tk 逻辑)。""" + sw, sh = cls.screen_size() + max_w, max_h = sw // 3, sh * 3 // 5 + 180 + scale = min(max_w / base_w, max_h / base_h, 1.0) + return max(280, int(base_w * scale)), max(140, int(base_h * scale)) + + @classmethod + def feature_window_size(cls, feature_id: str) -> Optional[tuple[int, int, int, int]]: + spec = FEATURE_WINDOW_SPECS.get(feature_id) + if spec is None: + return None + design_w, design_h, base_w, base_h = spec + if feature_id == "training_assist": + w, h = cls.main_window_size() + else: + w, h = cls.scale_dialog_size(base_w, base_h) + return w, h, design_w, design_h + + @classmethod + def center_on_screen(cls, widget: QWidget, width: int, height: int) -> None: + sw, sh = cls.screen_size() + x = max(0, (sw - width) // 2) + y = max(0, (sh - height) // 2) + widget.setGeometry(x, y, width, height) + + +# --------------------------------------------------------------------------- +# App branding — 应用图标 +# --------------------------------------------------------------------------- + +APP_NAME = "TALENT ACADEMY" +APP_ICON_PATH = "icon.png" +_APP_ICON: Optional[QIcon] = None + + +def app_icon() -> QIcon: + global _APP_ICON + if _APP_ICON is None: + path = UITheme.resource_root() / APP_ICON_PATH + _APP_ICON = QIcon(str(path)) if path.is_file() else QIcon() + return _APP_ICON + + +def apply_window_icon(widget: QWidget) -> None: + widget.setWindowIcon(app_icon()) + + +def _hex_to_colorref(hex_color: str) -> int: + h = str(hex_color).lstrip("#") + if len(h) != 6: + return 0 + r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16) + return r | (g << 8) | (b << 16) + + +def apply_window_titlebar_theme(widget: QWidget, bg_hex=None, fg_hex=None) -> None: + if sys.platform != "win32": + return + try: + import ctypes + + hwnd = int(widget.winId()) + dwmapi = ctypes.windll.dwmapi + dark = ctypes.c_int(1) + dwmapi.DwmSetWindowAttribute(hwnd, 20, ctypes.byref(dark), ctypes.sizeof(dark)) + bg = _hex_to_colorref(bg_hex or UITheme.BG) + fg = _hex_to_colorref(fg_hex or UITheme.FG) + for attr, color in ((34, bg), (35, bg), (36, fg)): + value = ctypes.c_int(color) + dwmapi.DwmSetWindowAttribute(hwnd, attr, ctypes.byref(value), ctypes.sizeof(value)) + except Exception: + pass + + +def apply_fixed_window(widget: QWidget, width: int, height: int, alpha: float = WINDOW_ALPHA) -> None: + widget.setFixedSize(width, height) + WindowGeom.center_on_screen(widget, width, height) + widget.setWindowOpacity(float(alpha)) + apply_window_icon(widget) + apply_window_titlebar_theme(widget) + + +def create_app(argv=None) -> QApplication: + # 在任何可能 import pygame 的代码之前写入,避免 SDL 闪窗 + os.environ["SDL_VIDEODRIVER"] = "dummy" + os.environ["SDL_AUDIODRIVER"] = "dummy" + app = QApplication.instance() + if app is None: + app = QApplication(argv or sys.argv) + UITheme.load_fonts() + app.setWindowIcon(app_icon()) + app.setStyleSheet(UITheme.qss()) + return app + + +# --------------------------------------------------------------------------- +# Capability / Feature / Shell +# --------------------------------------------------------------------------- + + +class CapabilitySet: + def __init__(self, enabled: Optional[Iterable[str]] = None): + self._enabled = set(enabled if enabled is not None else DEFAULT_CAPABILITIES) + + def has(self, key: str) -> bool: + return key in self._enabled + + def set_enabled(self, key: str, on: bool) -> None: + if on: + self._enabled.add(key) + else: + self._enabled.discard(key) + + def replace(self, keys: Iterable[str]) -> None: + self._enabled = set(keys) + + def copy(self) -> "CapabilitySet": + return CapabilitySet(self._enabled) + + def as_set(self) -> set[str]: + return set(self._enabled) + + +class Feature(QWidget): + """功能页基类:在 ui/ 中继承并组合 WidgetKit。""" + + feature_id: str = "" + title: str = "" + + def apply_capabilities(self, caps: CapabilitySet) -> None: + pass + + def on_activated(self) -> None: + pass + + def on_deactivated(self) -> None: + pass + + +class FeatureRegistry: + def __init__(self): + self._features: dict[str, Feature] = {} + + def register(self, feature: Feature) -> None: + if not feature.feature_id: + raise ValueError("feature_id 不能为空") + self._features[feature.feature_id] = feature + + def get(self, feature_id: str) -> Optional[Feature]: + return self._features.get(feature_id) + + def all_ids(self) -> list[str]: + return list(self._features.keys()) + + def values(self) -> list[Feature]: + return list(self._features.values()) + + +class AppShell(QMainWindow): + """主壳:QStackedWidget 切换 Feature,统一下发 Capability。""" + + featureChanged = Signal(str) + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("TALENT ACADEMY") + apply_window_icon(self) + self._registry = FeatureRegistry() + self._caps = CapabilitySet() + self._current_id: Optional[str] = None + self._stack = QStackedWidget() + self.setCentralWidget(self._stack) + + @property + def registry(self) -> FeatureRegistry: + return self._registry + + @property + def current_id(self) -> Optional[str]: + return self._current_id + + @property + def capabilities(self) -> CapabilitySet: + return self._caps + + def register(self, feature: Feature) -> None: + self._registry.register(feature) + self._stack.addWidget(feature) + feature.apply_capabilities(self._caps) + + def show_feature(self, feature_id: str) -> None: + feature = self._registry.get(feature_id) + if feature is None: + raise KeyError(f"未注册功能: {feature_id}") + if self._current_id and self._current_id != feature_id: + prev = self._registry.get(self._current_id) + if prev is not None: + prev.on_deactivated() + self._stack.setCurrentWidget(feature) + self._current_id = feature_id + if feature.title: + self.setWindowTitle(f"TALENT ACADEMY - {feature.title}") + else: + self.setWindowTitle("TALENT ACADEMY") + feature.apply_capabilities(self._caps) + feature.on_activated() + self.featureChanged.emit(feature_id) + apply_feature_window(self, feature_id) + + def set_capabilities(self, caps: CapabilitySet) -> None: + self._caps = caps.copy() + for feature in self._registry.values(): + feature.apply_capabilities(self._caps) + + def current_feature(self) -> Optional[Feature]: + if not self._current_id: + return None + return self._registry.get(self._current_id) + + +def apply_feature_window(shell: AppShell, feature_id: str) -> None: + """按功能页切换窗口尺寸与主题缩放。""" + spec = WindowGeom.feature_window_size(feature_id) + if spec is None: + return + width, height, design_w, design_h = spec + UITheme.apply_scale(width, height, design_w, design_h) + app = QApplication.instance() + if app is not None: + app.setStyleSheet(UITheme.qss()) + apply_fixed_window(shell, width, height) + + +# --------------------------------------------------------------------------- +# WidgetKit +# --------------------------------------------------------------------------- + + +class CardFrame(QFrame): + def __init__(self, parent=None, flat: bool = False, metric: bool = False): + super().__init__(parent) + if metric: + self.setObjectName("MetricCard") + elif flat: + self.setObjectName("SurfaceFlat") + else: + self.setObjectName("Card") + self.setAttribute(Qt.WA_StyledBackground, True) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + + +class StatusBar(QWidget): + """同一行两列:左事件提示(左对齐),右倍速提示(右对齐)。""" + + def __init__(self, text: str = "", parent=None): + super().__init__(parent) + self.setObjectName("StatusBar") + self.setAutoFillBackground(False) + row = QHBoxLayout(self) + row.setContentsMargins(0, 0, 0, 0) + row.setSpacing(UITheme.scaled(12, 8)) + + self._event = QLabel("") + self._event.setObjectName("StatusEvent") + self._event.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + self._event.setWordWrap(False) + + self._rate = QLabel("") + self._rate.setObjectName("StatusRate") + self._rate.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + self._rate.setWordWrap(False) + + row.addWidget(self._event, 1) + row.addWidget(self._rate, 1) + self.set_status(text or "", "muted") + + def set_status(self, text: str, kind: str = "muted") -> None: + self._event.setText(text) + self._event.setStyleSheet(f"color: {status_kind_color(kind)}; background: transparent;") + + def set_rate_status(self, text: str, kind: str = "muted") -> None: + self._rate.setText(text) + self._rate.setStyleSheet(f"color: {status_kind_color(kind)}; background: transparent;") + + +class ActionBar(QWidget): + """通用水平按钮行;clicked(button_id)。默认居中,可左/右对齐。""" + + clicked = Signal(str) + + def __init__( + self, + parent=None, + button_defs: Optional[list[dict]] = None, + alignment: Qt.AlignmentFlag = Qt.AlignCenter, + ): + super().__init__(parent) + self.setObjectName("ActionBar") + self.setAutoFillBackground(False) + self._buttons: dict[str, QPushButton] = {} + outer = QHBoxLayout(self) + outer.setContentsMargins(0, 0, 0, 0) + row = QWidget() + row.setObjectName("ActionBarRow") + row.setAutoFillBackground(False) + self._row_layout = QHBoxLayout(row) + self._row_layout.setContentsMargins(0, 0, 0, 0) + self._row_layout.setSpacing(UITheme.scaled(14, 8)) + align = int(alignment) + if align & int(Qt.AlignLeft): + outer.addWidget(row) + outer.addStretch(1) + elif align & int(Qt.AlignRight): + outer.addStretch(1) + outer.addWidget(row) + else: + outer.addStretch(1) + outer.addWidget(row) + outer.addStretch(1) + for spec in button_defs or []: + self.add_button( + spec["id"], + spec.get("text", spec["id"]), + enabled=spec.get("enabled", True), + visible=spec.get("visible", True), + ) + + def add_button(self, button_id: str, text: str, enabled: bool = True, visible: bool = True) -> QPushButton: + btn = QPushButton(text) + btn.setObjectName("FlatButton") + btn.setEnabled(enabled) + btn.setVisible(visible) + btn.clicked.connect(lambda _=False, i=button_id: self.clicked.emit(i)) + self._row_layout.addWidget(btn) + self._buttons[button_id] = btn + return btn + + def set_button(self, button_id: str, text: Optional[str] = None, enabled: Optional[bool] = None, visible: Optional[bool] = None) -> None: + btn = self._buttons.get(button_id) + if btn is None: + return + if text is not None: + btn.setText(text) + if enabled is not None: + btn.setEnabled(enabled) + if visible is not None: + btn.setVisible(visible) + + def set_button_visible(self, button_id: str, visible: bool) -> None: + self.set_button(button_id, visible=visible) + + def button(self, button_id: str) -> Optional[QPushButton]: + return self._buttons.get(button_id) + + +class MetricStrip(QWidget): + """一行三列:缸头温度 / 扭矩 / 健康值。""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setAutoFillBackground(False) + self._health_pct = None + self._health_flash_on = False + self._health_flash_timer = QTimer(self) + self._health_flash_timer.timeout.connect(self._tick_health_flash) + root = QHBoxLayout(self) + root.setContentsMargins(0, 0, 0, 0) + root.setSpacing(UITheme.scaled(8, 4)) + self.cht_value, self.cht_unit = self._add_column(root, "缸头温度", "F", tech_big=True) + self.torque_value, self.torque_unit = self._add_column(root, "扭矩", "lb-ft", tech_big=True) + self.health_value, self.health_unit = self._add_column(root, "健康值", "%", tech_big=True) + self.set_cht(None) + self.set_torque(None) + self.set_health(None) + + def _add_column(self, row: QHBoxLayout, title: str, unit: str, tech_big: bool = False): + card = CardFrame(metric=True) + cell_l = QVBoxLayout(card) + cell_l.setContentsMargins( + UITheme.scaled(12, 6), + UITheme.scaled(12, 6), + UITheme.scaled(12, 6), + UITheme.scaled(12, 6), + ) + cell_l.setSpacing(2) + t = QLabel(title) + t.setObjectName("MetricCardTitle") + t.setAlignment(Qt.AlignCenter) + cell_l.addWidget(t) + bottom = QHBoxLayout() + bottom.setContentsMargins(0, 0, 0, 0) + value = QLabel(format_metric_value(None)) + value.setObjectName("MetricValue") + value.setFont(UITheme.font_tech(UITheme.FONT_CHT)) + value.setAlignment(Qt.AlignLeft | Qt.AlignBottom) + unit_l = QLabel(unit) + unit_l.setObjectName("MetricUnit") + unit_l.setAlignment(Qt.AlignLeft | Qt.AlignBottom) + bottom.addWidget(value, 1) + bottom.addWidget(unit_l, 0) + cell_l.addLayout(bottom) + row.addWidget(card, 1) + return value, unit_l + + def _paint_pair(self, value_lbl: QLabel, unit_lbl: QLabel, text: str, color: str) -> None: + value_lbl.setText(text) + value_lbl.setStyleSheet(f"color: {color}; background: transparent;") + unit_lbl.setStyleSheet(f"color: {color}; background: transparent;") + + def set_cht(self, value) -> None: + self._paint_pair(self.cht_value, self.cht_unit, format_metric_value(value), cht_color(value)) + + def set_torque(self, value) -> None: + self._paint_pair(self.torque_value, self.torque_unit, format_metric_value(value), torque_color(value)) + + def set_health(self, pct) -> None: + self._health_pct = pct + display = None if pct is None else round(float(pct), 1) + band = health_band(display) + if band == "flash": + self.start_health_flash() + return + self.stop_health_flash() + self._paint_pair( + self.health_value, + self.health_unit, + format_metric_value(display), + health_color(display), + ) + + def start_health_flash(self) -> None: + if self._health_pct is None: + return + if not self._health_flash_timer.isActive(): + self._health_flash_on = False + self._tick_health_flash() + self._health_flash_timer.start(HEALTH_FLASH_MS) + + def stop_health_flash(self) -> None: + self._health_flash_timer.stop() + self._health_flash_on = False + + def _tick_health_flash(self) -> None: + if self._health_pct is None: + self.stop_health_flash() + return + self._health_flash_on = not self._health_flash_on + color = UITheme.ERROR if self._health_flash_on else UITheme.FLASH_BLUE + display = round(float(self._health_pct), 1) + self._paint_pair( + self.health_value, + self.health_unit, + format_metric_value(display), + color, + ) + + +class LapBoard(QWidget): + """模式列 + LAP 表 + NET(PENALTY)TOTAL(三等分布局)。""" + + def __init__(self, parent=None, rows: int = LAP_TABLE_ROWS): + super().__init__(parent) + self._rows = rows + self._session_mode = "freeflight" + card = CardFrame(self) + root = QVBoxLayout(self) + root.setContentsMargins(0, 0, 0, 0) + root.addWidget(card) + row = QHBoxLayout(card) + row.setContentsMargins(UITheme.scaled(8, 4), UITheme.scaled(8, 4), UITheme.scaled(8, 4), UITheme.scaled(8, 4)) + row.setSpacing(UITheme.scaled(8, 4)) + + mode_col = QVBoxLayout() + mode_col.setAlignment(Qt.AlignCenter) + mode_col.setSpacing(UITheme.scaled(6, 3)) + self.mission_label = QLabel("") + self.mission_label.setObjectName("SessionMissionLabel") + self.mission_label.setFont(UITheme.font(UITheme.FONT_MD)) + self.mission_label.setAlignment(Qt.AlignCenter) + self.mission_label.setWordWrap(True) + self.mission_label.setVisible(False) + self.mode_label = QLabel("") + self.mode_label.setObjectName("SessionModeLabel") + self.mode_label.setFont(UITheme.font_tech(UITheme.FONT_LG)) + self.mode_label.setAlignment(Qt.AlignCenter) + self.mode_label.setWordWrap(True) + mode_col.addStretch(1) + mode_col.addWidget(self.mission_label, 0, Qt.AlignCenter) + mode_col.addWidget(self.mode_label, 0, Qt.AlignCenter) + mode_col.addStretch(1) + + lap_col = QVBoxLayout() + lap_col.setAlignment(Qt.AlignCenter) + self._lap_labels: list[tuple[QLabel, QLabel]] = [] + grid = QGridLayout() + grid.setHorizontalSpacing(UITheme.scaled(28, 16)) + grid.setVerticalSpacing(UITheme.scaled(1, 0)) + grid.setColumnStretch(0, 1) + grid.setColumnStretch(1, 1) + for i in range(rows): + name = QLabel(f"LAP {i + 1:02d}") + name.setObjectName("InfoValue") + name.setFont(UITheme.font_tech(UITheme.FONT_LG)) + tm = QLabel("--") + tm.setObjectName("InfoValue") + tm.setFont(UITheme.font_tech(UITheme.FONT_LG)) + grid.addWidget(name, i, 0, Qt.AlignCenter) + grid.addWidget(tm, i, 1, Qt.AlignCenter) + self._lap_labels.append((name, tm)) + lap_col.addLayout(grid) + + total_col = QVBoxLayout() + total_col.setAlignment(Qt.AlignCenter) + total_title = QLabel("NET(PENALTY)TOTAL") + total_title.setObjectName("MetricTitle") + total_title.setAlignment(Qt.AlignCenter) + self.total_label = QLabel("--") + self.total_label.setObjectName("LapTotalValue") + self.total_label.setAlignment(Qt.AlignCenter) + self.total_label.setFont(UITheme.font_tech(UITheme.FONT_LAP_TOTAL, bold=True)) + total_col.addWidget(total_title) + total_col.addWidget(self.total_label) + + row.addLayout(mode_col, 1) + row.addLayout(lap_col, 1) + row.addLayout(total_col, 1) + self.set_session_mode("freeflight") + self.set_mission_title(None) + + def set_mission_title(self, title: Optional[str] = None) -> None: + text = (title or "").strip() + if text: + self.mission_label.setText(text) + self.mission_label.setVisible(True) + else: + self.mission_label.setText("") + self.mission_label.setVisible(False) + + def set_session_mode(self, mode: str = "freeflight") -> None: + self._session_mode = mode + if mode == "mission_sp": + self.mode_label.setText("Mission Mode") + self.mode_label.setStyleSheet(f"color: {UITheme.FLASH_BLUE}; background: transparent;") + elif mode == "mission_mp": + self.mode_label.setText("Multiplay Mode") + self.mode_label.setStyleSheet(f"color: {UITheme.WARNING}; background: transparent;") + else: + self.mode_label.setText("Free Flight Mode") + self.mode_label.setStyleSheet(f"color: {UITheme.SUCCESS}; background: transparent;") + + def set_laps( + self, + times: list, + total=None, + lap_penalties: Optional[list] = None, + total_penalty=None, + ) -> None: + for i, (_name, tm) in enumerate(self._lap_labels): + value = times[i] if i < len(times) else None + penalty = lap_penalties[i] if lap_penalties and i < len(lap_penalties) else None + tm.setText(format_lap_time_with_penalty(value, penalty)) + self.total_label.setText(format_total_time_display(total, total_penalty)) + + +# --------------------------------------------------------------------------- +# 可复用流程面板(页面在 ui/ 薄封装) +# --------------------------------------------------------------------------- + + +class SignalLightIndicator(QWidget): + """扁平圆形信号灯:idle 熄灭,run 蓝,ok 绿,fail 红。""" + + _COLORS = { + "idle": UITheme.SIGNAL_IDLE, + "run": UITheme.SIGNAL_RUN, + "ok": UITheme.SIGNAL_OK, + "fail": UITheme.SIGNAL_FAIL, + } + + def __init__(self, parent=None, diameter: int = 11): + super().__init__(parent) + self._state = "idle" + size = UITheme.scaled(diameter, 9) + self.setFixedSize(size, size) + + def set_state(self, state: str) -> None: + next_state = state if state in self._COLORS else "idle" + if next_state == self._state: + return + self._state = next_state + self.update() + + def paintEvent(self, _event) -> None: + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing, True) + painter.setPen(Qt.NoPen) + painter.setBrush(QColor(self._COLORS[self._state])) + margin = max(1, self.width() // 10) + diameter = min(self.width(), self.height()) - margin * 2 + x = (self.width() - diameter) / 2.0 + y = (self.height() - diameter) / 2.0 + painter.drawEllipse(int(x), int(y), int(diameter), int(diameter)) + painter.end() + + +class SimCheckPanel(QWidget): + actionClicked = Signal() + + _TITLE_OBJ = { + "idle": "CheckWait", + "run": "CheckRun", + "ok": "CheckOk", + "fail": "CheckFail", + } + + def __init__(self, parent=None, items: Optional[list[str]] = None): + super().__init__(parent) + labels = items or ["网络连接", "鉴权服务器", "模拟器"] + layout = QVBoxLayout(self) + layout.setContentsMargins( + UITheme.scaled(36, 20), + UITheme.scaled(32, 18), + UITheme.scaled(36, 20), + UITheme.scaled(28, 16), + ) + layout.setSpacing(0) + title = QLabel("FSX竞速助手") + title.setObjectName("Title") + title.setAlignment(Qt.AlignCenter) + self.subtitle = QLabel("Initializing...") + self.subtitle.setObjectName("Subtitle") + self.subtitle.setAlignment(Qt.AlignCenter) + layout.addWidget(title) + layout.addSpacing(UITheme.scaled(6, 3)) + layout.addWidget(self.subtitle) + layout.addSpacing(UITheme.scaled(18, 10)) + + list_card = CardFrame() + list_card.setObjectName("CheckListCard") + list_l = QVBoxLayout(list_card) + list_l.setContentsMargins( + UITheme.scaled(22, 14), + UITheme.scaled(20, 12), + UITheme.scaled(22, 14), + UITheme.scaled(20, 12), + ) + list_l.setSpacing(UITheme.scaled(16, 8)) + self._rows = [] + for label in labels: + row = QHBoxLayout() + row.setSpacing(UITheme.scaled(12, 6)) + mark = SignalLightIndicator(diameter=13) + name = QLabel(label) + name.setObjectName("CheckWait") + detail = QLabel("等待") + detail.setObjectName("CheckItemDetail") + row.addWidget(mark, 0, Qt.AlignVCenter) + row.addWidget(name, 0, Qt.AlignVCenter) + row.addStretch(1) + row.addWidget(detail, 0, Qt.AlignVCenter) + list_l.addLayout(row) + self._rows.append({"mark": mark, "title": name, "detail": detail}) + layout.addWidget(list_card, 1) + layout.addSpacing(UITheme.scaled(24, 14)) + + self.action_btn = QPushButton("START") + self.action_btn.setObjectName("FlatButton") + self.action_btn.setMinimumWidth(UITheme.scaled(120, 96)) + self.action_btn.setEnabled(False) + self.action_btn.clicked.connect(self.actionClicked.emit) + btn_row = QHBoxLayout() + btn_row.setContentsMargins(0, 0, 0, UITheme.scaled(4, 2)) + btn_row.addStretch(1) + btn_row.addWidget(self.action_btn) + btn_row.addStretch(1) + layout.addLayout(btn_row) + + def set_subtitle(self, text: str) -> None: + self.subtitle.setText(text) + + def set_item(self, index: int, state: str, detail: str) -> None: + if index < 0 or index >= len(self._rows): + return + widgets = self._rows[index] + widgets["mark"].set_state(state) + widgets["title"].setObjectName(self._TITLE_OBJ.get(state, "CheckWait")) + widgets["detail"].setText(detail) + w = widgets["title"] + w.style().unpolish(w) + w.style().polish(w) + + def set_action(self, text: str, enabled: bool = True) -> None: + self.action_btn.setText(text) + self.action_btn.setEnabled(enabled) + + def set_busy(self, busy: bool) -> None: + if busy: + self.action_btn.setEnabled(False) + + +class AuthPanel(QWidget): + loginRequested = Signal(str, str) + + def __init__(self, parent=None): + super().__init__(parent) + layout = QVBoxLayout(self) + layout.setContentsMargins(UITheme.scaled(24, 14), UITheme.scaled(20, 10), UITheme.scaled(24, 14), UITheme.scaled(20, 10)) + + body = QVBoxLayout() + title = QLabel("TALENT ACADEMY") + title.setObjectName("Title") + title.setAlignment(Qt.AlignCenter) + sub = QLabel("模拟飞行训练助手") + sub.setObjectName("Subtitle") + sub.setAlignment(Qt.AlignCenter) + body.addWidget(title) + body.addWidget(sub) + body.addSpacing(UITheme.scaled(14, 6)) + body.addWidget(QLabel("用户名")) + self.username = QLineEdit() + body.addWidget(self.username) + body.addSpacing(UITheme.scaled(10, 4)) + body.addWidget(QLabel("密码")) + self.password = QLineEdit() + self.password.setEchoMode(QLineEdit.Password) + body.addWidget(self.password) + body.addStretch(1) + layout.addLayout(body, 1) + + self.status = QLabel("") + self.status.setObjectName("Muted") + self.status.setAlignment(Qt.AlignCenter) + self.status.setWordWrap(True) + layout.addWidget(self.status) + + self.login_btn = QPushButton("登录") + self.login_btn.setObjectName("FlatButton") + self.login_btn.clicked.connect(self._emit_login) + btn_row = QHBoxLayout() + btn_row.addStretch(1) + btn_row.addWidget(self.login_btn) + btn_row.addStretch(1) + layout.addLayout(btn_row) + + self.username.returnPressed.connect(self._emit_login) + self.password.returnPressed.connect(self._emit_login) + + def _emit_login(self) -> None: + self.loginRequested.emit(self.username.text(), self.password.text()) + + def get_credentials(self) -> tuple[str, str]: + return self.username.text(), self.password.text() + + def set_status(self, text: str, kind: str = "muted") -> None: + self.status.setText(text) + color = status_kind_color(kind if kind != "ok" else "ok") + if kind == "error": + color = UITheme.ERROR + self.status.setStyleSheet(f"color: {color}; background: transparent;") + + def set_login_enabled(self, enabled: bool) -> None: + self.login_btn.setEnabled(enabled) + + def clear_password(self) -> None: + self.password.clear() + + +# --------------------------------------------------------------------------- +# 自定义控件(跟飞设置等) +# --------------------------------------------------------------------------- + + +class SlideToggle(QWidget): + """主题圆角滑动开关(统一配色,勿在外部硬编码样式)。""" + + toggled = Signal(bool) + + def __init__(self, parent=None, checked: bool = False): + super().__init__(parent) + self._checked = bool(checked) + self._track_w = UITheme.scaled(52, 42) + self._track_h = UITheme.scaled(26, 20) + self.setFixedSize(self._track_w, self._track_h) + self.setCursor(Qt.PointingHandCursor) + self.setAutoFillBackground(False) + + def isChecked(self) -> bool: + return self._checked + + def setChecked(self, checked: bool) -> None: + checked = bool(checked) + if self._checked == checked: + return + self._checked = checked + self.update() + self.toggled.emit(self._checked) + + def paintEvent(self, _event) -> None: + p = QPainter(self) + p.setRenderHint(QPainter.Antialiasing) + p.setPen(Qt.NoPen) + enabled = self.isEnabled() + pad = max(2, UITheme.scaled(3, 2)) + knob_d = self._track_h - 2 * pad + radius = self._track_h / 2 + if self._checked: + track_color = QColor(UITheme.TOGGLE_TRACK_ON) + border_color = None + else: + track_color = QColor(UITheme.TOGGLE_TRACK_OFF) + border_color = QColor(UITheme.TOGGLE_TRACK_BORDER) + if not enabled: + track_color.setAlpha(140) + p.setPen(Qt.NoPen) + p.setBrush(track_color) + p.drawRoundedRect(0, 0, self._track_w, self._track_h, radius, radius) + if border_color is not None: + pen = QPen(border_color) + pen.setWidth(max(1, UITheme.scaled(1, 1))) + p.setPen(pen) + p.setBrush(Qt.NoBrush) + p.drawRoundedRect(0, 0, self._track_w - 1, self._track_h - 1, radius, radius) + kx = (self._track_w - pad - knob_d) if self._checked else pad + knob_color = QColor( + UITheme.TOGGLE_KNOB_ON if self._checked else UITheme.TOGGLE_KNOB_OFF + ) + if not enabled: + knob_color.setAlpha(140) + p.setPen(Qt.NoPen) + p.setBrush(knob_color) + p.drawEllipse(int(kx), pad, int(knob_d), int(knob_d)) + + def mousePressEvent(self, event: QMouseEvent) -> None: + if event.button() == Qt.LeftButton and self.isEnabled(): + self.setChecked(not self._checked) + + +class DualStateButton(QPushButton): + """双状态按钮:与「对比数据」FlatButton 同款,仅按状态切换文案。""" + + def __init__( + self, + on_text: str, + off_text: str, + parent=None, + checked: bool = True, + width: int = 140, + height: int = 36, + ): + super().__init__(parent) + # on_text:开/显示态文案(动作:关闭/隐藏) + # off_text:关/隐藏态文案(动作:打开/显示) + self._on_text = str(on_text) + self._off_text = str(off_text) + self.setObjectName("DualStateButton") + self.setCheckable(True) + self.setCursor(Qt.PointingHandCursor) + self.setFixedSize(UITheme.scaled(int(width), 72), UITheme.scaled(int(height), 28)) + self.toggled.connect(self._sync_appearance) + self.setChecked(bool(checked)) + self._sync_appearance(self.isChecked()) + + def _sync_appearance(self, checked: bool = False) -> None: + self.setText(self._on_text if checked else self._off_text) + + +class RoundSlider(QWidget): + """主题圆角滑动条(胶囊轨道 + 圆形滑块)。""" + + valueChanged = Signal(float) + + def __init__( + self, + parent=None, + from_: float = 0.0, + to: float = 1.0, + value: float = 0.0, + resolution: float = 0.1, + length: Optional[int] = None, + ): + super().__init__(parent) + self._from = float(from_) + self._to = float(to) + self._resolution = float(resolution) + self._value = self._clamp(value) + width = length if length is not None else UITheme.scaled(320, 220) + height = UITheme.scaled(32, 26) + self.setFixedSize(width, height) + self.setCursor(Qt.PointingHandCursor) + self.setAutoFillBackground(False) + self.setAttribute(Qt.WA_TranslucentBackground, True) + self.setStyleSheet("background: transparent;") + self._pad_x = UITheme.scaled(12, 10) + self._thumb_r = UITheme.scaled(10, 8) + self._track_h = UITheme.scaled(6, 5) + + def value(self) -> float: + return self._value + + def setValue(self, value: float) -> None: + value = self._clamp(value) + if abs(self._value - value) < 1e-9: + return + self._value = value + self.update() + self.valueChanged.emit(self._value) + + def _clamp(self, v: float) -> float: + span = max(1e-6, self._to - self._from) + v = max(self._from, min(self._to, float(v))) + if self._resolution and self._resolution > 0: + v = round(v / self._resolution) * self._resolution + return max(self._from, min(self._to, v)) + + def _track_bounds(self) -> tuple[float, float, float]: + track_x1 = self._pad_x + self._thumb_r + track_x2 = self.width() - self._pad_x - self._thumb_r + track_y = self.height() // 2 + return track_x1, track_x2, track_y + + def _value_to_x(self, v: float) -> float: + track_x1, track_x2, _ = self._track_bounds() + frac = (self._clamp(v) - self._from) / max(1e-6, self._to - self._from) + return track_x1 + frac * (track_x2 - track_x1) + + def _x_to_value(self, x: float) -> float: + track_x1, track_x2, _ = self._track_bounds() + frac = (x - track_x1) / max(1e-6, track_x2 - track_x1) + frac = max(0.0, min(1.0, frac)) + return self._clamp(self._from + frac * (self._to - self._from)) + + def paintEvent(self, _event) -> None: + p = QPainter(self) + p.setRenderHint(QPainter.Antialiasing) + track_x1, track_x2, track_y = self._track_bounds() + tx = self._value_to_x(self._value) + ty1 = track_y - self._track_h / 2 + r = self._track_h / 2 + # 深色底轨 + 边框,避免在 CHART_WELL 背景上隐没 + p.setPen(QPen(QColor(UITheme.BORDER), 1.0)) + p.setBrush(QColor(UITheme.BG)) + p.drawRoundedRect(int(track_x1), int(ty1), int(track_x2 - track_x1), int(self._track_h), r, r) + if tx > track_x1 + 1: + p.setPen(Qt.NoPen) + p.setBrush(QColor(UITheme.ACCENT)) + p.drawRoundedRect(int(track_x1), int(ty1), int(tx - track_x1), int(self._track_h), r, r) + p.setPen(QPen(QColor(UITheme.BORDER), 1.0)) + p.setBrush(QColor(UITheme.FG_ON_ACCENT)) + p.drawEllipse(int(tx - self._thumb_r), int(track_y - self._thumb_r), int(2 * self._thumb_r), int(2 * self._thumb_r)) + + def _apply_pos(self, pos) -> None: + self.setValue(self._x_to_value(pos.x())) + + def mousePressEvent(self, event: QMouseEvent) -> None: + if event.button() == Qt.LeftButton: + self._apply_pos(event.pos()) + + def mouseMoveEvent(self, event: QMouseEvent) -> None: + if event.buttons() & Qt.LeftButton: + self._apply_pos(event.pos()) + + +class ShadowFollowDialog(QDialog): + """跟飞参数设置;Accepted 时 result_value 为 dict。""" + + def __init__( + self, + parent=None, + lead_s: float = 3.0, + smoke_enabled: bool = True, + cold_cabin_enabled: bool = False, + ): + super().__init__(parent) + from models.shadow_plane import SHADOW_LEAD_DEFAULT_S, SHADOW_LEAD_MAX_S, SHADOW_LEAD_MIN_S + + self._min_s = SHADOW_LEAD_MIN_S + self._max_s = SHADOW_LEAD_MAX_S + self.result_value: Optional[dict] = None + self.setWindowTitle("跟飞设置") + self.setModal(True) + w, h = WindowGeom.scale_dialog_size(420, 320) + UITheme.apply_scale(w, h, 420, 320) + apply_fixed_window(self, w, h) + + layout = QVBoxLayout(self) + layout.setContentsMargins( + UITheme.scaled(28, 12), + UITheme.scaled(24, 10), + UITheme.scaled(28, 12), + UITheme.scaled(24, 10), + ) + title = QLabel("请设置跟飞参数") + title.setAlignment(Qt.AlignCenter) + layout.addWidget(title) + + initial = float(lead_s if lead_s is not None else SHADOW_LEAD_DEFAULT_S) + initial = max(self._min_s, min(self._max_s, initial)) + self._value_label = QLabel(f"领先时间:{initial:.1f} 秒") + self._value_label.setAlignment(Qt.AlignCenter) + layout.addWidget(self._value_label) + + self._slider = RoundSlider( + from_=self._min_s, + to=self._max_s, + value=initial, + resolution=0.1, + length=UITheme.scaled(320, 220), + ) + self._slider.valueChanged.connect(self._on_slide) + slider_row = QHBoxLayout() + slider_row.addStretch(1) + slider_row.addWidget(self._slider) + slider_row.addStretch(1) + layout.addLayout(slider_row) + + # hint = QLabel(f"{self._min_s:.0f}s ←——→ {self._max_s:.0f}s") + # hint.setObjectName("Muted") + # hint.setAlignment(Qt.AlignCenter) + # layout.addWidget(hint) + + options_row = QGridLayout() + options_row.setHorizontalSpacing(UITheme.scaled(12, 8)) + options_row.setColumnStretch(0, 1) + options_row.setColumnStretch(1, 0) + options_row.setColumnStretch(2, 1) + options_row.setColumnStretch(3, 0) + options_row.setColumnStretch(4, 1) + cold_lbl = QLabel("开启冷舱") + cold_lbl.setObjectName("Muted") + cold_lbl.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + options_row.addWidget(cold_lbl, 0, 0) + self._cold_toggle = SlideToggle(checked=bool(cold_cabin_enabled)) + options_row.addWidget(self._cold_toggle, 0, 1) + smoke_lbl = QLabel("拉烟") + smoke_lbl.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + options_row.addWidget(smoke_lbl, 0, 2) + self._smoke_toggle = SlideToggle(checked=bool(smoke_enabled)) + options_row.addWidget(self._smoke_toggle, 0, 3) + layout.addLayout(options_row) + + btn_row = QHBoxLayout() + btn_row.setSpacing(UITheme.scaled(14, 8)) + btn_row.addStretch(1) + btn_ok = QPushButton("开始跟飞") + btn_ok.setObjectName("FlatButton") + btn_cancel = QPushButton("取消") + btn_cancel.setObjectName("FlatButton") + btn_ok.clicked.connect(self._on_ok) + btn_cancel.clicked.connect(self.reject) + btn_row.addWidget(btn_ok) + btn_row.addWidget(btn_cancel) + btn_row.addStretch(1) + layout.addLayout(btn_row) + + def _on_slide(self, value: float) -> None: + self._value_label.setText(f"领先时间:{value:.1f} 秒") + + def _on_ok(self) -> None: + self.result_value = { + "lead": max(self._min_s, min(self._max_s, float(self._slider.value()))), + "smoke": bool(self._smoke_toggle.isChecked()), + "cold_cabin": bool(self._cold_toggle.isChecked()), + } + self.accept() + + +def show_shadow_follow_settings( + parent=None, + lead_s: float = 3.0, + smoke_enabled: bool = True, + cold_cabin_enabled: bool = False, +) -> Optional[dict]: + dlg = ShadowFollowDialog( + parent, + lead_s=lead_s, + smoke_enabled=smoke_enabled, + cold_cabin_enabled=cold_cabin_enabled, + ) + if dlg.exec_() == QDialog.Accepted: + return dlg.result_value + return None + + +# --------------------------------------------------------------------------- +# Dialogs +# --------------------------------------------------------------------------- + + +def play_ui_alert_sound() -> None: + try: + if sys.platform == "win32": + import winsound + + winsound.MessageBeep(winsound.MB_ICONEXCLAMATION) + else: + app = QApplication.instance() + if app is not None: + app.beep() + except Exception: + pass + + +class ThemeAlertDialog(QDialog): + """主题化提示框(替代系统 QMessageBox)。""" + + _DIALOG_W = 360 + _DIALOG_H = 160 + + def __init__( + self, + parent=None, + title: str = "提示", + message: str = "", + kind: str = "warn", + button_text: str = "确定", + show_title: bool = True, + play_sound: bool = True, + ): + super().__init__(parent) + self._play_sound = bool(play_sound) + self.setWindowTitle(title if show_title else "提示") + self.setModal(True) + w, h = WindowGeom.scale_dialog_size(self._DIALOG_W, self._DIALOG_H) + UITheme.apply_scale(w, h, self._DIALOG_W, self._DIALOG_H) + apply_fixed_window(self, w, h) + + layout = QVBoxLayout(self) + layout.setContentsMargins( + UITheme.scaled(28, 12), + UITheme.scaled(24, 10), + UITheme.scaled(28, 12), + UITheme.scaled(24, 10), + ) + layout.setSpacing(UITheme.scaled(12, 8)) + + if show_title: + title_lbl = QLabel(title) + title_lbl.setObjectName("DialogTitle") + title_lbl.setAlignment(Qt.AlignCenter) + layout.addWidget(title_lbl) + + msg_lbl = QLabel(message) + msg_lbl.setObjectName("DialogTitle" if not show_title else "Muted") + msg_lbl.setAlignment(Qt.AlignCenter) + msg_lbl.setWordWrap(True) + msg_lbl.setStyleSheet( + f"color: {status_kind_color(kind)}; background: transparent;" + if not show_title or kind != "muted" + else "" + ) + layout.addWidget(msg_lbl) + + btn = QPushButton(button_text) + btn.setObjectName("FlatButton") + btn.clicked.connect(self.accept) + row = QHBoxLayout() + row.addStretch(1) + row.addWidget(btn) + row.addStretch(1) + layout.addLayout(row) + + def showEvent(self, event) -> None: + super().showEvent(event) + if self._play_sound: + play_ui_alert_sound() + + +def show_theme_alert( + parent=None, + title: str = "提示", + message: str = "", + kind: str = "warn", + button_text: str = "确定", + show_title: bool = True, + play_sound: bool = True, +) -> int: + return ThemeAlertDialog( + parent, + title=title, + message=message, + kind=kind, + button_text=button_text, + show_title=show_title, + play_sound=play_sound, + ).exec_() + + +class SimNotFoundDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("提示") + self.setModal(True) + w, h = WindowGeom.scale_dialog_size(380, 180) + apply_fixed_window(self, w, h) + layout = QVBoxLayout(self) + layout.setContentsMargins(28, 24, 28, 24) + label = QLabel("未检测到飞行模拟器\n\n请确保 FSX 已启动后再试") + label.setAlignment(Qt.AlignCenter) + layout.addWidget(label) + btn = QPushButton("关闭") + btn.setObjectName("FlatButton") + btn.clicked.connect(self.accept) + row = QHBoxLayout() + row.addStretch(1) + row.addWidget(btn) + row.addStretch(1) + layout.addLayout(row) + + +class CollapsibleSection(QFrame): + """可折叠区块:标题栏 + 可收起的内容区(边框随展开状态变化)。""" + + expandedChanged = Signal(bool) + _TITLE_H = 28 + _BODY_H = 136 + + def __init__( + self, + title: str, + content: QWidget, + parent=None, + expanded: bool = False, + body_height: Optional[int] = None, + ): + super().__init__(parent) + self.setObjectName("CollapsibleBox") + self._title = title + self._expandable = False + self._inactive_tooltip = "" + self._content = content + self._body_height = ( + body_height if body_height is not None else UITheme.scaled(self._BODY_H, 112) + ) + + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + self._title_bar = QFrame() + self._title_bar.setObjectName("CollapsibleTitle") + self._title_bar.setFixedHeight(UITheme.scaled(self._TITLE_H, 24)) + title_layout = QHBoxLayout(self._title_bar) + title_layout.setContentsMargins( + UITheme.scaled(10, 8), + 0, + UITheme.scaled(8, 6), + 0, + ) + title_layout.setSpacing(0) + + self._header = QPushButton(self._header_text(expanded)) + self._header.setObjectName("CollapsibleHeader") + self._header.setCheckable(True) + self._header.setFlat(True) + self._header.setAttribute(Qt.WA_AlwaysShowToolTips, True) + self._header.clicked.connect(self._on_header_clicked) + title_layout.addWidget(self._header, 1) + layout.addWidget(self._title_bar) + + self._body = QFrame() + self._body.setObjectName("CollapsibleBody") + self._body.setFixedHeight(self._body_height) + body_layout = QVBoxLayout(self._body) + body_layout.setContentsMargins( + UITheme.scaled(10, 8), + UITheme.scaled(8, 6), + UITheme.scaled(10, 8), + UITheme.scaled(8, 6), + ) + body_layout.setSpacing(0) + body_layout.addWidget(self._content) + layout.addWidget(self._body) + + self.setExpandable(False) + self.setExpanded(expanded) + + @classmethod + def default_body_height(cls) -> int: + return UITheme.scaled(cls._BODY_H, 112) + + def _header_text(self, expanded: bool) -> str: + arrow = "▾" if expanded else "▸" + return f"{arrow} {self._title}" + + def _on_header_clicked(self) -> None: + if not self._expandable: + self._header.setChecked(False) + return + self.setExpanded(self._header.isChecked()) + + def setInactiveTooltip(self, text: str) -> None: + self._inactive_tooltip = (text or "").strip() + self._apply_inactive_tooltip() + + def _apply_inactive_tooltip(self) -> None: + tip = self._inactive_tooltip if not self._expandable else "" + self.setToolTip(tip) + self._title_bar.setToolTip(tip) + self._header.setToolTip(tip) + + def setExpandable(self, expandable: bool) -> None: + self._expandable = bool(expandable) + self._header.setEnabled(self._expandable) + self._apply_inactive_tooltip() + if not self._expandable: + self.setExpanded(False) + + def setExpanded(self, expanded: bool) -> None: + if expanded and not self._expandable: + expanded = False + expanded = bool(expanded) + self._body.setVisible(expanded) + self._content.setVisible(expanded) + self._header.setChecked(expanded) + self._header.setText(self._header_text(expanded)) + self.setProperty("collapsed", "true" if not expanded else "false") + self.style().unpolish(self) + self.style().polish(self) + self.expandedChanged.emit(expanded) + + def isExpanded(self) -> bool: + return self._body.isVisible() + + def setEnabled(self, enabled: bool) -> None: + super().setEnabled(enabled) + if not enabled: + self.setExpandable(False) + self._content.setEnabled(enabled) + + +class ShadowFollowOptionsPanel(QWidget): + """跟飞参数区(嵌入开始监测弹窗)。""" + + @classmethod + def body_height(cls) -> int: + traj_h = UITheme.scaled(28, 24) + gap = UITheme.scaled(8, 6) + options_h = UITheme.scaled(58, 48) + content_h = traj_h + gap + options_h + body_margins = UITheme.scaled(8, 6) + UITheme.scaled(8, 6) + return content_h + body_margins + + def __init__( + self, + parent=None, + lead_s: float = 3.0, + smoke_enabled: bool = True, + trajectory_path: Optional[str] = None, + ): + super().__init__(parent) + self.setAutoFillBackground(False) + from models.shadow_plane import SHADOW_LEAD_DEFAULT_S, SHADOW_LEAD_MAX_S, SHADOW_LEAD_MIN_S + + self._min_s = SHADOW_LEAD_MIN_S + self._max_s = SHADOW_LEAD_MAX_S + self._trajectory_path = (trajectory_path or "").strip() or None + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(UITheme.scaled(8, 6)) + + traj_row = QHBoxLayout() + traj_row.setSpacing(UITheme.scaled(8, 6)) + traj_lbl = QLabel("轨迹文件") + traj_lbl.setObjectName("Muted") + traj_lbl.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + traj_row.addWidget(traj_lbl) + self._path_edit = QLineEdit() + self._path_edit.setReadOnly(True) + self._path_edit.setPlaceholderText("请选择 CSV 轨迹") + if self._trajectory_path: + self._apply_trajectory_display(self._trajectory_path) + traj_row.addWidget(self._path_edit, 1) + btn_browse = QPushButton("浏览") + btn_browse.setObjectName("IconSm") + btn_browse.clicked.connect(self._browse_trajectory) + traj_row.addWidget(btn_browse) + layout.addLayout(traj_row) + + options_row = QHBoxLayout() + options_row.setSpacing(UITheme.scaled(12, 8)) + + initial = float(lead_s if lead_s is not None else SHADOW_LEAD_DEFAULT_S) + initial = max(self._min_s, min(self._max_s, initial)) + + lead_cell = QWidget() + lead_cell.setAutoFillBackground(False) + lead_layout = QVBoxLayout(lead_cell) + lead_layout.setContentsMargins(0, 0, 0, 0) + lead_layout.setSpacing(UITheme.scaled(6, 4)) + lead_layout.setAlignment(Qt.AlignCenter) + self._value_label = QLabel(f"领先时间:{initial:.1f} 秒") + self._value_label.setObjectName("Muted") + self._value_label.setAlignment(Qt.AlignCenter) + lead_layout.addWidget(self._value_label, 0, Qt.AlignCenter) + self._slider = RoundSlider( + from_=self._min_s, + to=self._max_s, + value=initial, + resolution=0.1, + length=UITheme.scaled(168, 128), + ) + self._slider.valueChanged.connect(self._on_slide) + lead_layout.addWidget(self._slider, 0, Qt.AlignCenter) + options_row.addWidget(lead_cell, 1) + + smoke_cell = QWidget() + smoke_cell.setAutoFillBackground(False) + smoke_layout = QVBoxLayout(smoke_cell) + smoke_layout.setContentsMargins(0, 0, 0, 0) + smoke_layout.setSpacing(UITheme.scaled(6, 4)) + smoke_layout.setAlignment(Qt.AlignCenter) + smoke_lbl = QLabel("拉烟") + smoke_lbl.setObjectName("Muted") + smoke_lbl.setAlignment(Qt.AlignCenter) + smoke_layout.addWidget(smoke_lbl, 0, Qt.AlignCenter) + self._smoke_toggle = SlideToggle(checked=bool(smoke_enabled)) + smoke_layout.addWidget(self._smoke_toggle, 0, Qt.AlignCenter) + options_row.addWidget(smoke_cell, 1) + layout.addLayout(options_row) + + def _apply_trajectory_display(self, path: str) -> None: + p = Path(path) + self._path_edit.setText(p.name) + self._path_edit.setToolTip(str(p)) + + def _browse_trajectory(self) -> None: + from models.shadow_plane import default_trajectory_dir + + initial_dir = default_trajectory_dir() + initial_dir.mkdir(parents=True, exist_ok=True) + start_dir = str(initial_dir) + if self._trajectory_path: + parent = Path(self._trajectory_path).parent + if parent.is_dir(): + start_dir = str(parent) + path, _ = QFileDialog.getOpenFileName( + self.window(), + "选择跟飞轨迹文件", + start_dir, + "CSV 轨迹 (*.csv);;所有文件 (*.*)", + ) + if not path: + return + self._trajectory_path = path + self._apply_trajectory_display(path) + + def _on_slide(self, value: float) -> None: + self._value_label.setText(f"领先时间:{value:.1f} 秒") + + def lead_s(self) -> float: + return max(self._min_s, min(self._max_s, float(self._slider.value()))) + + def smoke_enabled(self) -> bool: + return bool(self._smoke_toggle.isChecked()) + + def trajectory_path(self) -> Optional[str]: + return self._trajectory_path + + +class ShadowFollowSettingsPanel(QWidget): + """跟飞设定折叠区。""" + + _INACTIVE_TOOLTIP = "跟飞模式未开启" + + def __init__( + self, + parent=None, + lead_s: float = 3.0, + smoke_enabled: bool = True, + trajectory_path: Optional[str] = None, + expanded: bool = False, + ): + super().__init__(parent) + self.setAutoFillBackground(False) + self._body_reserve_h = ShadowFollowOptionsPanel.body_height() + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + self._shadow_panel = ShadowFollowOptionsPanel( + self, + lead_s=lead_s, + smoke_enabled=smoke_enabled, + trajectory_path=trajectory_path, + ) + self._section = CollapsibleSection( + "Shadow Plane 设定", + self._shadow_panel, + expanded=expanded, + body_height=self._body_reserve_h, + ) + self._section.setInactiveTooltip(self._INACTIVE_TOOLTIP) + self._section.expandedChanged.connect(self._sync_body_reserve) + self._body_reserve = QWidget() + self._body_reserve.setAutoFillBackground(False) + layout.addWidget(self._section) + layout.addWidget(self._body_reserve) + self.set_expandable(expanded, auto_expand=expanded) + self._sync_body_reserve(self._section.isExpanded()) + + def _sync_body_reserve(self, expanded: bool) -> None: + self._body_reserve.setFixedHeight(0 if expanded else self._body_reserve_h) + + def set_expandable(self, expandable: bool, auto_expand: bool = False) -> None: + self._section.setExpandable(expandable) + if expandable and auto_expand: + self._section.setExpanded(True) + elif not expandable: + self._section.setExpanded(False) + + def setEnabled(self, enabled: bool) -> None: + super().setEnabled(enabled) + if not enabled: + self.set_expandable(False) + self._shadow_panel.setEnabled(enabled) + + @property + def section(self) -> CollapsibleSection: + return self._section + + @property + def shadow_panel(self) -> ShadowFollowOptionsPanel: + return self._shadow_panel + + +class StartMonitoringDialog(QDialog): + """开始监测设置;Accepted 时 result_value 为 dict。""" + + _DIALOG_W = 420 + _DIALOG_H = 500 + + def __init__( + self, + parent=None, + session_mode: str = "freeflight", + cold_cabin_enabled: bool = False, + shadow_follow_enabled: bool = False, + lead_s: float = 3.0, + smoke_enabled: bool = True, + trajectory_path: Optional[str] = None, + ): + super().__init__(parent) + self.result_value: Optional[dict] = None + self._session_mode = session_mode + self.setWindowTitle("开始监测") + self.setModal(True) + self._apply_fixed_size() + + row_gap = UITheme.scaled(14, 10) + layout = QVBoxLayout(self) + layout.setContentsMargins( + UITheme.scaled(28, 12), + UITheme.scaled(24, 10), + UITheme.scaled(28, 12), + UITheme.scaled(24, 10), + ) + layout.setSpacing(row_gap) + + title = QLabel("启动参数设置") + title.setObjectName("DialogTitle") + title.setAlignment(Qt.AlignCenter) + layout.addWidget(title) + + toggles_row = QWidget() + toggles_row.setAutoFillBackground(False) + toggles_layout = QHBoxLayout(toggles_row) + toggles_layout.setContentsMargins(0, 0, 0, 0) + toggles_layout.setSpacing(UITheme.scaled(12, 8)) + + cold_cell = QWidget() + cold_cell.setAutoFillBackground(False) + cold_layout = QVBoxLayout(cold_cell) + cold_layout.setContentsMargins( + UITheme.scaled(8, 6), + UITheme.scaled(8, 6), + UITheme.scaled(8, 6), + UITheme.scaled(8, 6), + ) + cold_layout.setSpacing(UITheme.scaled(6, 4)) + cold_layout.setAlignment(Qt.AlignCenter) + cold_lbl = QLabel("开启冷舱") + cold_lbl.setObjectName("Muted") + cold_lbl.setAlignment(Qt.AlignCenter) + cold_layout.addWidget(cold_lbl, 0, Qt.AlignCenter) + self._cold_toggle = SlideToggle(checked=bool(cold_cabin_enabled)) + cold_layout.addWidget(self._cold_toggle, 0, Qt.AlignCenter) + toggles_layout.addWidget(cold_cell, 1) + + shadow_cell = QWidget() + shadow_cell.setAutoFillBackground(False) + shadow_layout = QVBoxLayout(shadow_cell) + shadow_layout.setContentsMargins( + UITheme.scaled(8, 6), + UITheme.scaled(8, 6), + UITheme.scaled(8, 6), + UITheme.scaled(8, 6), + ) + shadow_layout.setSpacing(UITheme.scaled(6, 4)) + shadow_layout.setAlignment(Qt.AlignCenter) + shadow_lbl = QLabel("跟飞") + shadow_lbl.setObjectName("Muted") + shadow_lbl.setAlignment(Qt.AlignCenter) + shadow_layout.addWidget(shadow_lbl, 0, Qt.AlignCenter) + shadow_on = bool(shadow_follow_enabled) and session_mode != "mission_mp" + self._shadow_toggle = SlideToggle(checked=shadow_on) + self._shadow_toggle.toggled.connect(self._on_shadow_toggled) + shadow_layout.addWidget(self._shadow_toggle, 0, Qt.AlignCenter) + toggles_layout.addWidget(shadow_cell, 1) + layout.addWidget(toggles_row) + + self._shadow_settings = ShadowFollowSettingsPanel( + self, + lead_s=lead_s, + smoke_enabled=smoke_enabled, + trajectory_path=trajectory_path, + expanded=shadow_on, + ) + layout.addWidget(self._shadow_settings) + self._apply_shadow_toggle(shadow_on, auto_expand=shadow_on) + + hw_row = QHBoxLayout() + hw_row.setSpacing(UITheme.scaled(8, 6)) + hw_row.addStretch(1) + self._btn_hw = QPushButton("手柄设定") + self._btn_hw.setObjectName("FlatButton") + self._btn_hw.clicked.connect(self._open_hardware_settings) + hw_row.addWidget(self._btn_hw) + hw_row.addStretch(1) + layout.addLayout(hw_row) + + start_mode = QWidget() + start_mode.setAutoFillBackground(False) + start_mode_layout = QVBoxLayout(start_mode) + start_mode_layout.setContentsMargins( + UITheme.scaled(8, 6), + UITheme.scaled(8, 6), + UITheme.scaled(8, 6), + UITheme.scaled(8, 6), + ) + start_mode_layout.setSpacing(UITheme.scaled(10, 6)) + self._mode_label = QLabel("启动方式") + self._mode_label.setObjectName("Muted") + self._mode_label.setAlignment(Qt.AlignCenter) + start_mode_layout.addWidget(self._mode_label) + + mode_btns = QWidget() + mode_btns.setAutoFillBackground(False) + mode_row = QHBoxLayout(mode_btns) + mode_row.setContentsMargins(0, 0, 0, 0) + mode_row.setSpacing(UITheme.scaled(14, 8)) + mode_row.addStretch(1) + self._btn_current = QPushButton("当前任务") + self._btn_current.setObjectName("FlatButton") + self._btn_current.clicked.connect(lambda: self._choose("current")) + mode_row.addWidget(self._btn_current) + self._btn_reset = QPushButton("重置任务") + self._btn_reset.setObjectName("FlatButton") + self._btn_reset.clicked.connect(lambda: self._choose("reset")) + mode_row.addWidget(self._btn_reset) + mode_row.addStretch(1) + start_mode_layout.addWidget(mode_btns) + layout.addWidget(start_mode) + + self._apply_session_mode(session_mode) + + def _apply_fixed_size(self) -> None: + w, h = WindowGeom.scale_dialog_size(self._DIALOG_W, self._DIALOG_H) + UITheme.apply_scale(w, h, self._DIALOG_W, self._DIALOG_H) + apply_fixed_window(self, w, h) + + def _apply_shadow_toggle(self, checked: bool, auto_expand: bool = False) -> None: + self._shadow_settings.set_expandable(checked, auto_expand=auto_expand) + + def _on_shadow_toggled(self, checked: bool) -> None: + if self._session_mode == "mission_mp": + return + self._apply_shadow_toggle(checked, auto_expand=checked) + + def _apply_session_mode(self, session_mode: str) -> None: + self._session_mode = session_mode + multiplay = session_mode == "mission_mp" + self._btn_reset.setEnabled(not multiplay) + self._shadow_toggle.setEnabled(not multiplay) + self._shadow_settings.setEnabled(not multiplay) + if multiplay: + if self._shadow_toggle.isChecked(): + self._shadow_toggle.setChecked(False) + self._apply_shadow_toggle(False) + else: + on = self._shadow_toggle.isChecked() + self._apply_shadow_toggle(on, auto_expand=on) + + def _open_hardware_settings(self) -> None: + from models.hardware_monitor import reload_stick + from ui.hardware_settings_dialog import show_hardware_monitor_settings + + if show_hardware_monitor_settings(self) is not None: + reload_stick() + + def _choose(self, task: str) -> None: + shadow_on = ( + self._session_mode != "mission_mp" + and self._shadow_toggle.isChecked() + ) + trajectory_path = self._shadow_settings.shadow_panel.trajectory_path() + if shadow_on and not trajectory_path: + show_theme_alert( + self, + message="轨迹文件未选择!", + show_title=False, + ) + return + self.result_value = { + "task": task, + "cold_cabin": bool(self._cold_toggle.isChecked()), + "shadow_follow": shadow_on, + "lead": self._shadow_settings.shadow_panel.lead_s(), + "smoke": self._shadow_settings.shadow_panel.smoke_enabled(), + "trajectory_path": trajectory_path if shadow_on else None, + } + self.accept() + + +def show_start_monitoring( + parent=None, + session_mode: str = "freeflight", + cold_cabin_enabled: bool = False, + shadow_follow_enabled: bool = False, + lead_s: float = 3.0, + smoke_enabled: bool = True, + trajectory_path: Optional[str] = None, +) -> Optional[dict]: + dlg = StartMonitoringDialog( + parent, + session_mode=session_mode, + cold_cabin_enabled=cold_cabin_enabled, + shadow_follow_enabled=shadow_follow_enabled, + lead_s=lead_s, + smoke_enabled=smoke_enabled, + trajectory_path=trajectory_path, + ) + if dlg.exec_() == QDialog.Accepted: + return dlg.result_value + return None + + +def show_sim_not_found(parent=None) -> int: + return SimNotFoundDialog(parent).exec_() + + +class TrajectorySaveDialog(QDialog): + def __init__(self, parent=None, ok: bool = True, path: Optional[str] = None, message: str = ""): + super().__init__(parent) + self.setWindowTitle("保存轨迹") + self.setModal(True) + w, h = WindowGeom.scale_dialog_size(480, 200) + apply_fixed_window(self, w, h) + layout = QVBoxLayout(self) + layout.setContentsMargins(28, 24, 28, 24) + layout.setSpacing(12) + status = QLabel(message or ("保存成功" if ok else "保存失败")) + status.setAlignment(Qt.AlignCenter) + status.setStyleSheet(f"color: {UITheme.SUCCESS if ok else UITheme.ERROR}; background: transparent;") + layout.addWidget(status) + if path: + path_lbl = QLabel(str(path)) + path_lbl.setAlignment(Qt.AlignCenter) + path_lbl.setWordWrap(True) + path_lbl.setStyleSheet(f"color: {UITheme.FG_MUTED}; background: transparent;") + layout.addWidget(path_lbl) + btn = QPushButton("确定") + btn.setObjectName("FlatButton") + btn.clicked.connect(self.accept) + row = QHBoxLayout() + row.addStretch(1) + row.addWidget(btn) + row.addStretch(1) + layout.addLayout(row) + + +def show_trajectory_save_result( + parent=None, + ok: bool = True, + path: Optional[Path] = None, + message: str = "", +) -> int: + text = message or ("保存成功" if ok else "保存失败") + path_text = str(path) if path else "" + return TrajectorySaveDialog(parent, ok=ok, path=path_text or None, message=text).exec_() + + +# --------------------------------------------------------------------------- +# 烟测入口(假数据;运行前请先安装 PySide2) +# --------------------------------------------------------------------------- + + +def _run_smoke() -> int: + from ui import register_default_features + + app = create_app() + shell = AppShell() + w, h = WindowGeom.main_window_size() + UITheme.apply_scale(w, h, 680, 820) + app.setStyleSheet(UITheme.qss()) + register_default_features(shell) + apply_fixed_window(shell, w, h) + + stub = shell.registry.get("mission_stub") + if stub is not None: + stub.secondaryClicked.connect(lambda: shell.show_feature("training_assist")) + + train = shell.registry.get("training_assist") + if train is not None and hasattr(train, "actions"): + # 烟测:开始监测按钮临时用来跳到 stub,验证整页切换 + train.startMonitorClicked.connect(lambda: shell.show_feature("mission_stub")) + + shell.show_feature("training_assist") + shell.show() + + page = shell.current_feature() + hist = ChartHistoryBuffer(TRAINING_CHART) + t0 = time.time() + for i in range(120): + hist.append( + t0 + i * 0.5, + torque=400 + 200 * math.sin(i / 8), + health=98.5 - (i % 40) * 0.05, + ) + if hasattr(page, "set_cht"): + page.set_cht(110) + page.set_torque(560) + page.set_health(98.2) + page.set_laps( + [52.3, 51.8, None, None], + 104.1, + lap_penalties=[5.0, 2.5, None, None], + total_penalty=7.5, + ) + page.set_status("烟测:训练助手假数据|点「开始监测」切 stub", "ok") + page.update_chart(hist) + page.add_lap_marker(t0 + 30, 1) + if hasattr(page, "set_stick_axes"): + page.set_stick_axes( + pitch=0.35, + roll=-0.2, + yaw=0.1, + throttle=0.6, + connected=True, + ) + return app.exec_() + + +if __name__ == "__main__": + from app_run import main + + sys.exit(main()) + diff --git a/app_run.py b/app_run.py new file mode 100644 index 00000000..e484b9dc --- /dev/null +++ b/app_run.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" +模块:app_run +职责:应用入口;启动 Qt 壳与 AppController +依赖:app_frame、ui、app_controller +""" + +from __future__ import annotations + +import sys + + +def main(argv=None) -> int: + """ + 说明:正常启动或 --smoke 冒烟 + 参数: + argv — 命令行参数;默认 sys.argv + 返回: + 进程退出码 + """ + args = list(argv if argv is not None else sys.argv) + if "--smoke" in args: + from app_frame import _run_smoke + + return _run_smoke() + + from app_frame import AppShell, create_app + from ui import register_flow_features + from app_controller import AppController + + app = create_app(args) + shell = AppShell() + # 先只挂自检/登录;训练页(Charts + pygame)推迟到自检窗显示后再建,避免闪窗 + register_flow_features(shell) + controller = AppController(shell) + controller.start() + return app.exec_() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/charts/__init__.py b/charts/__init__.py new file mode 100644 index 00000000..e790cc30 --- /dev/null +++ b/charts/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +""" +模块:charts +职责:图表包公开入口;页面侧优先使用 TrendChart +""" + +from .facade.trend_chart import TrendChart + +__all__ = ["TrendChart"] diff --git a/charts/base/__init__.py b/charts/base/__init__.py new file mode 100644 index 00000000..52d8423b --- /dev/null +++ b/charts/base/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""包占位:charts.base(请从具体子模块导入)。""" diff --git a/charts/base/bar_chart.py b/charts/base/bar_chart.py new file mode 100644 index 00000000..58c38076 --- /dev/null +++ b/charts/base/bar_chart.py @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.base.bar_chart +职责:横向堆叠柱状图(速度带停留,按圈分色) +依赖:BaseChart、theme、models.speed_bands / lap_reference +""" + +from __future__ import annotations + +from typing import List, Optional, Sequence + +from PySide2.QtCore import Qt +from PySide2.QtGui import QColor, QFont + +from charts.base.base_chart import BaseChart +from charts.context import ChartRenderContext +from charts.qtcharts import ( + QBarCategoryAxis, + QBarSet, + QChart, + QHorizontalStackedBarSeries, + QValueAxis, +) +from charts.theme import axis_border_color, axis_muted_color, compare_lap_qcolor, lap_color +from models.chart import ChartConfig, ChartFieldSpec +from models.lap_reference import LAP_COUNT +from models.speed_bands import DEFAULT_SPEED_BANDS +from models.track_compare import CompareStyle + + +class BarChart(BaseChart): + """横向堆叠柱:当前会话 + 可选对比参考层。""" + + chart_type = "bar" + + def __init__(self, config: Optional[ChartConfig] = None) -> None: + super().__init__() + self._config = config + self._labels = [b.label for b in DEFAULT_SPEED_BANDS] + self._dwell_by_lap: List[List[float]] = [ + [0.0] * len(self._labels) for _ in range(LAP_COUNT) + ] + self._ref_dwell_by_lap: Optional[List[List[float]]] = None + self._visible_laps: List[bool] = [True] * LAP_COUNT + self._series: Optional[QHorizontalStackedBarSeries] = None + self._ref_series: Optional[QHorizontalStackedBarSeries] = None + self._bar_sets: List[QBarSet] = [] + self._ref_bar_sets: List[QBarSet] = [] + self._axis_y: Optional[QBarCategoryAxis] = None + self._axis_x: Optional[QValueAxis] = None + self._compare_style = CompareStyle() + self._time_axis_length = 150.0 + + def attach_to_qchart(self, qchart: QChart) -> None: + self._qchart = qchart + # 图例与下方「显示LAPn」重复,且占高度会导致类目标签被压成 "..." + qchart.legend().hide() + + self._series = QHorizontalStackedBarSeries() + self._bar_sets = [] + for lap in range(LAP_COUNT): + bar_set = QBarSet(f"LAP{lap + 1}") + bar_set.setColor(lap_color(lap + 1)) + bar_set.setBorderColor(QColor(0, 0, 0, 0)) + for _ in self._labels: + bar_set.append(0.0) + self._bar_sets.append(bar_set) + self._series.append(bar_set) + qchart.addSeries(self._series) + + self._ref_series = QHorizontalStackedBarSeries() + self._ref_bar_sets = [] + for lap in range(LAP_COUNT): + bar_set = QBarSet(f"R{lap + 1}") + c = compare_lap_qcolor(lap + 1, opacity=float(self._compare_style.opacity)) + bar_set.setColor(c) + border = QColor(c) + border.setAlphaF(min(1.0, c.alphaF() + 0.15)) + bar_set.setBorderColor(border) + for _ in self._labels: + bar_set.append(0.0) + self._ref_bar_sets.append(bar_set) + self._ref_series.append(bar_set) + qchart.addSeries(self._ref_series) + + self._axis_y = QBarCategoryAxis() + self._axis_y.append(self._labels) + self._style_category_axis(self._axis_y) + qchart.addAxis(self._axis_y, Qt.AlignLeft) + self._series.attachAxis(self._axis_y) + self._ref_series.attachAxis(self._axis_y) + + self._axis_x = QValueAxis() + self._style_value_axis(self._axis_x) + # 不显示 X 轴标题,省出垂直空间给类目标签(过矮会变成 "...") + self._axis_x.setTitleVisible(False) + self._axis_x.setLabelFormat("%.0f") + self._axis_x.setRange(0.0, self._time_axis_length) + qchart.addAxis(self._axis_x, Qt.AlignBottom) + self._series.attachAxis(self._axis_x) + self._ref_series.attachAxis(self._axis_x) + + def set_config(self, config: ChartConfig) -> None: + self._config = config + + def set_time_axis_length(self, seconds: float) -> None: + """与折线图 X 轴视窗长度对齐。""" + self._time_axis_length = max(1.0, float(seconds)) + self.apply_axis_ranges(None) + + def set_dwell_by_lap( + self, + dwell_by_lap: Sequence[Sequence[float]], + labels: Optional[Sequence[str]] = None, + ) -> None: + if labels is not None and list(labels) != self._labels: + self._labels = list(labels) + if self._axis_y is not None: + self._axis_y.clear() + self._axis_y.append(self._labels) + for bar_set in self._bar_sets + self._ref_bar_sets: + while bar_set.count() < len(self._labels): + bar_set.append(0.0) + rows: List[List[float]] = [] + for lap in range(LAP_COUNT): + src = dwell_by_lap[lap] if lap < len(dwell_by_lap) else [] + row = [max(0.0, float(src[i])) if i < len(src) else 0.0 for i in range(len(self._labels))] + rows.append(row) + self._dwell_by_lap = rows + self._rebuild() + + def set_compare_dwell_by_lap( + self, + dwell_by_lap: Optional[Sequence[Sequence[float]]], + labels: Optional[Sequence[str]] = None, + ) -> None: + if labels is not None and list(labels) != self._labels: + self._labels = list(labels) + if self._axis_y is not None: + self._axis_y.clear() + self._axis_y.append(self._labels) + for bar_set in self._bar_sets + self._ref_bar_sets: + while bar_set.count() < len(self._labels): + bar_set.append(0.0) + if dwell_by_lap is None: + self._ref_dwell_by_lap = None + else: + rows: List[List[float]] = [] + for lap in range(LAP_COUNT): + src = dwell_by_lap[lap] if lap < len(dwell_by_lap) else [] + row = [ + max(0.0, float(src[i])) if i < len(src) else 0.0 + for i in range(len(self._labels)) + ] + rows.append(row) + self._ref_dwell_by_lap = rows + self._rebuild() + + def set_compare_style(self, style: CompareStyle) -> None: + self._compare_style = style + for lap, bar_set in enumerate(self._ref_bar_sets): + c = compare_lap_qcolor(lap + 1, opacity=float(style.opacity)) + bar_set.setColor(c) + border = QColor(c) + border.setAlphaF(min(1.0, c.alphaF() + 0.15)) + bar_set.setBorderColor(border) + self._rebuild() + + def set_visible_laps(self, visible: Sequence[bool]) -> None: + self._visible_laps = [ + bool(visible[i]) if i < len(visible) else True for i in range(LAP_COUNT) + ] + self._rebuild() + + def update_series(self, ctx: Optional[ChartRenderContext]) -> None: + self._rebuild() + + def apply_axis_ranges(self, ctx: Optional[ChartRenderContext]) -> None: + if self._axis_x is None: + return + # X 轴时长与折线图视窗绑定,不再按柱值峰值自适应 + self._axis_x.setRange(0.0, float(self._time_axis_length)) + + def primary_series(self): + return self._series + + def field_axis(self, field: ChartFieldSpec) -> Optional[QValueAxis]: + return None + + def axis_x(self) -> Optional[QValueAxis]: + return self._axis_x + + def clear(self) -> None: + self._dwell_by_lap = [[0.0] * len(self._labels) for _ in range(LAP_COUNT)] + self._rebuild() + + def _rebuild(self) -> None: + if not self._bar_sets: + return + n = len(self._labels) + for lap, bar_set in enumerate(self._bar_sets): + while bar_set.count() < n: + bar_set.append(0.0) + show = self._visible_laps[lap] if lap < len(self._visible_laps) else True + row = self._dwell_by_lap[lap] if lap < len(self._dwell_by_lap) else [] + for i in range(n): + val = float(row[i]) if show and i < len(row) else 0.0 + bar_set.replace(i, val) + + show_ref = ( + self._ref_dwell_by_lap is not None + and self._compare_style.visible + and bool(self._ref_bar_sets) + ) + if self._ref_series is not None: + self._ref_series.setVisible(show_ref) + for lap, bar_set in enumerate(self._ref_bar_sets): + while bar_set.count() < n: + bar_set.append(0.0) + if not show_ref: + for i in range(n): + bar_set.replace(i, 0.0) + continue + show = self._visible_laps[lap] if lap < len(self._visible_laps) else True + row = self._ref_dwell_by_lap[lap] if lap < len(self._ref_dwell_by_lap) else [] + for i in range(n): + val = float(row[i]) if show and i < len(row) else 0.0 + bar_set.replace(i, val) + self.apply_axis_ranges(None) + + def _style_value_axis(self, axis: QValueAxis) -> None: + axis.setLabelsColor(axis_muted_color()) + axis.setGridLineColor(axis_border_color()) + axis.setLinePenColor(axis_border_color()) + axis.setTitleBrush(axis_muted_color()) + + def _style_category_axis(self, axis: QBarCategoryAxis) -> None: + axis.setLabelsColor(axis_muted_color()) + axis.setGridLineColor(axis_border_color()) + axis.setLinePenColor(axis_border_color()) + # 固定较小字号,避免继承全局 QSS 后行高不够被显示成 "..." + font = QFont(axis.labelsFont()) + font.setPointSize(8) + axis.setLabelsFont(font) diff --git a/charts/base/base_chart.py b/charts/base/base_chart.py new file mode 100644 index 00000000..c77668cd --- /dev/null +++ b/charts/base/base_chart.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.base.base_chart +职责:基底图表抽象接口(系列、轴、清空) +依赖:charts.context、models.chart(TYPE_CHECKING) +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any, Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from PySide2.QtCharts import QtCharts + + QAbstractSeries = QtCharts.QAbstractSeries + QChart = QtCharts.QChart + QValueAxis = QtCharts.QValueAxis + from charts.context import ChartRenderContext + from models.chart import ChartConfig, ChartFieldSpec +else: + QAbstractSeries = Any + QChart = Any + QValueAxis = Any + + +class BaseChart(ABC): + """基底图表:挂到 QChart 上,由 ChartSlot 驱动 render。""" + + chart_type: str = "base" + + def __init__(self) -> None: + self._qchart: Optional[QChart] = None + self._config: Any = None + + @property + def qchart(self) -> Optional[QChart]: + return self._qchart + + def create_qchart(self) -> QChart: + """默认笛卡尔图;雷达等可覆写为 QPolarChart。""" + from charts.qtcharts import QChart as _QChart + + return _QChart() + + @abstractmethod + def attach_to_qchart(self, qchart: QChart) -> None: + """创建系列与轴并挂到 qchart。""" + ... + + @abstractmethod + def set_config(self, config: "ChartConfig") -> None: + """更新字段配置(不一定重建系列)。""" + ... + + @abstractmethod + def update_series(self, ctx: "ChartRenderContext") -> None: + """按上下文刷新系列数据点。""" + ... + + @abstractmethod + def apply_axis_ranges(self, ctx: "ChartRenderContext") -> None: + """按上下文设置轴范围。""" + ... + + @abstractmethod + def primary_series(self) -> Optional[QAbstractSeries]: + """平移/圈标映射用的主系列。""" + ... + + @abstractmethod + def field_axis(self, field: "ChartFieldSpec") -> Optional[QValueAxis]: + """字段对应 Y 轴;不支持时返回 None。""" + ... + + @abstractmethod + def axis_x(self) -> Optional[QValueAxis]: + """时间/类别 X 轴。""" + ... + + @abstractmethod + def clear(self) -> None: + """清空业务系列数据。""" + ... + + @property + def config(self): + return self._config diff --git a/charts/base/line_chart.py b/charts/base/line_chart.py new file mode 100644 index 00000000..4eadd84a --- /dev/null +++ b/charts/base/line_chart.py @@ -0,0 +1,532 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.base.line_chart +职责:双 Y 轴折线基底(业务曲线 + 可选对比层);按槽位显隐字段 +依赖:BaseChart、ChartRenderContext、theme、models.chart +""" + +from __future__ import annotations + +import math +from typing import Dict, Iterable, Optional, Sequence, Set + +from charts.qtcharts import QChart, QLineSeries, QValueAxis +from PySide2.QtCore import Qt, QPointF +from PySide2.QtGui import QFont, QPen +from PySide2.QtWidgets import QGraphicsTextItem + +from charts.base.base_chart import BaseChart +from charts.context import ChartRenderContext +from charts.theme import ( + axis_border_color, + axis_muted_color, + compare_field_qcolor, + compare_pen_style, + field_line_color, + field_pen_style, +) +from models.chart import ( + AxisSide, + ChartConfig, + ChartFieldSpec, + ChartHistoryBuffer, + SERIES_SLOT_SIDES, + series_points, +) +from models.track_compare import CompareStyle +from app_frame import UITheme + + +class LineChart(BaseChart): + """可配置多字段折线;工具栏左右槽指定显示字段与所挂 Y 轴。""" + + chart_type = "line" + + def __init__(self, config: Optional[ChartConfig] = None) -> None: + super().__init__() + from models.chart import TRAINING_CHART + + self._config = config or TRAINING_CHART + self._series: Dict[str, QLineSeries] = {} + self._compare_series: Dict[str, QLineSeries] = {} + self._axes_y: Dict[str, QValueAxis] = {} + self._axis_x: Optional[QValueAxis] = None + self._primary_field = self._config.primary_field() + self._primary_series: Optional[QLineSeries] = None + self._compare_history: Optional[ChartHistoryBuffer] = None + self._compare_style = CompareStyle() + # 字段当前挂载的 Y 轴侧(由左右槽覆盖目录默认 side) + self._field_axis_side: Dict[str, AxisSide] = { + f.id: f.side for f in self._config.fields + } + # 每侧当前绑定的字段 id(槽位驱动) + self._side_field: Dict[str, Optional[str]] = { + AxisSide.LEFT.value: None, + AxisSide.RIGHT.value: None, + } + from models.chart import DEFAULT_SERIES_SLOTS, DEFAULT_SERIES_VISIBLE + + self._visible_ids: Set[str] = { + str(fid) + for fid, vis in zip(DEFAULT_SERIES_SLOTS, DEFAULT_SERIES_VISIBLE) + if vis and fid + } + for i, fid in enumerate(DEFAULT_SERIES_SLOTS): + if i < len(SERIES_SLOT_SIDES) and fid: + side = SERIES_SLOT_SIDES[i] + self._field_axis_side[fid] = side + self._side_field[side.value] = fid + self._cursor_t: Optional[float] = None + self._cursor_lines: list = [] + self._cursor_series: Optional[QLineSeries] = None + self._cursor_label: Optional[QGraphicsTextItem] = None + + def attach_to_qchart(self, qchart: QChart) -> None: + self._qchart = qchart + for field in self._config.fields: + series = QLineSeries() + series.setName(field.id) + pen = QPen(field_line_color(field.color_key, getattr(field, "color", ""))) + pen.setWidthF(float(field.line_width) if field.line_width else 1.8) + pen.setStyle(field_pen_style(getattr(field, "line_style", "") or "")) + series.setPen(pen) + self._series[field.id] = series + qchart.addSeries(series) + if field.id == self._primary_field.id: + self._primary_series = series + + ref = QLineSeries() + ref.setName(f"{field.id}_ref") + ref.setPen(self._compare_pen_for_field(field)) + self._compare_series[field.id] = ref + qchart.addSeries(ref) + + self._axis_x = QValueAxis() + self._style_axis(self._axis_x) + self._axis_x.setTitleText(self._config.x_axis_title) + self._axis_x.setTickInterval(self._config.x_axis_major_sec) + self._axis_x.setMinorTickCount(max(0, int(self._config.x_axis_major_sec) - 1)) + self._axis_x.setLabelFormat("%.0f") + + for side in (AxisSide.LEFT, AxisSide.RIGHT): + axis = QValueAxis() + self._style_axis(axis) + align = Qt.AlignLeft if side == AxisSide.LEFT else Qt.AlignRight + self._axes_y[side.value] = axis + qchart.addAxis(axis, align) + + qchart.addAxis(self._axis_x, Qt.AlignBottom) + for field in self._config.fields: + side = self._field_axis_side.get(field.id, field.side) + y_axis = self._axes_y.get(side.value) + if y_axis is not None: + self._series[field.id].attachAxis(y_axis) + self._compare_series[field.id].attachAxis(y_axis) + self._series[field.id].attachAxis(self._axis_x) + self._compare_series[field.id].attachAxis(self._axis_x) + + self._apply_visibility() + self._refresh_axis_meta() + + def set_config(self, config: ChartConfig) -> None: + self._config = config + self._primary_field = config.primary_field() + self._primary_series = self._series.get(self._primary_field.id) + self._refresh_axis_meta() + + def _bind_series_to_side(self, field_id: str, side: AxisSide) -> None: + """将业务/对比系列改挂到指定 Y 轴;仅卸已附着的 Y 轴,避免 Qt 报 Axis not attached。""" + series = self._series.get(field_id) + ref = self._compare_series.get(field_id) + target = self._axes_y.get(side.value) + if series is None or target is None: + return + if self._field_axis_side.get(field_id) == side: + return + + y_axes = set(self._axes_y.values()) + + def _rebind(s) -> None: + if s is None: + return + try: + attached = list(s.attachedAxes()) + except Exception: + attached = [] + for axis in attached: + if axis in y_axes: + s.detachAxis(axis) + s.attachAxis(target) + + _rebind(series) + _rebind(ref) + self._field_axis_side[field_id] = side + + def set_slot_selection(self, slots: Sequence[tuple]) -> None: + """ + 说明:按左右槽绑定字段到对应 Y 轴并更新显隐 + 参数: + slots — [(field_id, visible), ...],索引 0=左轴,1=右轴 + 注意: + 两槽选同一字段时左侧优先;不立刻改轴范围(交由 render) + """ + visible_ids: Set[str] = set() + side_field: Dict[str, Optional[str]] = { + AxisSide.LEFT.value: None, + AxisSide.RIGHT.value: None, + } + used: Set[str] = set() + for i, item in enumerate(slots): + if i >= len(SERIES_SLOT_SIDES): + break + fid, vis = item[0], bool(item[1]) + fid = str(fid or "") + side = SERIES_SLOT_SIDES[i] + if not fid or fid not in self._series: + continue + if fid in used: + # 同字段不可同时占两侧;跳过后出现的槽 + continue + used.add(fid) + self._bind_series_to_side(fid, side) + side_field[side.value] = fid + if vis: + visible_ids.add(fid) + self._side_field = side_field + self._visible_ids = visible_ids + self._apply_visibility() + # 主系列优先左轴槽 + left_id = side_field.get(AxisSide.LEFT.value) + primary = None + if left_id and left_id in visible_ids: + primary = self._config.field_by_id(left_id) + if primary is None: + right_id = side_field.get(AxisSide.RIGHT.value) + if right_id and right_id in visible_ids: + primary = self._config.field_by_id(right_id) + if primary is not None: + self._primary_field = primary + self._primary_series = self._series.get(primary.id) + else: + self._primary_series = None + + def set_visible_fields(self, field_ids: Iterable[str]) -> None: + """仅更新显隐(不改轴绑定);优先用 set_slot_selection。""" + self._visible_ids = {str(x) for x in field_ids if x} + self._apply_visibility() + primary = None + left_id = self._side_field.get(AxisSide.LEFT.value) + if left_id and left_id in self._visible_ids: + primary = self._config.field_by_id(left_id) + if primary is None: + for field in self._config.fields: + if field.id in self._visible_ids: + primary = field + break + if primary is not None: + self._primary_field = primary + self._primary_series = self._series.get(primary.id) + else: + self._primary_series = None + + def set_compare_history(self, history: Optional[ChartHistoryBuffer]) -> None: + """绑定/清除对比历史缓冲。""" + self._compare_history = history + if history is None: + self._clear_compare_points() + + def set_compare_style(self, style: CompareStyle) -> None: + """更新对比层笔样式与显隐。""" + self._compare_style = style + self._apply_compare_pens() + self._apply_visibility() + + def _apply_visibility(self) -> None: + for field_id, series in self._series.items(): + try: + series.setVisible(field_id in self._visible_ids) + except Exception: + pass + show_ref = bool(self._compare_style.visible) and self._compare_history is not None + for field_id, series in self._compare_series.items(): + try: + series.setVisible(show_ref and field_id in self._visible_ids) + except Exception: + pass + + def _visible_fields(self) -> list: + return [f for f in self._config.fields if f.id in self._visible_ids] + + def is_field_visible(self, field_id: str) -> bool: + """字段当前是否由工具栏勾选显示。""" + return str(field_id) in self._visible_ids + + def _refresh_axis_meta(self) -> None: + """按左右槽绑定字段刷新轴标题与范围;无绑定时隐藏该侧轴。""" + for side in (AxisSide.LEFT, AxisSide.RIGHT): + axis = self._axes_y.get(side.value) + if axis is None: + continue + fid = self._side_field.get(side.value) + field = self._config.field_by_id(fid) if fid else None + if field is None or fid not in self._visible_ids: + try: + axis.setVisible(False) + except Exception: + pass + continue + ymin, ymax = field.ylim() + if not math.isfinite(ymin) or not math.isfinite(ymax) or ymax <= ymin: + continue + try: + axis.setVisible(True) + axis.setRange(ymin, ymax) + except Exception: + pass + axis.setTitleText(field.axis_title or "") + + def field_axis(self, field: ChartFieldSpec) -> Optional[QValueAxis]: + """按槽位实际挂载侧取 Y 轴(覆盖目录默认 side)。""" + side = self._field_axis_side.get(field.id, field.side) + return self._axes_y.get(side.value) + + def _compare_pen_for_field(self, field: ChartFieldSpec) -> QPen: + """历史层:原字段色降饱和 + 固定线宽/虚线/透明度。""" + pen = QPen( + compare_field_qcolor( + field.color_key, + getattr(field, "color", ""), + opacity=float(self._compare_style.opacity), + ) + ) + pen.setWidthF(float(self._compare_style.line_width)) + pen.setStyle(compare_pen_style(self._compare_style)) + return pen + + def _apply_compare_pens(self) -> None: + fields_by_id = {f.id: f for f in self._config.fields} + for field_id, series in self._compare_series.items(): + field = fields_by_id.get(field_id) + if field is None: + continue + series.setPen(self._compare_pen_for_field(field)) + + def _style_axis(self, axis: QValueAxis) -> None: + axis.setLabelsColor(axis_muted_color()) + axis.setGridLineColor(axis_border_color()) + axis.setLinePenColor(axis_border_color()) + axis.setTitleBrush(axis_muted_color()) + + def _replace_series(self, series: QLineSeries, xs, ys) -> None: + try: + series.clear() + points = [QPointF(x, y) for x, y in series_points(xs, ys)] + if points: + series.replace(points) + except Exception: + pass + + def update_series(self, ctx: ChartRenderContext) -> None: + if not ctx.xs: + for field_id, series in self._series.items(): + if field_id in self._visible_ids: + series.clear() + else: + for field in self._config.fields: + series = self._series.get(field.id) + if series is None: + continue + # 隐藏系列跳过写点,避免 clear/replace 与轴范围联动崩 Qt Charts + if field.id not in self._visible_ids: + continue + self._replace_series( + series, + ctx.xs, + ctx.field_data.get(field.id, []), + ) + self._update_compare_in_view(float(ctx.view_start), float(ctx.view_end)) + + def _update_compare_in_view(self, view_start: float, view_end: float) -> None: + if self._compare_history is None or not self._compare_style.visible: + self._clear_compare_points() + return + xs, field_data = self._compare_history.slice_view(view_start, view_end) + for field in self._config.fields: + series = self._compare_series.get(field.id) + if series is None: + continue + if field.id not in self._visible_ids: + try: + series.setVisible(False) + except Exception: + pass + continue + try: + series.setVisible(True) + except Exception: + pass + self._replace_series(series, xs, field_data.get(field.id, [])) + + def _clear_compare_points(self) -> None: + for series in self._compare_series.values(): + series.clear() + try: + series.setVisible(False) + except Exception: + pass + + def apply_axis_ranges(self, ctx: ChartRenderContext) -> None: + if self._axis_x is not None: + start = float(ctx.view_start) + end = float(ctx.view_end) + if not math.isfinite(start) or not math.isfinite(end) or end <= start: + window = self._config.view_window_s if self._config else 150.0 + start, end = 0.0, float(window) + try: + self._axis_x.setRange(start, end) + except Exception: + pass + self._refresh_axis_meta() + + def apply_empty_axis_ranges(self) -> None: + """无数据时轴钉在 [0, window],并清空对比点。""" + window = self._config.view_window_s + if self._axis_x is not None: + try: + self._axis_x.setRange(0.0, window) + except Exception: + pass + self._refresh_axis_meta() + self._update_compare_in_view(0.0, float(window)) + + def side_field_ids(self) -> Dict[str, Optional[str]]: + """当前左右槽绑定的字段 id。""" + return dict(self._side_field) + + def set_cursor(self, t_rel: Optional[float], lines: Optional[Sequence[str]] = None) -> None: + """设置全局游标相对时间与浮窗文案;None 清除。""" + self._cursor_t = None if t_rel is None else float(t_rel) + self._cursor_lines = list(lines or []) + + def clear_cursor(self) -> None: + self._cursor_t = None + self._cursor_lines = [] + self._clear_cursor_graphics() + + def _clear_cursor_graphics(self) -> None: + qchart = self._qchart + if self._cursor_series is not None and qchart is not None: + try: + qchart.removeSeries(self._cursor_series) + except Exception: + pass + self._cursor_series = None + if self._cursor_label is not None and qchart is not None: + scene = qchart.scene() + if scene is not None: + try: + scene.removeItem(self._cursor_label) + except Exception: + pass + self._cursor_label = None + + def compare_history(self) -> Optional[ChartHistoryBuffer]: + return self._compare_history + + def primary_series(self) -> Optional[QLineSeries]: + # 仅返回当前可见且有点的主系列,避免圈标/平移 mapToPosition 踩 + if self._primary_series is None: + return None + try: + if not self._primary_series.isVisible() or self._primary_series.count() <= 0: + return None + except Exception: + return None + return self._primary_series + + def _cursor_anchor_series(self) -> Optional[QLineSeries]: + """游标定位锚点:本场主系列优先,否则同字段对比系列。""" + primary = self.primary_series() + if primary is not None: + return primary + if self._primary_field is None: + return None + ref = self._compare_series.get(self._primary_field.id) + if ref is None: + return None + try: + if not ref.isVisible() or ref.count() <= 0: + return None + except Exception: + return None + return ref + + def update_cursor(self, ctx: Optional[ChartRenderContext]) -> None: + """按当前视窗重绘游标竖线与浮窗。""" + self._clear_cursor_graphics() + if ctx is None or ctx.qchart is None or self._cursor_t is None: + return + t = float(self._cursor_t) + if not math.isfinite(t): + return + if t < float(ctx.view_start) - 1e-6 or t > float(ctx.view_end) + 1e-6: + return + axis = self.field_axis(self._primary_field) if self._primary_field else None + axis_x = self._axis_x + if axis is None or axis_x is None or self._primary_field is None: + return + ymin, ymax = self._primary_field.ylim() + line = QLineSeries() + pen = QPen(Qt.white) + pen.setWidthF(1.2) + pen.setStyle(Qt.SolidLine) + line.setPen(pen) + line.append(t, ymin) + line.append(t, ymax) + ctx.qchart.addSeries(line) + line.attachAxis(axis_x) + line.attachAxis(axis) + self._cursor_series = line + + if not self._cursor_lines: + return + anchor = self._cursor_anchor_series() + if anchor is not None: + pos = ctx.qchart.mapToPosition(QPointF(t, ymax), anchor) + else: + # 无系列点时按绘图区与 X 轴范围推算像素位置 + plot = ctx.qchart.plotArea() + span = max(1e-6, float(ctx.view_end) - float(ctx.view_start)) + ratio = (t - float(ctx.view_start)) / span + pos = QPointF(plot.left() + ratio * plot.width(), plot.top()) + if not math.isfinite(pos.x()) or not math.isfinite(pos.y()): + return + plot = ctx.qchart.plotArea() + px = min(max(pos.x(), plot.left()), plot.right()) + text = "
".join(self._cursor_lines) + label = QGraphicsTextItem() + label.setHtml( + f"
{text}
" + ) + font = QFont(UITheme.FONT_FAMILY_TECH) + font.setPixelSize(10) + label.setFont(font) + scene = ctx.qchart.scene() + if scene is None: + return + scene.addItem(label) + rect = label.boundingRect() + lx = px + 8 + if lx + rect.width() > plot.right(): + lx = px - rect.width() - 8 + ly = plot.top() + 4 + label.setPos(lx, ly) + self._cursor_label = label + + def axis_x(self) -> Optional[QValueAxis]: + return self._axis_x + + def clear(self) -> None: + for series in self._series.values(): + series.clear() diff --git a/charts/container.py b/charts/container.py new file mode 100644 index 00000000..fc42b7a4 --- /dev/null +++ b/charts/container.py @@ -0,0 +1,358 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.container +职责:图表槽位 / 二级布局 / 页面级显示区 +依赖:BaseChart、蒙版、PanChartView、models.chart +""" + +from __future__ import annotations + +import math +from typing import List, Optional + +from PySide2.QtCore import QMargins, Qt +from PySide2.QtGui import QPen +from PySide2.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget + +from app_frame import UITheme +from charts.base.base_chart import BaseChart +from charts.base.bar_chart import BarChart +from charts.base.line_chart import LineChart +from charts.context import ChartRenderContext +from charts.overlay.base_overlay import ChartOverlay +from charts.overlay.registry import create_overlays +from charts.theme import chart_surface_color, chart_well_border_color, chart_well_color +from charts.widgets.pan_chart_view import PanChartView +from models.chart import ( + ChartConfig, + ChartHistoryBuffer, + relative_times, + view_bounds, +) +from models.data_bridge import ChartOverlayConfig + + +class ChartSlot(QWidget): + """单槽:一个 QChart + 基底图表 + 蒙版列表。""" + + def __init__( + self, + base_chart: BaseChart, + overlays: Optional[List[ChartOverlay]] = None, + overlay_config: Optional[ChartOverlayConfig] = None, + enable_pan: bool = True, + parent=None, + ): + super().__init__(parent) + self.base_chart = base_chart + self.overlays: List[ChartOverlay] = overlays if overlays is not None else create_overlays(overlay_config) + for ov in self.overlays: + ov.bind_base(base_chart) + + self.live_mode = True + self.view_end: Optional[float] = None + self._history = None + self._time_base_override = None + self._dragging = False + self._drag_start_x: Optional[float] = None + self._drag_start_view_end: Optional[float] = None + self._enable_pan = enable_pan + self._rendering = False + self._render_pending = False + + surface = chart_surface_color() + well = chart_well_color() + well_pen = QPen(chart_well_border_color(), 1.0) + well_pen.setJoinStyle(Qt.MiterJoin) + + self.qchart = base_chart.create_qchart() + self.qchart.legend().hide() + self.qchart.setBackgroundBrush(surface) + # 绘图区使用预置井区色 + 边框 + self.qchart.setPlotAreaBackgroundBrush(well) + self.qchart.setPlotAreaBackgroundVisible(True) + if hasattr(self.qchart, "setPlotAreaBackgroundPen"): + self.qchart.setPlotAreaBackgroundPen(well_pen) + self.qchart.setBackgroundRoundness(0) + self.qchart.setMargins(QMargins(0, 0, 0, 0)) + base_chart.attach_to_qchart(self.qchart) + + self.view = PanChartView( + self.qchart, + self._on_pan_press if enable_pan else lambda _p: None, + self._on_pan_move if enable_pan else lambda _p: None, + self._on_pan_release if enable_pan else lambda: None, + self, + ) + self.view.setStyleSheet(f"background: {UITheme.SURFACE};") + + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(self.view, 1) + + def lap_overlay(self): + """返回槽内 LAP 蒙版实例(若有)。""" + from charts.overlay.lap_marker_overlay import LapMarkerOverlay + + for ov in self.overlays: + if isinstance(ov, LapMarkerOverlay): + return ov + return None + + def bind_history(self, history) -> None: + """绑定时间序列缓冲(不立刻渲染)。""" + self._history = history + + def set_time_base_override(self, history) -> None: + """可选:用对比历史等覆盖视窗时间基准(拖时间轴时与游标对齐)。""" + self._time_base_override = history + + def set_config(self, config: ChartConfig) -> None: + """转发基底图表配置。""" + self.base_chart.set_config(config) + + def _view_window_s(self) -> float: + cfg = self.base_chart.config + return cfg.view_window_s if cfg else 150.0 + + def _live_history_len(self) -> int: + if self._history is None: + return 0 + try: + return len(list(self._history.timestamps)) + except Exception: + return 0 + + def _render_history(self): + """渲染时间基准:显式覆盖 > 本场缓冲 > 可见的折线对比历史。""" + override = getattr(self, "_time_base_override", None) + if override is not None: + try: + if len(list(override.timestamps)) > 0: + return override + except Exception: + pass + if self._live_history_len() > 0: + return self._history + if isinstance(self.base_chart, LineChart): + style = getattr(self.base_chart, "_compare_style", None) + if style is not None and not bool(getattr(style, "visible", True)): + return None + cmp_hist = self.base_chart.compare_history() + if cmp_hist is not None and len(cmp_hist) > 0: + return cmp_hist + return None + + def _history_slice(self, view_start: float, view_end: float, history=None): + """按视窗切缓冲;兼容旧式 getattr 字段缓冲。""" + buf = self._history if history is None else history + config = self.base_chart.config + if buf is None: + return [], {field.id: [] for field in config.fields} + if isinstance(buf, ChartHistoryBuffer): + return buf.slice_view(view_start, view_end) + times = relative_times(list(buf.timestamps)) + xs: list[float] = [] + out: dict[str, list[float]] = {field.id: [] for field in config.fields} + for i, x in enumerate(times): + if x < view_start or x > view_end: + continue + xs.append(x) + for field in config.fields: + legacy = getattr(buf, field.id, None) + if legacy is None and field.id == "torque": + legacy = getattr(buf, "torque", None) + if legacy is None and field.id == "health": + legacy = getattr(buf, "health_pct", None) + if legacy is not None and i < len(legacy): + out[field.id].append(float(legacy[i])) + else: + out[field.id].append(float("nan")) + return xs, out + + def _build_context(self, view_start: float, view_end: float, xs, field_data) -> ChartRenderContext: + return ChartRenderContext( + history=self._history, + view_start=view_start, + view_end=view_end, + view_window_s=self._view_window_s(), + xs=xs, + field_data=field_data, + qchart=self.qchart, + axis_x=self.base_chart.axis_x(), + live_mode=self.live_mode, + config=self.base_chart.config, + ) + + def render(self) -> None: + """按当前 live/view_end 重绘基底与蒙版。""" + if getattr(self, "_rendering", False): + self._render_pending = True + return + self._rendering = True + self._render_pending = False + try: + self._render_impl() + finally: + self._rendering = False + if self._render_pending: + self._render_pending = False + self.render() + + def _render_impl(self) -> None: + # 柱状图不依赖趋势缓冲(速度带可选附图) + if isinstance(self.base_chart, BarChart): + self.base_chart.update_series(None) + self.base_chart.apply_axis_ranges(None) + return + + # 先清蒙版再改轴范围,避免 QAreaSeries 仍挂在轴上时 setRange 导致 Qt Charts 崩进程 + for ov in self.overlays: + ov.clear(self.qchart) + + history = self._render_history() + if history is None: + self.base_chart.clear() + if isinstance(self.base_chart, LineChart): + self.base_chart.apply_empty_axis_ranges() + window = self._view_window_s() + ctx = self._build_context(0.0, float(window), [], {}) + self.base_chart.update_cursor(ctx) + return + + times = relative_times(list(history.timestamps)) + t_max = times[-1] if times else 0.0 + if not math.isfinite(t_max): + t_max = 0.0 + view_start, view_end = view_bounds(t_max, self.view_end, self._view_window_s()) + + # 仅本场缓冲写入主系列;对比-only 时主系列留空,由 update_series 画对比层 + if self._live_history_len() > 0 and history is self._history: + xs, field_data = self._history_slice(view_start, view_end, history=self._history) + else: + xs, field_data = [], {} + + ctx = self._build_context(view_start, view_end, xs, field_data) + self.base_chart.update_series(ctx) + self.base_chart.apply_axis_ranges(ctx) + # 本场无点时仍更新游标(对比系列可作锚点) + if xs: + for ov in self.overlays: + ov.update(ctx) + if isinstance(self.base_chart, LineChart): + self.base_chart.update_cursor(ctx) + + def update_history(self, history, reset_live: bool = False) -> None: + """更新缓冲并渲染;live 或显式复位时钉回最新端。""" + self._history = history + if reset_live or self.live_mode: + self.view_end = None + self.render() + + def _on_pan_press(self, pos) -> None: + primary = self.base_chart.primary_series() + if (primary is None or primary.count() <= 0) and isinstance(self.base_chart, LineChart): + primary = self.base_chart._cursor_anchor_series() + if primary is None or primary.count() <= 0: + return + x = self.qchart.mapToValue(pos, primary).x() + if math.isnan(x) or not math.isfinite(x): + return + self._dragging = True + self._drag_start_x = float(x) + self._drag_start_view_end = self.view_end + + def _on_pan_move(self, pos) -> None: + if not self._dragging or self._drag_start_x is None: + return + history = self._render_history() + if history is None: + return + primary = self.base_chart.primary_series() + if (primary is None or primary.count() <= 0) and isinstance(self.base_chart, LineChart): + primary = self.base_chart._cursor_anchor_series() + if primary is None or primary.count() <= 0: + return + x = self.qchart.mapToValue(pos, primary).x() + if math.isnan(x) or not math.isfinite(x): + return + dx = float(x) - self._drag_start_x + times = relative_times(list(history.timestamps)) + if not times: + return + base_end = self._drag_start_view_end + if base_end is None: + base_end = times[-1] + window = self._view_window_s() + t_max = times[-1] + new_end = float(base_end) - dx + # t_max < window 时只能钉在 t_max,禁止把 view_end 抬到 window 造成空切片 + min_end = min(window, t_max) if t_max > 0 else 0.0 + new_end = max(min_end, min(new_end, t_max)) + self.live_mode = False + self.view_end = new_end + self.render() + + def _on_pan_release(self) -> None: + self._dragging = False + self._drag_start_x = None + + +class ChartPane(QWidget): + """二级容器:single / horizontal / vertical 布局。""" + + def __init__(self, layout_mode: str = "single", parent=None): + super().__init__(parent) + self.layout_mode = layout_mode + self._slots: List[ChartSlot] = [] + if layout_mode == "horizontal": + self._layout = QHBoxLayout(self) + else: + self._layout = QVBoxLayout(self) + self._layout.setContentsMargins(0, 0, 0, 0) + self._layout.setSpacing(UITheme.scaled(4, 2)) + + def add_slot(self, slot: ChartSlot, stretch: int = 1) -> None: + self._slots.append(slot) + self._layout.addWidget(slot, stretch) + + def add_widget(self, widget: QWidget, stretch: int = 1) -> None: + self._layout.addWidget(widget, stretch) + + +class ChartDisplayArea(QWidget): + """页面级总容器:上行趋势图,下行航迹图 + 操纵输入。""" + + def __init__( + self, + main_slot: ChartSlot, + bottom_slots: Optional[List[ChartSlot]] = None, + bottom_widgets: Optional[List[QWidget]] = None, + parent=None, + ): + super().__init__(parent) + self.main_slot = main_slot + self.bottom_slots = list(bottom_slots or []) + + root = QVBoxLayout(self) + root.setContentsMargins(0, 0, 0, 0) + root.setSpacing(UITheme.scaled(6, 3)) + + root.addWidget(main_slot, 3) + + widgets = list(bottom_widgets) if bottom_widgets is not None else list(self.bottom_slots) + if widgets: + bottom_pane = ChartPane("horizontal") + bottom_pane.setMinimumHeight(UITheme.scaled(200, 170)) + for w in widgets: + w.setVisible(True) + if isinstance(w, ChartSlot): + bottom_pane.add_slot(w, 1) + else: + bottom_pane.add_widget(w, 1) + root.addWidget(bottom_pane, 3) + + def update_all(self, history) -> None: + """刷新主槽趋势;附图由专用 API 更新。""" + self.main_slot.update_history(history, reset_live=False) diff --git a/charts/context.py b/charts/context.py new file mode 100644 index 00000000..169dea91 --- /dev/null +++ b/charts/context.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.context +职责:一次 render 周期的共享上下文(视窗、切片、Qt 引用) +依赖:QtCharts(TYPE_CHECKING) +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from PySide2.QtCharts import QtCharts + + QChart = QtCharts.QChart + QValueAxis = QtCharts.QValueAxis +else: + QChart = Any + QValueAxis = Any + + +@dataclass +class ChartRenderContext: + """一次 render 周期内的共享上下文。""" + + history: Any = None + view_start: float = 0.0 + view_end: float = 150.0 + view_window_s: float = 150.0 + xs: List[float] = field(default_factory=list) + field_data: Dict[str, List[float]] = field(default_factory=dict) + qchart: Optional[QChart] = None + axis_x: Optional[QValueAxis] = None + live_mode: bool = True + config: Any = None diff --git a/charts/facade/__init__.py b/charts/facade/__init__.py new file mode 100644 index 00000000..8451c210 --- /dev/null +++ b/charts/facade/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""包占位:charts.facade(请从 charts 或 trend_chart 导入)。""" diff --git a/charts/facade/trend_chart.py b/charts/facade/trend_chart.py new file mode 100644 index 00000000..f8e49e78 --- /dev/null +++ b/charts/facade/trend_chart.py @@ -0,0 +1,693 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.facade.trend_chart +职责:训练页成品趋势图 facade(主折线 + 航迹图 + 操纵输入 + 全局游标 + 对比) +依赖:ChartDisplayArea、LineChart、TrackMapPane、StickInputPane、overlays、toolbar +""" + +from __future__ import annotations + +from typing import Callable, List, Optional, Sequence + +from PySide2.QtCore import Signal, QTimer, Qt +from PySide2.QtGui import QColor, QPalette +from PySide2.QtWidgets import ( + QFileDialog, + QHBoxLayout, + QLabel, + QVBoxLayout, + QWidget, +) + +from app_frame import UITheme +from charts.base.line_chart import LineChart +from charts.container import ChartDisplayArea, ChartSlot +from charts.overlay.registry import create_overlays +from charts.widgets.chart_toolbar import ChartToolbar +from charts.widgets.stick_input import StickInputPane +from charts.widgets.timeline_scrubber import TimelineScrubber +from charts.widgets.track_map_pane import TrackMapPane +from models.chart import ( + ChartConfig, + ChartHistoryBuffer, + TRAINING_CHART, + relative_times, + series_option, +) +from models.data_bridge import load_overlay_config + +from models.hardware_monitor import StickAxes +from models.track_compare import ( + CompareStyle, + TrackComparePayload, + default_compare_dir, + load_track_compare, +) +from models.track_map import TrackPoint, points_from_trajectory_samples + + +class TrendChart(QWidget): + """可配置趋势图;底部全局游标同步折线 / 航迹 / 摇杆。""" + + compareActiveChanged = Signal(bool, str) # active, label + compareHintChanged = Signal(str) + speedSourceChanged = Signal(str) # airspeed | groundspeed + + def __init__(self, parent=None, config: Optional[ChartConfig] = None): + super().__init__(parent) + self._config = config or TRAINING_CHART + overlay_config = load_overlay_config() + + self.main_slot = ChartSlot( + LineChart(self._config), + create_overlays(overlay_config), + ) + self._track_pane = TrackMapPane() + self._track_pane.speedSourceChanged.connect(self._on_speed_source_changed) + self._stick_pane = StickInputPane() + + self._display = ChartDisplayArea( + self.main_slot, + bottom_slots=[], + bottom_widgets=[self._track_pane, self._stick_pane], + ) + self._toolbar = ChartToolbar(self) + self._toolbar.set_save_target(self._display.grab) + self._toolbar.seriesSelectionChanged.connect(self._on_series_selection) + self._compare_style = CompareStyle() + self._compare_payload: Optional[TrackComparePayload] = None + self._pending_slots = None + self._history: Optional[ChartHistoryBuffer] = None + self._follow_live = True + self._syncing_scrub = False + self._live_stick: Optional[StickAxes] = None + + # 全局时间轴:整图最下方独立子容器 + self._timeline = self._build_timeline_pane() + + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(UITheme.scaled(4, 2)) + layout.addWidget(self._toolbar, 0) + layout.addWidget(self._display, 1) + layout.addWidget(self._timeline, 0) + self.main_slot.live_mode = True + self.main_slot.view_end = None + + self._apply_series_selection(self._toolbar.selection()) + + def _build_timeline_pane(self) -> QWidget: + """略深于 SURFACE 的时间轴条;滑块为向右飞行的剪影飞机。""" + pane_bg = "#122536" + border = UITheme.TOGGLE_TRACK_BORDER + + pane = QWidget(self) + pane.setObjectName("TimelinePane") + pane.setAttribute(Qt.WA_StyledBackground, True) + pane.setAutoFillBackground(True) + pal = pane.palette() + pal.setColor(QPalette.Window, QColor(pane_bg)) + pal.setColor(QPalette.Base, QColor(pane_bg)) + pane.setPalette(pal) + pane.setStyleSheet( + f""" + QWidget#TimelinePane {{ + background-color: {pane_bg}; + border: 1px solid {border}; + border-radius: {UITheme.scaled(4, 3)}px; + }} + QWidget#TimelinePane QLabel {{ + color: {UITheme.FG_MUTED}; + background: transparent; + border: none; + }} + """ + ) + + row = QHBoxLayout(pane) + row.setContentsMargins( + UITheme.scaled(10, 7), + UITheme.scaled(6, 4), + UITheme.scaled(10, 7), + UITheme.scaled(6, 4), + ) + row.setSpacing(UITheme.scaled(10, 7)) + scrub_lbl = QLabel("时间轴") + self._scrub = TimelineScrubber(track_color=pane_bg) + self._scrub.setObjectName("TimelineScrubber") + self._scrub.setMinimum(0) + self._scrub.setMaximum(0) + self._scrub.setValue(0) + self._scrub.setEnabled(False) + self._scrub.valueChanged.connect(self._on_scrub) + self._scrub_pos = QLabel("0/0") + self._scrub_pos.setMinimumWidth(UITheme.scaled(56, 44)) + self._scrub_pos.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + row.addWidget(scrub_lbl, 0) + row.addWidget(self._scrub, 1) + row.addWidget(self._scrub_pos, 0) + return pane + + def _on_series_selection(self, slots) -> None: + """工具栏变更:推迟到下一事件循环,避免与轮询 render 重入崩 Qt Charts。""" + self._pending_slots = list(slots) + QTimer.singleShot(0, self._flush_series_selection) + + def _flush_series_selection(self) -> None: + slots = self._pending_slots + if slots is None: + return + self._pending_slots = None + self._apply_series_selection(slots) + self._apply_global_cursor() + + def _apply_series_selection(self, slots) -> None: + line = self.main_slot.base_chart + if isinstance(line, LineChart): + line.set_slot_selection(slots) + self.main_slot.render() + + @property + def live_mode(self) -> bool: + return self.main_slot.live_mode + + @live_mode.setter + def live_mode(self, value: bool) -> None: + self.main_slot.live_mode = value + + @property + def view_end(self) -> Optional[float]: + return self.main_slot.view_end + + @view_end.setter + def view_end(self, value: Optional[float]) -> None: + self.main_slot.view_end = value + + def set_side_pane_visible(self, visible: bool) -> None: + """兼容旧 API:附图区已固定显示在下方。""" + self._track_pane.setVisible(bool(visible)) + self._stick_pane.setVisible(bool(visible)) + + def set_config(self, config: ChartConfig) -> None: + self._config = config + self.main_slot.set_config(config) + self.main_slot.render() + + def set_save_caption_provider(self, provider: Optional[Callable]) -> None: + self._toolbar.set_save_caption_provider(provider) + + def set_save_enabled(self, enabled: bool) -> None: + self._toolbar.set_save_enabled(enabled) + + def save_chart(self) -> None: + self._toolbar.save_chart() + + def update(self, history) -> None: + self._history = history if isinstance(history, ChartHistoryBuffer) else history + if self.live_mode: + self.view_end = None + self._display.update_all(history) + # 跟随直播时钉在最新点 + n = self._track_pane.scrub_point_count() + if self._follow_live and n > 0: + self._set_scrub_index(n - 1, apply=True) + else: + self._apply_global_cursor() + + def resume_live_follow(self) -> None: + """重新开始监测等场景:恢复时间轴跟随与实时摇杆刷新。""" + self._follow_live = True + self.live_mode = True + self.view_end = None + self.main_slot.set_time_base_override(None) + n = self._timeline_length() + if n > 0: + self._set_scrub_index(n - 1, apply=True) + elif self._live_stick is not None: + self._stick_pane.set_stick(self._live_stick) + self._stick_pane.clear_compare() + else: + self._apply_global_cursor() + + def set_track_points(self, points: Sequence[TrackPoint]) -> None: + was_following = self._follow_live + self._track_pane.set_track_points(points) + self._refresh_timeline_range(prefer_end=was_following) + n = self._timeline_length() + # 钉在末端即视为跟随直播(含刚开监测、点数尚少) + if n <= 1 or self._scrub.value() >= n - 1: + self._follow_live = True + self._apply_global_cursor() + + def is_compare_visible(self) -> bool: + return bool(getattr(self._compare_style, "visible", True)) + + def _timeline_length(self) -> int: + """时间轴长度:本场航迹优先;历史显隐关闭时不用对比缓冲。""" + n_scrub = self._track_pane.scrub_point_count() + if n_scrub > 1: + return n_scrub + if isinstance(self._history, ChartHistoryBuffer) and len(self._history) > 1: + return len(self._history) + if ( + self.is_compare_visible() + and self._compare_payload is not None + and isinstance(self._compare_payload.history, ChartHistoryBuffer) + and len(self._compare_payload.history) > 1 + ): + return len(self._compare_payload.history) + return max(n_scrub, 0) + + def _history_for_cursor(self) -> Optional[ChartHistoryBuffer]: + """折线取值缓冲:与时间轴驱动源对齐;关闭「显示历史数据」时不用对比历史。""" + n_live = self._track_pane.point_count() + n_cmp = self._track_pane.compare_point_count() + use_compare = ( + self.is_compare_visible() + and n_live <= 1 + and n_cmp > 1 + and self._compare_payload is not None + and isinstance(self._compare_payload.history, ChartHistoryBuffer) + and len(self._compare_payload.history) > 0 + ) + if use_compare: + return self._compare_payload.history + if isinstance(self._history, ChartHistoryBuffer) and len(self._history) > 0: + return self._history + if ( + self.is_compare_visible() + and self._compare_payload is not None + and isinstance(self._compare_payload.history, ChartHistoryBuffer) + and len(self._compare_payload.history) > 0 + ): + return self._compare_payload.history + return None + + def _refresh_timeline_range(self, *, prefer_end: bool) -> None: + """按当前数据源刷新时间轴可用范围(含对比-only 场景)。""" + n = self._timeline_length() + self._syncing_scrub = True + if n <= 1: + self._scrub.setEnabled(False) + self._scrub.setMaximum(0) + self._scrub.setValue(0) + self._follow_live = True + else: + self._scrub.setEnabled(True) + self._scrub.setMaximum(n - 1) + # 键盘翻页约 2% 轨迹,至少 10 点 + self._scrub.setPageStep(max(10, n // 50)) + if prefer_end: + self._scrub.setValue(n - 1) + self._follow_live = True + else: + self._scrub.setValue(min(self._scrub.value(), n - 1)) + self._follow_live = self._scrub.value() >= n - 1 + self._syncing_scrub = False + idx = self._scrub.value() if n > 0 else 0 + if self._track_pane.scrub_point_count() > 0: + self._track_pane.set_cursor_index(idx) + self._update_scrub_label() + + def reset_track_filters(self) -> None: + self._track_pane.reset_filters() + + def reset_speed_band_filters(self) -> None: + self.reset_track_filters() + + def speed_band_source(self) -> str: + return self._track_pane.speed_source() + + def _on_speed_source_changed(self, source: str) -> None: + self.speedSourceChanged.emit(str(source)) + + def set_stick_axes( + self, + pitch: float = 0.0, + roll: float = 0.0, + yaw: float = 0.0, + throttle: float = 0.0, + connected: bool = True, + ) -> None: + axes = StickAxes( + pitch=pitch, + roll=roll, + yaw=yaw, + throttle=throttle, + connected=connected, + ) + self._live_stick = axes + if self._follow_live: + self._stick_pane.set_stick(axes) + self._sync_compare_stick(self._live_cursor_time()) + + def set_stick(self, axes: StickAxes) -> None: + self._live_stick = axes + if self._follow_live: + self._stick_pane.set_stick(axes) + self._sync_compare_stick(self._live_cursor_time()) + + def clear_stick(self) -> None: + self._live_stick = None + self._stick_pane.clear() + + def _live_cursor_time(self) -> Optional[float]: + """当前游标相对时间(供实时摇杆刷新时对齐历史层)。""" + pt = self._track_pane.cursor_point() + if pt is None: + return None + return self._history_cursor_time(pt, self._track_pane.cursor_index()) + + def _on_scrub(self, value: int) -> None: + if self._syncing_scrub: + return + n = self._timeline_length() + if n <= 0: + self._follow_live = True + return + idx = max(0, min(n - 1, int(value))) + # 在末端即跟随直播;点数很少时也保持跟随,避免重启监测后摇杆停刷 + self._follow_live = idx >= n - 1 + if self._track_pane.scrub_point_count() > 0: + self._track_pane.set_cursor_index(idx) + self._update_scrub_label() + self._apply_global_cursor() + + def _set_scrub_index(self, index: int, *, apply: bool) -> None: + n = self._timeline_length() + if n <= 0: + return + idx = max(0, min(n - 1, int(index))) + self._syncing_scrub = True + self._scrub.setMaximum(max(0, n - 1)) + self._scrub.setEnabled(n > 1) + if n > 1: + self._scrub.setPageStep(max(10, n // 50)) + self._scrub.setValue(idx) + self._syncing_scrub = False + if self._track_pane.scrub_point_count() > 0: + self._track_pane.set_cursor_index(idx) + self._update_scrub_label() + if apply: + self._apply_global_cursor() + + def _update_scrub_label(self) -> None: + n = self._timeline_length() + if n <= 0: + self._scrub_pos.setText("0/0") + else: + self._scrub_pos.setText(f"{self._scrub.value() + 1}/{n}") + + def _history_cursor_time(self, pt: TrackPoint, idx: int) -> float: + """航迹时间 → 折线图相对 X(与 relative_times 对齐)。""" + hist = self._history_for_cursor() + if hist is not None and len(hist) > 0: + n_hist = len(hist) + n_track = self._track_pane.scrub_point_count() + if n_hist == n_track: + mapped = hist.relative_time_at(idx) + if mapped is not None: + return float(mapped) + t0 = float(hist.timestamps[0]) + return float(pt.timestamp_s) - t0 + return float(pt.timestamp_s) + + def _sync_line_view_to_cursor(self, t_rel: float) -> None: + """回放时把折线视窗钉到游标;跟随直播则回到最新端。""" + hist = self._history_for_cursor() + # 时间轴走对比源时,折线视窗也必须以对比历史为基准,否则游标会被裁掉 + live = self._history if isinstance(self._history, ChartHistoryBuffer) else None + if hist is not None and live is not None and hist is not live: + self.main_slot.set_time_base_override(hist) + else: + self.main_slot.set_time_base_override(None) + + if self._follow_live: + self.live_mode = True + self.view_end = None + return + self.live_mode = False + if hist is None or len(hist) == 0: + return + times = relative_times(list(hist.timestamps)) + if not times: + return + t_max = float(times[-1]) + self.view_end = max(0.0, min(float(t_rel), t_max)) + + def _stick_from_point(self, pt: Optional[TrackPoint]) -> Optional[StickAxes]: + if pt is None or not pt.stick_connected(): + return None + return StickAxes( + pitch=0.0 if pt.stick_pitch != pt.stick_pitch else float(pt.stick_pitch), + roll=0.0 if pt.stick_roll != pt.stick_roll else float(pt.stick_roll), + yaw=0.0 if pt.stick_yaw != pt.stick_yaw else float(pt.stick_yaw), + throttle=0.0 if pt.stick_throttle != pt.stick_throttle else float(pt.stick_throttle), + connected=True, + ) + + def _sync_compare_stick(self, t_rel: Optional[float]) -> None: + """有本场+可见历史时叠画对比摇杆;否则清除历史层。""" + has_live = self._track_pane.point_count() > 1 or ( + self._follow_live and self._live_stick is not None and self._live_stick.connected + ) + has_hist = ( + self.is_compare_visible() + and self._track_pane.compare_point_count() > 1 + ) + if not (has_live and has_hist): + self._stick_pane.clear_compare() + return + t = 0.0 if t_rel is None else float(t_rel) + cmp_pt = self._track_pane.compare_point_at_time(t) + axes = self._stick_from_point(cmp_pt) + if axes is None: + self._stick_pane.clear_compare() + else: + self._stick_pane.set_compare_stick(axes) + + def _lap_label_for_cursor(self, t_rel: Optional[float], pt: Optional[TrackPoint]) -> str: + """浮窗末行圈数;无有效圈号时补「LAP --」。""" + if pt is not None and int(getattr(pt, "lap", 0) or 0) > 0: + return f"LAP {int(pt.lap)}" + if t_rel is not None: + cmp_pt = self._track_pane.compare_point_at_time(float(t_rel)) + if cmp_pt is not None and int(getattr(cmp_pt, "lap", 0) or 0) > 0: + return f"LAP {int(cmp_pt.lap)}" + return "LAP --" + + def _cursor_float_lines( + self, + t_rel: float, + middle: Optional[List[str]] = None, + *, + pt: Optional[TrackPoint] = None, + ) -> List[str]: + """统一浮窗结构:首行时间、末行圈数,中间为业务字段。""" + lines: List[str] = [f"t {float(t_rel):.2f}s"] + for item in middle or []: + s = str(item).strip() + if not s: + continue + # 去掉调用方误带的时间/圈数行,避免重复 + low = s.lower() + if low.startswith("t ") and s.rstrip().endswith("s"): + continue + if low.startswith("lap"): + continue + lines.append(s) + lines.append(self._lap_label_for_cursor(t_rel, pt)) + return lines + + def _apply_global_cursor(self) -> None: + pt = self._track_pane.cursor_point() + line = self.main_slot.base_chart + idx = self._scrub.value() + # 无航迹点时仍可用趋势缓冲驱动折线游标(对比-only) + if pt is None: + hist = self._history_for_cursor() + if hist is None or len(hist) <= 0 or not isinstance(line, LineChart): + if isinstance(line, LineChart): + line.clear_cursor() + self.main_slot.render() + if self._follow_live and self._live_stick is not None: + self._stick_pane.set_stick(self._live_stick) + self._sync_compare_stick(None) + return + t = hist.relative_time_at(idx) + if t is None: + t = 0.0 + self._sync_line_view_to_cursor(float(t)) + middle: List[str] = [] + vals = hist.values_at_relative(float(t)) + sides = line.side_field_ids() + for side_key, title in (("left", "L"), ("right", "R")): + fid = sides.get(side_key) + if not fid: + continue + opt = series_option(fid) + name = opt.label if opt is not None else fid + v = vals.get(fid, float("nan")) + if v != v: + middle.append(f"{title} {name}: --") + else: + middle.append(f"{title} {name}: {v:.2f}") + line.set_cursor(float(t), self._cursor_float_lines(float(t), middle, pt=None)) + if self._follow_live and self._live_stick is not None: + self._stick_pane.set_stick(self._live_stick) + self._sync_compare_stick(float(t)) + self.main_slot.render() + return + + # 摇杆当前层:跟随直播用实时值,否则用本场切片 + cursor_t: Optional[float] = None + if self._follow_live and self._live_stick is not None: + self._stick_pane.set_stick(self._live_stick) + else: + live_axes = self._stick_from_point(pt) + if live_axes is not None: + self._stick_pane.set_axes( + live_axes.pitch, + live_axes.roll, + live_axes.yaw, + live_axes.throttle, + connected=True, + ) + else: + self._stick_pane.set_axes(0.0, 0.0, 0.0, 0.0, connected=False) + + # 折线竖线 + 左右 Y 轴字段值 + if isinstance(line, LineChart): + idx = self._track_pane.cursor_index() + t = self._history_cursor_time(pt, idx) + cursor_t = t + self._sync_line_view_to_cursor(t) + middle = [] + hist = self._history_for_cursor() + if hist is not None: + vals = hist.values_at_relative(t) + sides = line.side_field_ids() + for side_key, title in (("left", "L"), ("right", "R")): + fid = sides.get(side_key) + if not fid: + continue + opt = series_option(fid) + name = opt.label if opt is not None else fid + v = vals.get(fid, float("nan")) + if v != v: + middle.append(f"{title} {name}: --") + else: + middle.append(f"{title} {name}: {v:.2f}") + line.set_cursor(t, self._cursor_float_lines(t, middle, pt=pt)) + self.main_slot.render() + elif pt is not None: + cursor_t = float(pt.timestamp_s) + + self._sync_compare_stick(cursor_t) + + def is_compare_active(self) -> bool: + return self._compare_payload is not None + + def compare_style(self) -> CompareStyle: + return self._compare_style + + def compare_label(self) -> str: + if self._compare_payload is None: + return "" + return self._compare_payload.label or "" + + def load_compare_dialog(self) -> None: + start_dir = str(default_compare_dir()) + path, _ = QFileDialog.getOpenFileName( + self, + "选择对比轨迹文件", + start_dir, + "轨迹 CSV (*.csv);;所有文件 (*.*)", + ) + if not path: + return + try: + payload = load_track_compare(path) + except Exception as exc: + self.compareHintChanged.emit(f"对比加载失败: {exc}") + return + self.apply_compare(payload) + + def apply_compare(self, payload: TrackComparePayload) -> None: + self._compare_payload = payload + line = self.main_slot.base_chart + if isinstance(line, LineChart): + line.set_compare_history(payload.history) + line.set_compare_style(self._compare_style) + cmp_points: List[TrackPoint] = [] + try: + cmp_points = points_from_trajectory_samples(payload.trajectory.samples) + except Exception: + cmp_points = [] + self._track_pane.set_compare_track_points(cmp_points) + self.apply_compare_style(self._compare_style) + hints = [] + if not payload.has_trend: + hints.append("无趋势字段") + if not payload.has_laps: + hints.append("无圈时") + self.compareHintChanged.emit(";".join(hints) if hints else "") + # 对比载入后也要刷新时间轴(本场为空时可拖对比轨迹) + self._refresh_timeline_range(prefer_end=self._follow_live) + self._apply_global_cursor() + self.main_slot.render() + self.compareActiveChanged.emit(True, payload.label or "") + + def clear_compare(self) -> None: + self._compare_payload = None + line = self.main_slot.base_chart + if isinstance(line, LineChart): + line.set_compare_history(None) + self._track_pane.set_compare_track_points(None) + self._stick_pane.clear_compare() + self.compareHintChanged.emit("") + self.main_slot.set_time_base_override(None) + self._refresh_timeline_range(prefer_end=self._follow_live) + self._apply_global_cursor() + self.main_slot.render() + self.compareActiveChanged.emit(False, "") + + def apply_compare_style(self, style: CompareStyle) -> None: + self._compare_style = style + line = self.main_slot.base_chart + if isinstance(line, LineChart): + line.set_compare_style(style) + self._track_pane.set_compare_style(style) + # 关闭「显示历史数据」时清掉基于对比的游标/浮窗/摇杆,并切回本场时间基准 + if not self.is_compare_visible(): + self.main_slot.set_time_base_override(None) + if isinstance(line, LineChart) and self._track_pane.point_count() <= 1: + line.clear_cursor() + self._stick_pane.clear_compare() + # 摇杆当前层:跟随时恢复实时杆量,否则清零复位 + if self._follow_live and self._live_stick is not None: + self._stick_pane.set_stick(self._live_stick) + else: + self._stick_pane.clear() + self._refresh_timeline_range(prefer_end=self._follow_live) + self._apply_global_cursor() + self.main_slot.render() + + def add_lap_marker(self, timestamp=None, lap_number=None) -> None: + import time + + lap_ov = self.main_slot.lap_overlay() + if lap_ov is None: + return + ts = time.time() if timestamp is None else float(timestamp) + lap = int(lap_number) if lap_number is not None else len(lap_ov._lap_markers) + 1 + lap_ov.add_marker(lap, ts) + self.main_slot.render() + + def clear_lap_markers(self) -> None: + lap_ov = self.main_slot.lap_overlay() + if lap_ov is None: + return + lap_ov.clear_markers() + lap_ov.clear(self.main_slot.qchart) + self.main_slot.render() diff --git a/charts/overlay/__init__.py b/charts/overlay/__init__.py new file mode 100644 index 00000000..b78bf666 --- /dev/null +++ b/charts/overlay/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""包占位:charts.overlay(请从具体子模块导入)。""" diff --git a/charts/overlay/base_overlay.py b/charts/overlay/base_overlay.py new file mode 100644 index 00000000..dfd2d543 --- /dev/null +++ b/charts/overlay/base_overlay.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.overlay.base_overlay +职责:蒙版图层抽象接口 +依赖:BaseChart、ChartRenderContext(TYPE_CHECKING) +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any, Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from PySide2.QtCharts import QChart + from charts.base.base_chart import BaseChart + from charts.context import ChartRenderContext +else: + QChart = Any + + +class ChartOverlay(ABC): + """挂在基底上的蒙版层;由 ChartSlot 按 z_order 调用 update/clear。""" + + overlay_id: str = "overlay" + z_order: int = 0 + + def __init__(self) -> None: + self._base: Optional["BaseChart"] = None + + def bind_base(self, base: "BaseChart") -> None: + """绑定基底图表(取轴/系列用)。""" + self._base = base + + @abstractmethod + def update(self, ctx: "ChartRenderContext") -> None: + """按上下文重绘蒙版。""" + ... + + @abstractmethod + def clear(self, qchart: QChart) -> None: + """从 qchart 移除本层系列/图元。""" + ... diff --git a/charts/overlay/health_span_overlay.py b/charts/overlay/health_span_overlay.py new file mode 100644 index 00000000..55773b7c --- /dev/null +++ b/charts/overlay/health_span_overlay.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.overlay.health_span_overlay +职责:健康度阈值色带蒙版(X 区间 × Y 字段范围) +依赖:ChartOverlay、ThresholdSpanOverlaySpec、kind_resolver +""" + +from __future__ import annotations + +from typing import List, Optional + +from charts.qtcharts import QAreaSeries, QChart, QLineSeries, QValueAxis +from PySide2.QtCore import Qt +from PySide2.QtGui import QColor, QPen + +from charts.base.base_chart import BaseChart +from charts.context import ChartRenderContext +from charts.overlay.base_overlay import ChartOverlay +from models.chart import ChartFieldSpec +from models.data_bridge import ThresholdSpanOverlaySpec, kind_resolver + + + +class HealthSpanOverlay(ChartOverlay): + """按 health_span_kind 在视窗内绘制半透明色带。""" + + def __init__(self, spec: ThresholdSpanOverlaySpec) -> None: + super().__init__() + self.overlay_id = spec.overlay_id + self.z_order = spec.z_order + self._spec = spec + self._span_series: List[QAreaSeries] = [] + self._kind_fn = kind_resolver(spec.kind_resolver_name) + + def update(self, ctx: ChartRenderContext) -> None: + if self._base is None or ctx.qchart is None: + return + self.clear(ctx.qchart) + if not self._spec.enabled or self._kind_fn is None: + return + # 健康曲线未勾选显示时不画色带,避免与右侧轴范围联动冲突 + if hasattr(self._base, "is_field_visible") and not self._base.is_field_visible( + self._spec.source_field_id + ): + return + config = ctx.config + if config is None: + return + span_field = config.field_by_id(self._spec.axis_field_id) + if span_field is None: + return + xs = ctx.xs + span_values = ctx.field_data.get(self._spec.source_field_id, []) + if not xs or not span_values or len(xs) != len(span_values): + return + span_axis = self._base.field_axis(span_field) + if span_axis is None: + return + try: + if not span_axis.isVisible(): + return + except Exception: + pass + if self._spec.direction == "x": + self._update_x_spans(ctx.qchart, xs, span_values, span_field, span_axis, ctx.view_window_s) + # direction "y" / "xy" 预留扩展 + + def _span_color(self, kind: str) -> QColor: + hex_map = self._spec.colors + c = QColor(hex_map.get(kind, "#e74c3c")) + c.setAlphaF(self._spec.alpha) + return c + + def _add_x_span( + self, + qchart: QChart, + x0: float, + x1: float, + kind: str, + span_field: ChartFieldSpec, + span_axis: QValueAxis, + window: float, + ) -> None: + if kind is None: + return + if x1 < x0: + x0, x1 = x1, x0 + if x1 == x0: + x1 = x0 + max(0.05, window * 0.002) + ymin, ymax = span_field.ylim() + upper = QLineSeries() + lower = QLineSeries() + upper.append(x0, ymax) + upper.append(x1, ymax) + lower.append(x0, ymin) + lower.append(x1, ymin) + area = QAreaSeries(upper, lower) + area.setPen(QPen(Qt.NoPen)) + area.setBrush(self._span_color(kind)) + qchart.addSeries(area) + axis_x = self._base.axis_x() + if axis_x is not None: + area.attachAxis(axis_x) + area.attachAxis(span_axis) + self._span_series.append(area) + + def _update_x_spans( + self, + qchart: QChart, + xs, + span_values, + span_field: ChartFieldSpec, + span_axis: QValueAxis, + window: float, + ) -> None: + # 满窗约 1500 点时若阈值抖动会产生海量 QAreaSeries,拖垮 Qt;降采样边界检测 + n = len(xs) + stride = max(1, n // 400) if n > 400 else 1 + active_kind = None + start_x = None + prev_x = xs[0] + span_budget = 80 + for i in range(0, n, stride): + x = xs[i] + value = span_values[i] + kind = self._kind_fn(value) + if kind != active_kind: + if active_kind is not None and start_x is not None: + if span_budget <= 0: + active_kind = kind + start_x = x if kind is not None else None + prev_x = x + continue + self._add_x_span( + qchart, + start_x, + prev_x if prev_x > start_x else x, + active_kind, + span_field, + span_axis, + window, + ) + span_budget -= 1 + active_kind = kind + start_x = x if kind is not None else None + prev_x = x + if active_kind is not None and start_x is not None and span_budget > 0: + end_x = xs[-1] + if end_x <= start_x and len(xs) == 1: + end_x = start_x + max(0.05, window * 0.002) + self._add_x_span(qchart, start_x, end_x, active_kind, span_field, span_axis, window) + + def clear(self, qchart: QChart) -> None: + for series in self._span_series: + qchart.removeSeries(series) + self._span_series.clear() diff --git a/charts/overlay/lap_marker_overlay.py b/charts/overlay/lap_marker_overlay.py new file mode 100644 index 00000000..3707a151 --- /dev/null +++ b/charts/overlay/lap_marker_overlay.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.overlay.lap_marker_overlay +职责:圈完成竖线 + 顶部标签蒙版 +依赖:ChartOverlay、LapMarkerOverlaySpec +""" + +from __future__ import annotations + +import math +from typing import List, Optional, Tuple + +from charts.qtcharts import QChart, QLineSeries, QValueAxis +from PySide2.QtCore import QPointF +from PySide2.QtGui import QFont, QPen +from PySide2.QtWidgets import QGraphicsTextItem + +from charts.context import ChartRenderContext +from charts.overlay.base_overlay import ChartOverlay +from charts.theme import error_color +from models.data_bridge import LapMarkerOverlaySpec + + + +class LapMarkerOverlay(ChartOverlay): + """在相对时间轴上标注 LAP 完成时刻。""" + + def __init__(self, spec: Optional[LapMarkerOverlaySpec] = None) -> None: + super().__init__() + self._spec = spec or LapMarkerOverlaySpec() + self.overlay_id = self._spec.overlay_id + self.z_order = self._spec.z_order + self._lap_markers: List[Tuple[int, float]] = [] + self._lap_series: List[QLineSeries] = [] + self._lap_label_items: List[QGraphicsTextItem] = [] + + def set_markers(self, markers: List[Tuple[int, float]]) -> None: + """整体替换 (lap_number, absolute_timestamp) 列表。""" + self._lap_markers = list(markers) + + def add_marker(self, lap_number: int, timestamp: float) -> None: + """追加一个圈标。""" + self._lap_markers.append((lap_number, timestamp)) + + def clear_markers(self) -> None: + """仅清空数据,不立刻从场景移除图元。""" + self._lap_markers = [] + + def update(self, ctx: ChartRenderContext) -> None: + if self._base is None or ctx.qchart is None: + return + self.clear(ctx.qchart) + if not self._spec.enabled: + return + if not ctx.history or not self._lap_markers: + return + primary = self._base.primary_series() + if primary is None or primary.count() <= 0 or ctx.config is None: + return + ts_list = list(ctx.history.timestamps) + if not ts_list: + return + # 用当前主字段轴/量程,避免隐藏字段后仍按配置首字段映射导致非法坐标 + primary_field = getattr(self._base, "_primary_field", None) or ctx.config.primary_field() + left_axis = self._base.field_axis(primary_field) + if left_axis is None: + return + try: + if not left_axis.isVisible(): + return + except Exception: + pass + ymin, ymax = primary_field.ylim() + t0 = float(ts_list[0]) + # 视窗外扩一点避免边界抖动;仍拒绝明显越界,避免 mapToPosition 出 NaN + lo = float(ctx.view_start) - 1e-6 + hi = float(ctx.view_end) + 1e-6 + for lap_number, ts in self._lap_markers: + x = float(ts) - t0 + if not math.isfinite(x) or x < lo or x > hi: + continue + line = QLineSeries() + pen = QPen(error_color()) + pen.setWidthF(0.9) + line.setPen(pen) + line.append(x, ymin) + line.append(x, ymax) + ctx.qchart.addSeries(line) + axis_x = self._base.axis_x() + if axis_x is not None: + line.attachAxis(axis_x) + line.attachAxis(left_axis) + self._lap_series.append(line) + self._add_axis_label(ctx.qchart, x, lap_number, ymax, primary) + + def _add_axis_label( + self, + qchart: QChart, + x: float, + lap_number: int, + ymax: float, + primary, + ) -> None: + pos = qchart.mapToPosition(QPointF(x, ymax), primary) + if not math.isfinite(pos.x()) or not math.isfinite(pos.y()): + return + plot = qchart.plotArea() + # 映射到绘图区外时钳到区内,避免 QGraphicsItem 非法坐标直接崩 + px = min(max(pos.x(), plot.left()), plot.right()) + if not math.isfinite(px): + return + label = QGraphicsTextItem(self._spec.label_template.format(lap=lap_number)) + font = QFont() + font.setPointSize(8) + label.setFont(font) + label.setDefaultTextColor(error_color()) + scene = qchart.scene() + if scene is None: + return + scene.addItem(label) + rect = label.boundingRect() + label.setPos(px - rect.width() / 2.0, plot.top() - rect.height() - 2.0) + self._lap_label_items.append(label) + + def clear(self, qchart: QChart) -> None: + for series in self._lap_series: + try: + qchart.removeSeries(series) + except Exception: + pass + self._lap_series.clear() + scene = qchart.scene() + for item in self._lap_label_items: + if scene is not None: + try: + scene.removeItem(item) + except Exception: + pass + self._lap_label_items.clear() diff --git a/charts/overlay/registry.py b/charts/overlay/registry.py new file mode 100644 index 00000000..6d493ae1 --- /dev/null +++ b/charts/overlay/registry.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.overlay.registry +职责:按配置实例化蒙版列表并按 z_order 排序 +依赖:HealthSpanOverlay、LapMarkerOverlay、models.data_bridge +""" + +from __future__ import annotations + +from typing import Dict, List, Type + +from charts.overlay.base_overlay import ChartOverlay +from charts.overlay.health_span_overlay import HealthSpanOverlay +from charts.overlay.lap_marker_overlay import LapMarkerOverlay +from models.data_bridge import ( + ChartOverlayConfig, + LapMarkerOverlaySpec, + ThresholdSpanOverlaySpec, + load_overlay_config, +) + + +_OVERLAY_REGISTRY: Dict[str, Type[ChartOverlay]] = { + "threshold_span": HealthSpanOverlay, + "lap_marker": LapMarkerOverlay, +} + + +def register_overlay(overlay_type: str, cls: Type[ChartOverlay]) -> None: + """注册自定义蒙版类型。""" + _OVERLAY_REGISTRY[overlay_type] = cls + + +def create_overlays( + config: ChartOverlayConfig | None = None, +) -> List[ChartOverlay]: + """ + 说明:由 ChartOverlayConfig 生成已启用的蒙版实例 + 参数: + config — 缺省则 load_overlay_config() + 返回: + 按 z_order 升序的蒙版列表 + """ + cfg = config or load_overlay_config() + overlays: List[ChartOverlay] = [] + for spec in cfg.overlays: + if isinstance(spec, ThresholdSpanOverlaySpec): + if not spec.enabled: + continue + cls = _OVERLAY_REGISTRY.get("threshold_span", HealthSpanOverlay) + overlays.append(cls(spec)) + elif isinstance(spec, LapMarkerOverlaySpec): + if not spec.enabled: + continue + cls = _OVERLAY_REGISTRY.get("lap_marker", LapMarkerOverlay) + overlays.append(cls(spec)) + overlays.sort(key=lambda o: o.z_order) + return overlays diff --git a/charts/qtcharts.py b/charts/qtcharts.py new file mode 100644 index 00000000..74107b46 --- /dev/null +++ b/charts/qtcharts.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.qtcharts +职责:PySide2 QtCharts 兼容导入(类在 QtCharts 子模块下) +依赖:PySide2.QtCharts +""" + +from PySide2.QtCharts import QtCharts + +QAbstractSeries = QtCharts.QAbstractSeries +QAreaSeries = QtCharts.QAreaSeries +QBarCategoryAxis = QtCharts.QBarCategoryAxis +QBarSet = QtCharts.QBarSet +QCategoryAxis = QtCharts.QCategoryAxis +QChart = QtCharts.QChart +QChartView = QtCharts.QChartView +QHorizontalBarSeries = QtCharts.QHorizontalBarSeries +QHorizontalStackedBarSeries = QtCharts.QHorizontalStackedBarSeries +QLineSeries = QtCharts.QLineSeries +QPolarChart = QtCharts.QPolarChart +QValueAxis = QtCharts.QValueAxis + +__all__ = [ + "QAbstractSeries", + "QAreaSeries", + "QBarCategoryAxis", + "QBarSet", + "QCategoryAxis", + "QChart", + "QChartView", + "QHorizontalBarSeries", + "QHorizontalStackedBarSeries", + "QLineSeries", + "QPolarChart", + "QValueAxis", +] diff --git a/charts/theme.py b/charts/theme.py new file mode 100644 index 00000000..c207e0f3 --- /dev/null +++ b/charts/theme.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.theme +职责:图表配色适配(委托 UITheme 预置色号) +依赖:app_frame.UITheme +""" + +from __future__ import annotations + +from PySide2.QtGui import QColor + +from app_frame import UITheme + + +def field_line_color(color_key: str, color: str = "") -> QColor: + """优先用字段显式 color,否则按 color_key 取主题色。""" + if color: + return QColor(color) + color_map = { + "torque": UITheme.CHART_TORQUE, + "health": UITheme.CHART_HEALTH, + "airspeed": UITheme.CHART_AIRSPEED, + "groundspeed": UITheme.CHART_GROUNDSPEED, + "pitch": UITheme.CHART_PITCH, + "bank": UITheme.CHART_BANK, + } + return QColor(color_map.get(color_key, UITheme.FG)) + + +def desaturate_color(color, factor: float = 0.45) -> QColor: + """按 HSV 降低饱和度;factor 为保留比例(0.45 ≈ 降饱和约 55%)。""" + c = QColor(color) if not isinstance(color, QColor) else QColor(color) + h, s, v, a = c.getHsvF() + out = QColor() + out.setHsvF(h, max(0.0, min(1.0, s * float(factor))), v, a) + return out + + +def field_pen_style(line_style: str = "") -> "Qt.PenStyle": + """字段线型;空则实线。""" + from PySide2.QtCore import Qt + + return { + "solid": Qt.SolidLine, + "dash": Qt.DashLine, + "dot": Qt.DotLine, + "dashdot": Qt.DashDotLine, + }.get(line_style or "solid", Qt.SolidLine) + + +def reference_line_color() -> QColor: + """历史对比默认参考线颜色。""" + return QColor(UITheme.CHART_REFERENCE) + + +def compare_qcolor(style) -> QColor: + """从 CompareStyle.color 生成带透明度的 QColor(兼容旧单色用法)。""" + c = QColor(getattr(style, "color", UITheme.CHART_REFERENCE)) + alpha = max(0.0, min(1.0, float(getattr(style, "opacity", 0.7)))) + c.setAlphaF(alpha) + return c + + +def compare_field_qcolor( + color_key: str, + color: str = "", + opacity: float = 0.7, + sat_scale: float = 0.45, +) -> QColor: + """历史对比折线色:原字段色降饱和 + 固定透明度。""" + c = desaturate_color(field_line_color(color_key, color), sat_scale) + c.setAlphaF(max(0.0, min(1.0, float(opacity)))) + return c + + +def compare_lap_qcolor( + lap_number: int, + opacity: float = 0.7, + sat_scale: float = 0.45, +) -> QColor: + """历史对比速度带色:原 LAP 色降饱和 + 透明度。""" + c = desaturate_color(lap_color(lap_number), sat_scale) + c.setAlphaF(max(0.0, min(1.0, float(opacity)))) + return c + + +def compare_pen_style(style) -> "Qt.PenStyle": + """从 CompareStyle.line_style 映射 Qt 笔型。""" + from PySide2.QtCore import Qt + + return { + "solid": Qt.SolidLine, + "dash": Qt.DashLine, + "dot": Qt.DotLine, + "dashdot": Qt.DashDotLine, + }.get(getattr(style, "line_style", "dash"), Qt.DashLine) + + +def lap_color(lap_number: int) -> QColor: + """LAP1–LAP4 固定色;越界回退 FG。""" + idx = int(lap_number) - 1 + colors = UITheme.CHART_LAP_COLORS + if 0 <= idx < len(colors): + return QColor(colors[idx]) + return QColor(UITheme.FG) + + +def span_fill_color(kind: str, alpha: float | None = None) -> QColor: + hex_map = { + "orange": UITheme.CHART_HEALTH_ORANGE_FILL, + "blue": UITheme.CHART_HEALTH_BLUE_FILL, + "red": UITheme.CHART_HEALTH_RED_FILL, + } + c = QColor(hex_map.get(kind, UITheme.CHART_HEALTH_RED_FILL)) + c.setAlphaF(alpha if alpha is not None else UITheme.CHART_HEALTH_SPAN_ALPHA) + return c + + +def chart_surface_color() -> QColor: + """图表容器/表面填充色(UITheme.SURFACE)。""" + return QColor(UITheme.SURFACE) + + +def chart_well_color(_container=None) -> QColor: + """嵌套井区填充(预置 UITheme.CHART_WELL,相对 SURFACE 降亮 20%)。""" + return QColor(UITheme.CHART_WELL) + + +def chart_well_border_color() -> QColor: + """井区边框色。""" + return QColor(UITheme.BORDER) + + +def axis_muted_color() -> QColor: + """坐标轴标签/标题弱化色。""" + return QColor(UITheme.FG_MUTED) + + +def axis_border_color() -> QColor: + """坐标轴线与网格色。""" + return QColor(UITheme.BORDER) + + +def error_color() -> QColor: + """错误/圈标强调色。""" + return QColor(UITheme.ERROR) diff --git a/charts/widgets/__init__.py b/charts/widgets/__init__.py new file mode 100644 index 00000000..6775d0bc --- /dev/null +++ b/charts/widgets/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""包占位:charts.widgets(请从具体子模块导入)。""" diff --git a/charts/widgets/chart_toolbar.py b/charts/widgets/chart_toolbar.py new file mode 100644 index 00000000..b254ee48 --- /dev/null +++ b/charts/widgets/chart_toolbar.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.widgets.chart_toolbar +职责:左右 Y 轴系列选择(下拉+显隐)与图表保存截图 +依赖:models.chart 选项库、app_frame.UITheme +""" + +from __future__ import annotations + +import time +from typing import Callable, List, Optional, Sequence, Tuple + +from PySide2.QtCore import Qt, Signal +from PySide2.QtGui import QColor, QPainter, QPixmap +from PySide2.QtWidgets import ( + QCheckBox, + QComboBox, + QFileDialog, + QHBoxLayout, + QLabel, + QSizePolicy, + QWidget, +) + +from app_frame import UITheme +from models.chart import ( + CHART_SERIES_CATALOG, + DEFAULT_SERIES_SLOTS, + DEFAULT_SERIES_VISIBLE, + SERIES_SLOT_COUNT, + SERIES_SLOT_LABELS, +) + + +class ChartToolbar(QWidget): + """ + 一行两列:Y轴(L)/ Y轴(R)各「下拉选字段 + 显隐复选(同速度带 LAP 样式)」。 + 两个下拉共用同一选项池 CHART_SERIES_CATALOG;选中项分别挂到左/右 Y 轴。 + """ + + # [(field_id, visible), ...] 长度 2:索引 0=左轴,1=右轴 + seriesSelectionChanged = Signal(object) + + def __init__( + self, + parent=None, + catalog=None, + default_slots: Optional[Sequence[str]] = None, + default_visible: Optional[Sequence[bool]] = None, + ): + super().__init__(parent) + self._catalog = dict(catalog or CHART_SERIES_CATALOG) + self._get_save_caption: Optional[Callable] = None + self._save_target: Optional[Callable[[], QPixmap]] = None + self._save_enabled = True + self._updating = False + + slots = list(default_slots or DEFAULT_SERIES_SLOTS) + vis = list(default_visible or DEFAULT_SERIES_VISIBLE) + catalog_ids = list(self._catalog.keys()) + while len(slots) < SERIES_SLOT_COUNT: + slots.append(catalog_ids[len(slots) % len(catalog_ids)]) + while len(vis) < SERIES_SLOT_COUNT: + vis.append(True) + slots = slots[:SERIES_SLOT_COUNT] + vis = vis[:SERIES_SLOT_COUNT] + + row = QHBoxLayout(self) + row.setContentsMargins( + UITheme.scaled(4, 2), + UITheme.scaled(4, 2), + UITheme.scaled(4, 2), + UITheme.scaled(4, 2), + ) + row.setSpacing(UITheme.scaled(16, 8)) + self.setObjectName("ChartToolbar") + self.setAttribute(Qt.WA_StyledBackground, True) + + self._combos: List[QComboBox] = [] + self._checks: List[QCheckBox] = [] + for i in range(SERIES_SLOT_COUNT): + cell = QWidget(self) + cell.setObjectName("ChartToolbarCell") + cell.setAttribute(Qt.WA_StyledBackground, True) + cell_l = QHBoxLayout(cell) + cell_l.setContentsMargins(0, 0, 0, 0) + cell_l.setSpacing(UITheme.scaled(6, 3)) + + label = QLabel(SERIES_SLOT_LABELS[i]) + label.setObjectName("ChartHint") + + combo = QComboBox() + combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self._fill_combo(combo, slots[i]) + combo.currentIndexChanged.connect(self._emit_selection) + + check = QCheckBox("显示") + check.setChecked(bool(vis[i])) + check.toggled.connect(self._emit_selection) + + cell_l.addWidget(label, 0) + cell_l.addWidget(combo, 1) + cell_l.addWidget(check, 0) + row.addWidget(cell, 1) + self._combos.append(combo) + self._checks.append(check) + + def _fill_combo(self, combo: QComboBox, selected_id: str) -> None: + """两个下拉共用完整选项池。""" + combo.clear() + for opt in self._catalog.values(): + combo.addItem(opt.label, opt.id) + idx = combo.findData(selected_id) + combo.setCurrentIndex(idx if idx >= 0 else 0) + + def selection(self) -> List[Tuple[str, bool]]: + """当前左右槽 (field_id, visible)。""" + out: List[Tuple[str, bool]] = [] + for combo, check in zip(self._combos, self._checks): + field_id = str(combo.currentData() or "") + out.append((field_id, check.isChecked())) + return out + + def set_selection( + self, + slots: Sequence[Tuple[str, bool]], + ) -> None: + """回写左右槽选择。""" + self._updating = True + for i, combo in enumerate(self._combos): + if i >= len(slots): + break + field_id, visible = slots[i] + idx = combo.findData(field_id) + if idx >= 0: + combo.setCurrentIndex(idx) + self._checks[i].setChecked(bool(visible)) + self._updating = False + self._emit_selection() + + def set_catalog(self, catalog: dict) -> None: + """热更新选项库并重建两个下拉(共用全库)。""" + prev = self.selection() + self._catalog = dict(catalog) + self._updating = True + for i, combo in enumerate(self._combos): + cur = prev[i][0] if i < len(prev) else "" + self._fill_combo(combo, cur) + if i < len(prev): + self._checks[i].setChecked(prev[i][1]) + self._updating = False + self._emit_selection() + + def _emit_selection(self, *_args) -> None: + if self._updating: + return + self.seriesSelectionChanged.emit(self.selection()) + + def set_save_caption_provider(self, provider: Optional[Callable]) -> None: + """设置截图页眉圈时文案回调 → (rows, total_text)。""" + self._get_save_caption = provider + + def set_save_enabled(self, enabled: bool) -> None: + """能力开关:关闭后 save_chart 直接返回。""" + self._save_enabled = bool(enabled) + + def set_save_target(self, target: Callable[[], QPixmap]) -> None: + """设置截图目标(通常为 ChartDisplayArea.grab)。""" + self._save_target = target + + def save_chart(self) -> None: + """弹出另存对话框,合成页眉后写盘。""" + if not self._save_enabled or self._save_target is None: + return + default_name = time.strftime("chart_%Y%m%d_%H%M%S.png") + path, _ = QFileDialog.getSaveFileName( + self, + "保存图表", + default_name, + "PNG 图片 (*.png);;JPEG 图片 (*.jpg *.jpeg);;所有文件 (*.*)", + ) + if not path: + return + chart_pix = self._save_target() + lap_rows, total_text = [], "--" + if self._get_save_caption: + try: + lap_rows, total_text = self._get_save_caption() + except Exception: + lap_rows, total_text = [], "--" + header_h = 72 if lap_rows else 40 + out = QPixmap(chart_pix.width(), chart_pix.height() + header_h) + out.fill(QColor(UITheme.SURFACE)) + painter = QPainter(out) + painter.setPen(QColor(UITheme.FG)) + painter.setFont(UITheme.font_tech(UITheme.FONT_SM)) + y = 16 + if lap_rows: + for name, value in lap_rows: + painter.drawText(24, y, f"{name} {value}") + y += 16 + painter.setPen(QColor(UITheme.FG_TITLE)) + painter.setFont(UITheme.font(UITheme.FONT_SM)) + painter.drawText(out.width() - 160, 20, "NET(PENALTY)TOTAL") + painter.setPen(QColor(UITheme.FG)) + painter.setFont(UITheme.font_tech(UITheme.FONT_LG, bold=True)) + painter.drawText(out.width() - 160, 44, total_text or "--") + painter.drawPixmap(0, header_h, chart_pix) + painter.end() + out.save(path) diff --git a/charts/widgets/compare_style_panel.py b/charts/widgets/compare_style_panel.py new file mode 100644 index 00000000..e579a975 --- /dev/null +++ b/charts/widgets/compare_style_panel.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.widgets.compare_style_panel +职责:历史对比文件名显示 + 显隐复选(样式固定,无颜色/线型控件) +依赖:models.track_compare.CompareStyle、UITheme +""" + +from __future__ import annotations + +from PySide2.QtCore import Qt, Signal +from PySide2.QtWidgets import QCheckBox, QHBoxLayout, QLabel, QSizePolicy, QWidget + +from app_frame import UITheme +from models.track_compare import CompareStyle + + +class CompareStylePanel(QWidget): + """单行:完整文件名 +「显示历史数据」复选(同速度带 LAP 样式)。""" + + styleChanged = Signal(object) # CompareStyle + + def __init__(self, parent=None): + super().__init__(parent) + self.setObjectName("CompareStylePanel") + self._style = CompareStyle() + self._updating = False + + self.label = QLabel("") + self.label.setObjectName("ChartHint") + self.label.setStyleSheet("background: transparent;") + self.label.setWordWrap(False) + self.label.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) + self.label.setTextInteractionFlags(Qt.TextSelectableByMouse) + self.label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + + self.visible_cb = QCheckBox("显示历史数据") + self.visible_cb.setChecked(True) + self.visible_cb.setStyleSheet( + f"QCheckBox {{ color: {UITheme.FG}; background: transparent; }}" + ) + self.visible_cb.setToolTip("历史对比曲线显隐") + self.visible_cb.toggled.connect(self._emit_style) + + row = QHBoxLayout(self) + row.setContentsMargins( + UITheme.scaled(6, 4), + UITheme.scaled(4, 2), + UITheme.scaled(6, 4), + UITheme.scaled(4, 2), + ) + row.setSpacing(UITheme.scaled(10, 6)) + row.addWidget(self.label, 1) + row.addWidget(self.visible_cb, 0) + + self.setVisible(False) + + def style(self) -> CompareStyle: + """当前显隐 + 固定对比样式。""" + return CompareStyle(visible=self.visible_cb.isChecked()) + + def set_label(self, text: str) -> None: + """设置对比文件名(完整文件名)。""" + name = text or "" + self.label.setText(name) + self.label.setToolTip(name) + + def set_style(self, style: CompareStyle) -> None: + """回写显隐(不触发 styleChanged)。""" + self._updating = True + self._style = CompareStyle(visible=bool(style.visible)) + self.visible_cb.setChecked(bool(style.visible)) + self._updating = False + + def _emit_style(self, *_args) -> None: + if self._updating: + return + self.styleChanged.emit(self.style()) diff --git a/charts/widgets/pan_chart_view.py b/charts/widgets/pan_chart_view.py new file mode 100644 index 00000000..cbbedc38 --- /dev/null +++ b/charts/widgets/pan_chart_view.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.widgets.pan_chart_view +职责:拦截左键拖拽,交给 ChartSlot 平移回调 +依赖:charts.qtcharts.QChartView +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, Optional + +from charts.qtcharts import QChartView +from PySide2.QtCore import Qt +from PySide2.QtGui import QPainter + +if TYPE_CHECKING: + from PySide2.QtCore import QPoint + + +class PanChartView(QChartView): + """禁用橡皮筋,将左键 press/move/release 转发给槽位。""" + + def __init__(self, chart, on_press: Callable, on_move: Callable, on_release: Callable, parent=None): + super().__init__(chart, parent) + self._on_press = on_press + self._on_move = on_move + self._on_release = on_release + self.setRenderHint(QPainter.Antialiasing) + self.setRubberBand(QChartView.NoRubberBand) + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton: + self._on_press(event.pos()) + super().mousePressEvent(event) + + def mouseMoveEvent(self, event): + self._on_move(event.pos()) + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + if event.button() == Qt.LeftButton: + self._on_release() + super().mouseReleaseEvent(event) diff --git a/charts/widgets/speed_band_pane.py b/charts/widgets/speed_band_pane.py new file mode 100644 index 00000000..ee3e566c --- /dev/null +++ b/charts/widgets/speed_band_pane.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.widgets.speed_band_pane +职责:速度带柱状图面板 + LAP 过滤 + 空速/地速切换 +依赖:BarChart、ChartSlot、SlideToggle、models.speed_bands +""" + +from __future__ import annotations + +from typing import List, Optional, Sequence + +from PySide2.QtCore import Qt, Signal +from PySide2.QtWidgets import QCheckBox, QHBoxLayout, QLabel, QVBoxLayout, QWidget + +from app_frame import SlideToggle, UITheme +from charts.base.bar_chart import BarChart +from charts.container import ChartSlot +from models.lap_reference import LAP_COUNT +from models.speed_bands import ( + DEFAULT_SPEED_BANDS, + SPEED_SOURCE_AIRSPEED, + SPEED_SOURCE_GROUNDSPEED, +) +from models.track_compare import CompareStyle + + +class SpeedBandPane(QWidget): + """左下:堆叠柱状图 + 圈过滤复选框 + 空速/地速拨动开关。""" + + visibilityChanged = Signal() + speedSourceChanged = Signal(str) # airspeed | groundspeed + + def __init__(self, parent=None): + super().__init__(parent) + self.setObjectName("SpeedBandPane") + self._bar_chart = BarChart() + self.slot = ChartSlot(self._bar_chart, overlays=[], enable_pan=False) + # 6 档类目标签需要足够绘图区高度,过矮会被 Qt Charts 显示成 "..." + self.slot.setMinimumHeight(UITheme.scaled(200, 170)) + self._speed_source = SPEED_SOURCE_AIRSPEED + + row = QHBoxLayout() + row.setContentsMargins(0, UITheme.scaled(2, 1), 0, 0) + row.setSpacing(UITheme.scaled(8, 5)) + row.addStretch(1) + self._checks: List[QCheckBox] = [] + for i in range(LAP_COUNT): + cb = QCheckBox(f"显示LAP{i + 1}") + cb.setChecked(True) + cb.setStyleSheet( + f"QCheckBox {{ color: {UITheme.FG}; background: transparent; }}" + ) + cb.toggled.connect(self._on_toggled) + self._checks.append(cb) + row.addWidget(cb) + + ias_lbl = QLabel("IAS") + ias_lbl.setStyleSheet(f"color: {UITheme.FG}; background: transparent;") + ias_lbl.setAlignment(Qt.AlignVCenter | Qt.AlignRight) + gs_lbl = QLabel("GS") + gs_lbl.setStyleSheet(f"color: {UITheme.FG}; background: transparent;") + gs_lbl.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) + self._speed_toggle = SlideToggle(checked=False) + self._speed_toggle.toggled.connect(self._on_speed_toggled) + row.addWidget(ias_lbl) + row.addWidget(self._speed_toggle, 0, Qt.AlignVCenter) + row.addWidget(gs_lbl) + row.addStretch(1) + + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(self.slot, 1) + layout.addLayout(row) + + self._bar_chart.set_dwell_by_lap( + [[0.0] * len(DEFAULT_SPEED_BANDS) for _ in range(LAP_COUNT)], + [b.label for b in DEFAULT_SPEED_BANDS], + ) + self.slot.render() + + def speed_source(self) -> str: + return self._speed_source + + def set_speed_source(self, source: str) -> None: + """程序侧设置空速/地速,不重复发信号。""" + use_gs = source == SPEED_SOURCE_GROUNDSPEED + self._speed_source = ( + SPEED_SOURCE_GROUNDSPEED if use_gs else SPEED_SOURCE_AIRSPEED + ) + self._speed_toggle.blockSignals(True) + self._speed_toggle.setChecked(use_gs) + self._speed_toggle.blockSignals(False) + + def _on_speed_toggled(self, checked: bool = False) -> None: + self._speed_source = ( + SPEED_SOURCE_GROUNDSPEED if checked else SPEED_SOURCE_AIRSPEED + ) + self.speedSourceChanged.emit(self._speed_source) + + def _on_toggled(self, _checked: bool = False) -> None: + self._bar_chart.set_visible_laps(self.visible_laps()) + self.slot.render() + self.visibilityChanged.emit() + + def visible_laps(self) -> List[bool]: + """当前勾选的四圈可见性。""" + return [cb.isChecked() for cb in self._checks] + + def set_dwell_by_lap( + self, + dwell_by_lap: Sequence[Sequence[float]], + labels: Optional[Sequence[str]] = None, + ) -> None: + """写入当前会话速度带数据并重绘。""" + self._bar_chart.set_dwell_by_lap(dwell_by_lap, labels) + self._bar_chart.set_visible_laps(self.visible_laps()) + self.slot.render() + + def set_compare_dwell_by_lap( + self, + dwell_by_lap: Optional[Sequence[Sequence[float]]], + labels: Optional[Sequence[str]] = None, + ) -> None: + """写入/清除对比速度带数据。""" + self._bar_chart.set_compare_dwell_by_lap(dwell_by_lap, labels) + self.slot.render() + + def set_compare_style(self, style: CompareStyle) -> None: + """同步对比层样式。""" + self._bar_chart.set_compare_style(style) + self.slot.render() + + def set_time_axis_length(self, seconds: float) -> None: + """X 轴时长与主趋势视窗对齐。""" + self._bar_chart.set_time_axis_length(seconds) + self.slot.render() + + def reset_filters(self) -> None: + """四圈复选全部勾选并刷新。""" + for cb in self._checks: + cb.blockSignals(True) + cb.setChecked(True) + cb.blockSignals(False) + self._on_toggled() diff --git a/charts/widgets/stick_input.py b/charts/widgets/stick_input.py new file mode 100644 index 00000000..f61d5e34 --- /dev/null +++ b/charts/widgets/stick_input.py @@ -0,0 +1,631 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.widgets.stick_input +职责:俯仰/滚转田字格 + 油门/偏航填充 + 右侧设备下拉与数值列 +依赖:models.hardware_monitor、UITheme、services.simulator.DATA_POLL_MS +""" + +from __future__ import annotations + +import time +from typing import List, Optional, Tuple + +from PySide2.QtCore import QPointF, QRectF, Qt, Signal +from PySide2.QtGui import QColor, QFont, QPainter, QPen +from PySide2.QtWidgets import ( + QComboBox, + QHBoxLayout, + QLabel, + QSizePolicy, + QVBoxLayout, + QWidget, +) + +from app_frame import UITheme +from charts.theme import chart_surface_color, chart_well_border_color +from models.hardware_monitor import ( + DeviceIdentity, + JoystickInfo, + StickAxes, + list_joysticks, + load_config, + save_config, +) + +_LAYOUT_SCALE = 0.8 +# 设备列表轻量刷新间隔(不 quit/init);完整重扫仅在无设备时触发 +_DEVICE_SOFT_SCAN_S = 2.0 + + +def _clamp(v: float, lo: float = -1.0, hi: float = 1.0) -> float: + return max(lo, min(hi, float(v))) + + +def _axes_key( + pitch: float, + roll: float, + yaw: float, + throttle: float, + connected: bool, +) -> Tuple[float, float, float, float, bool]: + # 量化到显示精度,避免浮点抖动触发无意义重绘 + return ( + round(float(pitch), 2), + round(float(roll), 2), + round(float(yaw), 2), + round(float(throttle), 2), + bool(connected), + ) + + +class StickPlotWidget(QWidget): + """左侧操纵示意:油门 / 田字格 / 偏航(可叠画历史层)。""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.setMinimumSize(UITheme.scaled(96, 72), UITheme.scaled(96, 72)) + self._pitch = 0.0 + self._roll = 0.0 + self._yaw = 0.0 + self._throttle = 0.0 + self._connected = False + self._state_key = _axes_key(0, 0, 0, 0, False) + self._cmp_pitch = 0.0 + self._cmp_roll = 0.0 + self._cmp_yaw = 0.0 + self._cmp_throttle = 0.0 + self._cmp_connected = False + self._cmp_key = _axes_key(0, 0, 0, 0, False) + + def set_axes( + self, + pitch: float = 0.0, + roll: float = 0.0, + yaw: float = 0.0, + throttle: float = 0.0, + connected: bool = True, + ) -> None: + pitch = _clamp(pitch) + roll = _clamp(roll) + yaw = _clamp(yaw) + throttle = _clamp(throttle) + connected = bool(connected) + key = _axes_key(pitch, roll, yaw, throttle, connected) + if key == self._state_key: + return + self._pitch = pitch + self._roll = roll + self._yaw = yaw + self._throttle = throttle + self._connected = connected + self._state_key = key + self.update() + + def set_compare_axes( + self, + pitch: float = 0.0, + roll: float = 0.0, + yaw: float = 0.0, + throttle: float = 0.0, + connected: bool = True, + ) -> None: + pitch = _clamp(pitch) + roll = _clamp(roll) + yaw = _clamp(yaw) + throttle = _clamp(throttle) + connected = bool(connected) + key = _axes_key(pitch, roll, yaw, throttle, connected) + if key == self._cmp_key: + return + self._cmp_pitch = pitch + self._cmp_roll = roll + self._cmp_yaw = yaw + self._cmp_throttle = throttle + self._cmp_connected = connected + self._cmp_key = key + self.update() + + def clear(self) -> None: + self.set_axes(0.0, 0.0, 0.0, 0.0, connected=False) + + def clear_compare(self) -> None: + self.set_compare_axes(0.0, 0.0, 0.0, 0.0, connected=False) + + def paintEvent(self, _event) -> None: + p = QPainter(self) + p.setRenderHint(QPainter.Antialiasing, True) + bg = chart_surface_color() + # 先铺满整个控件,避免上下/外圈露出父级 BG 黑框 + p.fillRect(self.rect(), bg) + outer = QRectF(self.rect()).adjusted(2, 2, -2, -2) + if outer.width() < 28 or outer.height() < 28: + return + + border = chart_well_border_color() + muted = QColor(UITheme.FG_MUTED) + accent = QColor(UITheme.CHART_PITCH) + hist = QColor(UITheme.FLASH_BLUE) + hist.setAlphaF(0.70) + # 参数井区与所在容器同色(SURFACE),仅靠边框区分 + well = bg + + gap = max(3.0, min(outer.width(), outer.height()) * 0.035) + rail = max(8.0, min(outer.width() * 0.14, outer.height() * 0.14)) + avail_w = outer.width() - rail - gap + avail_h = outer.height() - rail - gap + side = min(avail_w, avail_h) * _LAYOUT_SCALE + rail_s = rail * _LAYOUT_SCALE + gap_s = gap * _LAYOUT_SCALE + + block_w = rail_s + gap_s + side + block_h = side + gap_s + rail_s + ox = outer.left() + max(0.0, (outer.width() - block_w) * 0.5) + oy = outer.top() + max(0.0, (outer.height() - block_h) * 0.5) + + throttle_rect = QRectF(ox, oy, rail_s, side) + grid_rect = QRectF(ox + rail_s + gap_s, oy, side, side) + yaw_rect = QRectF(grid_rect.left(), grid_rect.bottom() + gap_s, side, rail_s) + + # 底板只画一次;历史在下、当前在上 + self._draw_throttle_frame(p, throttle_rect, well, border) + self._draw_cross_frame(p, grid_rect, well, border, muted) + self._draw_yaw_frame(p, yaw_rect, well, border, accent) + if self._cmp_connected: + self._draw_throttle_fill( + p, throttle_rect, self._cmp_throttle, hist, draw_frame=False + ) + self._draw_cross_mark( + p, grid_rect, self._cmp_roll, self._cmp_pitch, hist + ) + self._draw_yaw_fill(p, yaw_rect, self._cmp_yaw, hist, draw_frame=False) + if self._connected: + self._draw_throttle_fill( + p, throttle_rect, self._throttle, accent, draw_frame=False + ) + # 当前监测俯仰相对硬件读数取反,使推杆方向与示意一致 + self._draw_cross_mark(p, grid_rect, self._roll, -self._pitch, accent) + self._draw_yaw_fill(p, yaw_rect, self._yaw, accent, draw_frame=False) + + def _draw_cross_frame(self, p, r, well, border, muted) -> None: + p.setPen(QPen(border, 1.0)) + p.setBrush(well) + p.drawRect(r) + cx, cy = r.center().x(), r.center().y() + p.setPen(QPen(muted, 1.0, Qt.DashLine)) + p.drawLine(QPointF(cx, r.top()), QPointF(cx, r.bottom())) + p.drawLine(QPointF(r.left(), cy), QPointF(r.right(), cy)) + p.drawLine(QPointF(r.left(), cy - r.height() * 0.25), QPointF(r.right(), cy - r.height() * 0.25)) + p.drawLine(QPointF(r.left(), cy + r.height() * 0.25), QPointF(r.right(), cy + r.height() * 0.25)) + p.drawLine(QPointF(cx - r.width() * 0.25, r.top()), QPointF(cx - r.width() * 0.25, r.bottom())) + p.drawLine(QPointF(cx + r.width() * 0.25, r.top()), QPointF(cx + r.width() * 0.25, r.bottom())) + + def _draw_cross_mark(self, p, r, roll, pitch, color) -> None: + cx, cy = r.center().x(), r.center().y() + pad = max(4.0, min(r.width(), r.height()) * 0.06) + arm = max(6.0, min(r.width(), r.height()) * 0.08) + x = cx + roll * (r.width() * 0.5 - pad) + y = cy - pitch * (r.height() * 0.5 - pad) + p.setPen(QPen(color, 1.8)) + p.drawLine(QPointF(x - arm, y), QPointF(x + arm, y)) + p.drawLine(QPointF(x, y - arm), QPointF(x, y + arm)) + + def _draw_throttle_frame(self, p, r, well, border) -> None: + p.setPen(QPen(border, 1.0)) + p.setBrush(well) + p.drawRect(r) + + def _draw_throttle_fill(self, p, r, value, color, *, draw_frame: bool = True) -> None: + if draw_frame: + self._draw_throttle_frame(p, r, chart_surface_color(), chart_well_border_color()) + ratio = _clamp((float(value) + 1.0) * 0.5, 0.0, 1.0) + inner_h = max(0.0, r.height() - 2.0) + fill_h = ratio * inner_h + if fill_h <= 0.5: + return + fill = QRectF(r.left() + 1.0, r.bottom() - 1.0 - fill_h, r.width() - 2.0, fill_h) + fill_c = QColor(color) + # 历史层已带 0.7 alpha;当前层沿用半透明填充 + if fill_c.alphaF() >= 0.99: + fill_c.setAlphaF(0.55) + p.fillRect(fill, fill_c) + + def _draw_yaw_frame(self, p, r, well, border, center_color) -> None: + p.setPen(QPen(border, 1.0)) + p.setBrush(well) + p.drawRect(r) + cx = r.center().x() + p.setPen(QPen(center_color, 1.5)) + p.drawLine(QPointF(cx, r.top() + 1.0), QPointF(cx, r.bottom() - 1.0)) + + def _draw_yaw_fill(self, p, r, value, color, *, draw_frame: bool = True) -> None: + if draw_frame: + self._draw_yaw_frame( + p, r, chart_surface_color(), chart_well_border_color(), color + ) + cx = r.center().x() + x = cx + value * (r.width() * 0.5 - 1.0) + fill = QRectF(min(x, cx), r.top() + 1.0, max(1.0, abs(x - cx)), r.height() - 2.0) + fill_c = QColor(color) + if fill_c.alphaF() >= 0.99: + fill_c.setAlphaF(0.55) + p.fillRect(fill, fill_c) + # 中线保持可见 + p.setPen(QPen(QColor(UITheme.CHART_PITCH), 1.5)) + p.drawLine(QPointF(cx, r.top() + 1.0), QPointF(cx, r.bottom() - 1.0)) + + +class StickValuesWidget(QWidget): + """右侧数值:标签黄色;当前值前景色;可选历史列亮蓝。""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) + self.setMinimumWidth(UITheme.scaled(108, 88)) + self._pitch = 0.0 + self._roll = 0.0 + self._yaw = 0.0 + self._throttle = 0.0 + self._connected = False + self._state_key = _axes_key(0, 0, 0, 0, False) + self._cmp_pitch = 0.0 + self._cmp_roll = 0.0 + self._cmp_yaw = 0.0 + self._cmp_throttle = 0.0 + self._cmp_connected = False + self._cmp_key = _axes_key(0, 0, 0, 0, False) + + def set_axes( + self, + pitch: float = 0.0, + roll: float = 0.0, + yaw: float = 0.0, + throttle: float = 0.0, + connected: bool = True, + ) -> None: + # 数值区显示原始读数,不做 [-1,1] 夹紧 + pitch = float(pitch) + roll = float(roll) + yaw = float(yaw) + throttle = float(throttle) + connected = bool(connected) + key = _axes_key(pitch, roll, yaw, throttle, connected) + if key == self._state_key: + return + self._pitch = pitch + self._roll = roll + self._yaw = yaw + self._throttle = throttle + self._connected = connected + self._state_key = key + self.update() + + def set_compare_axes( + self, + pitch: float = 0.0, + roll: float = 0.0, + yaw: float = 0.0, + throttle: float = 0.0, + connected: bool = True, + ) -> None: + pitch = float(pitch) + roll = float(roll) + yaw = float(yaw) + throttle = float(throttle) + connected = bool(connected) + key = _axes_key(pitch, roll, yaw, throttle, connected) + if key == self._cmp_key: + return + was = self._cmp_connected + self._cmp_pitch = pitch + self._cmp_roll = roll + self._cmp_yaw = yaw + self._cmp_throttle = throttle + self._cmp_connected = connected + self._cmp_key = key + if was != connected: + self.setMinimumWidth( + UITheme.scaled(168, 140) if connected else UITheme.scaled(108, 88) + ) + self.update() + + def clear(self) -> None: + self.set_axes(0.0, 0.0, 0.0, 0.0, connected=False) + + def clear_compare(self) -> None: + self.set_compare_axes(0.0, 0.0, 0.0, 0.0, connected=False) + + def paintEvent(self, _event) -> None: + p = QPainter(self) + p.setRenderHint(QPainter.Antialiasing, True) + # 与所在容器同色,避免数值区衬底跳色 + p.fillRect(self.rect(), chart_surface_color()) + r = QRectF(self.rect()).adjusted(2, 2, -2, -2) + label_c = QColor(UITheme.CHART_PITCH) + value_c = QColor(UITheme.FG) + hist_c = QColor(UITheme.FLASH_BLUE) + hist_c.setAlphaF(0.70) + muted = QColor(UITheme.FG_MUTED) + show_hist = bool(self._cmp_connected) + rows = ( + ("Pitch", -self._pitch, self._cmp_pitch), + ("Roll", self._roll, self._cmp_roll), + ("Yaw", self._yaw, self._cmp_yaw), + ("Throttle", self._throttle, self._cmp_throttle), + ) + font = QFont(UITheme.FONT_FAMILY_TECH) + font.setPixelSize(max(9, UITheme.scaled(10, 9))) + p.setFont(font) + + # 表头:当前 / 历史 + header_h = UITheme.scaled(14, 12) if show_hist else 0.0 + body = QRectF(r.left(), r.top() + header_h, r.width(), r.height() - header_h) + label_w = min(body.width() * (0.38 if show_hist else 0.55), UITheme.scaled(64, 52)) + val_w = (body.width() - label_w) / (2.0 if show_hist else 1.0) + if show_hist: + p.setPen(QPen(value_c)) + cur_hdr = QRectF(body.left() + label_w, r.top(), val_w, header_h) + hist_hdr = QRectF(body.left() + label_w + val_w, r.top(), val_w, header_h) + p.drawText(cur_hdr, Qt.AlignLeft | Qt.AlignVCenter, "当前") + p.setPen(QPen(hist_c)) + p.drawText(hist_hdr, Qt.AlignLeft | Qt.AlignVCenter, "历史") + + row_h = body.height() / len(rows) + for i, (name, val, cmp_val) in enumerate(rows): + cell = QRectF(body.left(), body.top() + i * row_h, body.width(), row_h) + label_rect = QRectF(cell.left(), cell.top(), label_w, cell.height()) + cur_rect = QRectF(cell.left() + label_w, cell.top(), val_w, cell.height()) + p.setPen(QPen(label_c)) + p.drawText(label_rect, Qt.AlignLeft | Qt.AlignVCenter, name) + p.setPen(QPen(value_c if self._connected else muted)) + text = f"{val:+.3f}" if self._connected else "--" + p.drawText(cur_rect, Qt.AlignLeft | Qt.AlignVCenter, text) + if show_hist: + hist_rect = QRectF( + cell.left() + label_w + val_w, cell.top(), val_w, cell.height() + ) + p.setPen(QPen(hist_c)) + p.drawText(hist_rect, Qt.AlignLeft | Qt.AlignVCenter, f"{cmp_val:+.3f}") + + +class StickInputPane(QWidget): + """操纵输入面板:左图 + 右列(下拉 / 数值 / 提示)。""" + + deviceChanged = Signal(object) # Optional[DeviceIdentity] + + def __init__(self, parent=None): + super().__init__(parent) + self.setObjectName("StickInputPane") + self.setAttribute(Qt.WA_StyledBackground, True) + self._devices: List[JoystickInfo] = [] + self._device_fp: tuple = () + self._updating_combo = False + self._last_scan_t = 0.0 + self._hint_kind = "" + + self.plot = StickPlotWidget(self) + self.values = StickValuesWidget(self) + + self._device_combo = QComboBox() + self._device_combo.setObjectName("StickDeviceCombo") + self._device_combo.setMinimumHeight(UITheme.scaled(26, 22)) + self._device_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self._device_combo.currentIndexChanged.connect(self._on_device_combo_changed) + + self._hint = QLabel("") + self._hint.setObjectName("Muted") + self._hint.setWordWrap(True) + self._hint.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self._hint.setStyleSheet("background: transparent;") + + right = QWidget(self) + right.setObjectName("StickValuesColumn") + right.setAttribute(Qt.WA_StyledBackground, True) + right.setStyleSheet( + f"QWidget#StickValuesColumn {{ background-color: {UITheme.SURFACE}; border: none; }}" + ) + right.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) + right.setMinimumWidth(UITheme.scaled(120, 96)) + right.setMaximumWidth(UITheme.scaled(220, 180)) + right_l = QVBoxLayout(right) + right_l.setContentsMargins(0, 0, 0, 0) + right_l.setSpacing(UITheme.scaled(4, 2)) + right_l.addWidget(self._device_combo, 0) + right_l.addWidget(self.values, 1) + right_l.addWidget(self._hint, 0) + + root = QHBoxLayout(self) + root.setContentsMargins(0, 0, 0, 0) + root.setSpacing(UITheme.scaled(6, 4)) + root.addWidget(self.plot, 3) + root.addWidget(right, 2) + + # 兼容旧属性名 + self.widget = self.plot + # 构造期不做 rescan(quit/init),避免与后续 HardwareMonitor 抢摇杆子系统 + self.refresh_devices(rescan=False) + self._sync_hint() + + @staticmethod + def _fingerprint(devices: List[JoystickInfo]) -> tuple: + return tuple((d.guid or d.name, d.index, d.num_axes) for d in devices) + + def _set_hint(self, kind: str, text: str) -> None: + if kind == self._hint_kind and self._hint.text() == text: + return + self._hint_kind = kind + self._hint.setText(text) + self._hint.setVisible(bool(text)) + + def _sync_hint(self) -> None: + if not self._devices or self._device_combo.count() <= 0: + self._set_hint("none", "未检测到摇杆,请连接 USB 控制器后等待自动刷新") + return + if self._current_device() is None: + self._set_hint("unselected", "请在上方下拉菜单中选择要监测的摇杆") + return + self._set_hint("", "") + + def refresh_devices(self, rescan: bool = False) -> None: + """刷新下拉列表。rescan=True 会重启 pygame 摇杆子系统,仅在无设备时使用。""" + devices = list_joysticks(rescan=rescan) + fp = self._fingerprint(devices) + self._last_scan_t = time.monotonic() + if fp == self._device_fp and (bool(devices) == (self._device_combo.count() > 0)): + self._devices = list(devices) + self._sync_hint() + return + + self._devices = list(devices) + self._device_fp = fp + cfg = load_config() + + self._updating_combo = True + self._device_combo.blockSignals(True) + self._device_combo.clear() + if not self._devices: + self._device_combo.blockSignals(False) + self._updating_combo = False + self._apply_disconnected() + self._sync_hint() + return + + sel = 0 + matched = False + for i, d in enumerate(self._devices): + self._device_combo.addItem(d.name, d.index) + if cfg.device.matches(d.identity()): + sel = i + matched = True + self._device_combo.setCurrentIndex(sel) + self._device_combo.blockSignals(False) + self._updating_combo = False + + chosen = self._devices[sel] + if not matched: + self._commit_device(chosen) + self._sync_hint() + + def _maybe_refresh_devices(self) -> None: + now = time.monotonic() + if now - self._last_scan_t < _DEVICE_SOFT_SCAN_S: + return + # 有设备时禁止 quit/init,避免读数闪断 + need_rescan = self._device_combo.count() <= 0 + self.refresh_devices(rescan=need_rescan) + + def _current_device(self) -> Optional[JoystickInfo]: + if self._device_combo.count() <= 0: + return None + idx = self._device_combo.currentData() + if idx is None: + return None + for d in self._devices: + if d.index == int(idx): + return d + return None + + def _commit_device(self, device: JoystickInfo) -> None: + cfg = load_config() + if cfg.device.matches(device.identity()) and cfg.device.guid == device.guid: + # 已是同一设备,避免无谓 save 触发 monitor 热重载闪烁 + if cfg.device.index == device.index and cfg.device.name == device.name: + return + cfg.device = device.identity() + save_config(cfg) + self.deviceChanged.emit(device.identity()) + + def _on_device_combo_changed(self, _index: int = 0) -> None: + if self._updating_combo: + return + dev = self._current_device() + if dev is None: + self._apply_disconnected() + self.deviceChanged.emit(None) + self._sync_hint() + return + self._commit_device(dev) + self._sync_hint() + + def _apply_disconnected(self) -> None: + self.plot.clear() + self.values.clear() + + def _apply_axes( + self, + pitch: float, + roll: float, + yaw: float, + throttle: float, + connected: bool, + ) -> None: + self.plot.set_axes(pitch, roll, yaw, throttle, connected) + self.values.set_axes(pitch, roll, yaw, throttle, connected) + + def _apply_compare_axes( + self, + pitch: float, + roll: float, + yaw: float, + throttle: float, + connected: bool, + ) -> None: + self.plot.set_compare_axes(pitch, roll, yaw, throttle, connected) + self.values.set_compare_axes(pitch, roll, yaw, throttle, connected) + + def set_axes( + self, + pitch: float = 0.0, + roll: float = 0.0, + yaw: float = 0.0, + throttle: float = 0.0, + connected: bool = True, + ) -> None: + self._maybe_refresh_devices() + if self._device_combo.count() <= 0 or self._current_device() is None: + self._apply_disconnected() + self._sync_hint() + return + self._apply_axes(pitch, roll, yaw, throttle, connected) + self._sync_hint() + + def set_stick(self, axes: StickAxes) -> None: + self._maybe_refresh_devices() + if self._device_combo.count() <= 0 or self._current_device() is None: + self._apply_disconnected() + self._sync_hint() + return + if not axes.connected: + self._apply_disconnected() + self._set_hint("lost", "摇杆已断开或暂时无法读取,请检查连接") + return + self._apply_axes(axes.pitch, axes.roll, axes.yaw, axes.throttle, True) + self._sync_hint() + + def set_compare_axes( + self, + pitch: float = 0.0, + roll: float = 0.0, + yaw: float = 0.0, + throttle: float = 0.0, + connected: bool = True, + ) -> None: + """叠画历史对比摇杆(不依赖本机设备选中状态)。""" + self._apply_compare_axes(pitch, roll, yaw, throttle, connected) + + def set_compare_stick(self, axes: Optional[StickAxes]) -> None: + if axes is None or not axes.connected: + self.clear_compare() + return + self.set_compare_axes( + axes.pitch, axes.roll, axes.yaw, axes.throttle, connected=True + ) + + def clear_compare(self) -> None: + self.plot.clear_compare() + self.values.clear_compare() + + def clear(self) -> None: + self._apply_disconnected() + self.clear_compare() + self._sync_hint() diff --git a/charts/widgets/timeline_scrubber.py b/charts/widgets/timeline_scrubber.py new file mode 100644 index 00000000..fecce39b --- /dev/null +++ b/charts/widgets/timeline_scrubber.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.widgets.timeline_scrubber +职责:时间轴滑条 —— 波音737顶视剪影自左向右滑行(可聚焦、键盘控制) +依赖:UITheme +""" + +from __future__ import annotations + +from PySide2.QtCore import QRectF, Qt +from PySide2.QtGui import QColor, QPainter, QPainterPath, QPen +from PySide2.QtWidgets import QAbstractSlider, QSizePolicy, QWidget + +from app_frame import UITheme + + +def _plane_path() -> QPainterPath: + """波音737 经典顶视剪影:机头朝右(飞行方向左→右)。单位盒 0..1 × 0..1。""" + p = QPainterPath() + # 自机头顺时针一圈:细长机身 + 后掠主翼 + 翼下发动机轮廓 + 平尾 + p.moveTo(0.98, 0.50) + p.cubicTo(0.96, 0.46, 0.90, 0.44, 0.84, 0.435) + p.lineTo(0.62, 0.435) + p.lineTo(0.48, 0.12) + p.lineTo(0.40, 0.10) + p.lineTo(0.38, 0.14) + p.lineTo(0.46, 0.38) + p.cubicTo(0.44, 0.40, 0.42, 0.42, 0.44, 0.445) + p.lineTo(0.28, 0.445) + p.lineTo(0.14, 0.28) + p.lineTo(0.08, 0.28) + p.lineTo(0.12, 0.445) + p.lineTo(0.04, 0.46) + p.cubicTo(0.01, 0.48, 0.01, 0.52, 0.04, 0.54) + p.lineTo(0.12, 0.555) + p.lineTo(0.08, 0.72) + p.lineTo(0.14, 0.72) + p.lineTo(0.28, 0.555) + p.lineTo(0.44, 0.555) + p.cubicTo(0.42, 0.58, 0.44, 0.60, 0.46, 0.62) + p.lineTo(0.38, 0.86) + p.lineTo(0.40, 0.90) + p.lineTo(0.48, 0.88) + p.lineTo(0.62, 0.565) + p.lineTo(0.84, 0.565) + p.cubicTo(0.90, 0.56, 0.96, 0.54, 0.98, 0.50) + p.closeSubpath() + return p + + +class TimelineScrubber(QAbstractSlider): + """横向时间轴:细航线 + 波音737顶视剪影飞机手柄(机头朝右)。""" + + def __init__(self, parent: QWidget = None, *, track_color: str = "#122536"): + super().__init__(parent) + self.setOrientation(Qt.Horizontal) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.setMinimumHeight(UITheme.scaled(30, 24)) + self.setMaximumHeight(UITheme.scaled(40, 32)) + self.setMouseTracking(True) + self.setFocusPolicy(Qt.StrongFocus) + self.setSingleStep(1) + self.setPageStep(10) + self._track_color = QColor(track_color) + self._dragging = False + self._hover = False + self._plane = _plane_path() + self._plane.setFillRule(Qt.WindingFill) + + def set_track_color(self, color: str) -> None: + self._track_color = QColor(color) + self.update() + + def _plane_size(self) -> tuple: + h = max(18.0, self.height() * 0.88) + w = h * 1.25 + return w, h + + def _groove_rect(self) -> QRectF: + margin_x = self._plane_size()[0] * 0.5 + cy = self.height() * 0.5 + gh = max(3.0, UITheme.scaled(4, 3)) + return QRectF( + margin_x, + cy - gh * 0.5, + max(1.0, self.width() - 2.0 * margin_x), + gh, + ) + + def _value_to_x(self, value: int) -> float: + groove = self._groove_rect() + span = max(1, self.maximum() - self.minimum()) + t = (float(value) - float(self.minimum())) / float(span) + t = max(0.0, min(1.0, t)) + return groove.left() + t * groove.width() + + def _x_to_value(self, x: float) -> int: + groove = self._groove_rect() + if groove.width() <= 1e-6: + return int(self.minimum()) + t = (float(x) - groove.left()) / groove.width() + t = max(0.0, min(1.0, t)) + span = max(0, self.maximum() - self.minimum()) + return int(round(self.minimum() + t * span)) + + def _plane_rect(self) -> QRectF: + w, h = self._plane_size() + cx = self._value_to_x(self.value()) + cy = self.height() * 0.5 + return QRectF(cx - w * 0.5, cy - h * 0.5, w, h) + + def paintEvent(self, _event) -> None: + p = QPainter(self) + p.setRenderHint(QPainter.Antialiasing, True) + bg = self._track_color + p.fillRect(self.rect(), bg) + + groove = self._groove_rect() + enabled = self.isEnabled() and self.maximum() > self.minimum() + line_c = QColor(UITheme.TOGGLE_TRACK_BORDER if enabled else UITheme.BLUE_MID) + fill_c = QColor(UITheme.ACCENT if enabled else UITheme.BLUE_MID) + fill_c.setAlphaF(0.35 if enabled else 0.18) + + p.setPen(QPen(line_c, 1.0)) + p.setBrush(bg) + p.drawRoundedRect(groove, groove.height() * 0.5, groove.height() * 0.5) + + cx = self._value_to_x(self.value()) + flown = QRectF(groove.left(), groove.top(), max(0.0, cx - groove.left()), groove.height()) + if flown.width() > 0.5: + p.setPen(Qt.NoPen) + p.setBrush(fill_c) + p.drawRoundedRect(flown, groove.height() * 0.5, groove.height() * 0.5) + + plane_c = QColor(UITheme.FG_ON_ACCENT if enabled else UITheme.FG_MUTED) + focused = self.hasFocus() + if self._hover or self._dragging or focused: + plane_c = QColor(UITheme.FLASH_BLUE if enabled else UITheme.FG_MUTED) + body = QColor(UITheme.ACCENT if enabled else UITheme.BLUE_MID) + if self._hover or self._dragging or focused: + body = QColor(UITheme.ACCENT_HOVER if enabled else UITheme.BLUE_MID) + + pr = self._plane_rect() + p.save() + p.translate(pr.topLeft()) + p.scale(pr.width(), pr.height()) + p.setPen(QPen(plane_c, 0.03)) + p.setBrush(body) + p.drawPath(self._plane) + p.setPen(QPen(plane_c, 0.04)) + p.setBrush(Qt.NoBrush) + p.drawPath(self._plane) + p.restore() + + if focused: + focus_c = QColor(UITheme.FLASH_BLUE) + focus_c.setAlphaF(0.85) + p.setPen(QPen(focus_c, 1.2, Qt.DashLine)) + p.setBrush(Qt.NoBrush) + p.drawRoundedRect(QRectF(self.rect()).adjusted(1.5, 1.5, -1.5, -1.5), 3, 3) + + def focusInEvent(self, event) -> None: + super().focusInEvent(event) + self.update() + + def focusOutEvent(self, event) -> None: + super().focusOutEvent(event) + self.update() + + def enterEvent(self, _event) -> None: + self._hover = True + self.update() + + def leaveEvent(self, _event) -> None: + self._hover = False + self.update() + + def keyPressEvent(self, event) -> None: + if not self.isEnabled() or self.maximum() <= self.minimum(): + super().keyPressEvent(event) + return + key = event.key() + mods = event.modifiers() + step = max(1, int(self.singleStep())) + page = max(step, int(self.pageStep())) + if mods & Qt.ControlModifier: + step = max(step * 5, page) + if key in (Qt.Key_Left, Qt.Key_Down): + self.setValue(self.value() - step) + elif key in (Qt.Key_Right, Qt.Key_Up): + self.setValue(self.value() + step) + elif key == Qt.Key_PageDown: + self.setValue(self.value() - page) + elif key == Qt.Key_PageUp: + self.setValue(self.value() + page) + elif key == Qt.Key_Home: + self.setValue(self.minimum()) + elif key == Qt.Key_End: + self.setValue(self.maximum()) + elif key == Qt.Key_Minus: + self.setValue(self.value() - step) + elif key in (Qt.Key_Plus, Qt.Key_Equal): + self.setValue(self.value() + step) + else: + super().keyPressEvent(event) + return + event.accept() + self.update() + + def mousePressEvent(self, event) -> None: + if event.button() != Qt.LeftButton or not self.isEnabled(): + return + self.setFocus(Qt.MouseFocusReason) + if self.maximum() <= self.minimum(): + return + self._dragging = True + self.setSliderDown(True) + self.setValue(self._x_to_value(event.pos().x())) + self.update() + + def mouseMoveEvent(self, event) -> None: + if self._dragging: + self.setValue(self._x_to_value(event.pos().x())) + self.update() + + def mouseReleaseEvent(self, event) -> None: + if event.button() == Qt.LeftButton and self._dragging: + self._dragging = False + self.setSliderDown(False) + self.setValue(self._x_to_value(event.pos().x())) + self.update() diff --git a/charts/widgets/track_map_pane.py b/charts/widgets/track_map_pane.py new file mode 100644 index 00000000..04b71e02 --- /dev/null +++ b/charts/widgets/track_map_pane.py @@ -0,0 +1,765 @@ +# -*- coding: utf-8 -*- +""" +模块:charts.widgets.track_map_pane +职责:赛道航迹连续线(全量速度色标)+ 进度游标 + LAP 过滤 + IAS/GS +依赖:models.track_map、SlideToggle、UITheme +""" + +from __future__ import annotations + +from typing import List, Optional, Sequence, Tuple + +from PySide2.QtCore import QPointF, QRectF, Qt, Signal +from PySide2.QtGui import QColor, QFont, QLinearGradient, QPainter, QPen +from PySide2.QtWidgets import ( + QCheckBox, + QHBoxLayout, + QLabel, + QSizePolicy, + QVBoxLayout, + QWidget, +) + +from app_frame import SlideToggle, UITheme +from charts.theme import chart_surface_color, chart_well_border_color, desaturate_color +from models.lap_reference import LAP_COUNT +from models.speed_bands import SPEED_SOURCE_AIRSPEED, SPEED_SOURCE_GROUNDSPEED +from models.track_compare import CompareStyle +from models.track_map import ( + TrackPoint, + compute_bounds, + filter_by_visible_laps, + fit_draw_rect, + project_to_pixel, + speed_range, + speed_tick_marks, + speed_to_rgb, +) + +# 当前轨迹更细、半透明,便于与历史重叠区分 +_LINE_WIDTH_LIVE = 3.2 +_LINE_WIDTH_COMPARE = 5.0 +_LINE_WIDTH_BG_LIVE = 6.0 +_LINE_WIDTH_BG_COMPARE = 9.0 +_LIVE_OPACITY = 0.9 + + +class TrackMapCanvas(QWidget): + """航迹连续线:全量速度色标;滚轮缩放、拖拽平移;游标高亮。""" + + _ZOOM_MIN = 1.0 + _ZOOM_MAX = 40.0 + _ZOOM_STEP = 1.15 + + cursorIndexChanged = Signal(int) + + def __init__(self, parent=None): + super().__init__(parent) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.setMinimumHeight(UITheme.scaled(140, 120)) + self.setMouseTracking(True) + self.setFocusPolicy(Qt.WheelFocus) + self.setCursor(Qt.OpenHandCursor) + self._points: List[TrackPoint] = [] + self._compare: List[TrackPoint] = [] + # 默认仅显示 LAP1 + self._visible_laps: List[bool] = [i == 0 for i in range(LAP_COUNT)] + self._speed_source = SPEED_SOURCE_AIRSPEED + self._compare_style = CompareStyle() + self._compare_visible = True + self._zoom = 1.0 + self._pan_x = 0.0 + self._pan_y = 0.0 + self._dragging = False + self._drag_last: Optional[QPointF] = None + self._cursor_index = 0 + self._pix_cache: List[Tuple[float, float]] = [] + + def set_points(self, points: Sequence[TrackPoint]) -> None: + self._points = list(points or []) + self._clamp_cursor() + self.update() + + def set_compare_points(self, points: Optional[Sequence[TrackPoint]]) -> None: + self._compare = list(points or []) + self.update() + + def set_visible_laps(self, visible: Sequence[bool]) -> None: + self._visible_laps = list(visible)[:LAP_COUNT] + while len(self._visible_laps) < LAP_COUNT: + self._visible_laps.append(False) + self._clamp_cursor() + self.update() + + def set_speed_source(self, source: str) -> None: + self._speed_source = ( + SPEED_SOURCE_GROUNDSPEED + if source == SPEED_SOURCE_GROUNDSPEED + else SPEED_SOURCE_AIRSPEED + ) + self.update() + + def set_compare_style(self, style: CompareStyle) -> None: + self._compare_style = style + self._compare_visible = bool(getattr(style, "visible", True)) + self.update() + + def clear(self) -> None: + self._points = [] + self._cursor_index = 0 + self.reset_view() + + def reset_view(self) -> None: + self._zoom = 1.0 + self._pan_x = 0.0 + self._pan_y = 0.0 + self._dragging = False + self._drag_last = None + self.setCursor(Qt.OpenHandCursor) + self.update() + + def visible_points(self) -> List[TrackPoint]: + return filter_by_visible_laps(self._points, self._visible_laps) + + def all_points(self) -> List[TrackPoint]: + return list(self._points) + + def compare_points(self) -> List[TrackPoint]: + return list(self._compare) + + def compare_point_at_time(self, t_rel: float) -> Optional[TrackPoint]: + """对比轨迹中最接近相对时间的采样;无可比数据返回 None。""" + if not self._compare or not self._compare_visible: + return None + t = float(t_rel) + best = self._compare[0] + best_d = abs(float(best.timestamp_s) - t) + for pt in self._compare[1:]: + d = abs(float(pt.timestamp_s) - t) + if d < best_d: + best_d = d + best = pt + return best + + def scrub_points(self) -> List[TrackPoint]: + """时间轴驱动点集:优先本场会话;仅在「显示历史数据」开启时回退对比轨迹。""" + if len(self._points) > 1: + return self._points + if self._compare_visible and len(self._compare) > 1: + return self._compare + if self._points: + return self._points + if self._compare_visible: + return self._compare + return [] + + def cursor_index(self) -> int: + return int(self._cursor_index) + + def cursor_point(self) -> Optional[TrackPoint]: + pts = self.scrub_points() + if not pts: + return None + idx = max(0, min(len(pts) - 1, self._cursor_index)) + return pts[idx] + + def set_cursor_index(self, index: int) -> None: + pts = self.scrub_points() + if not pts: + self._cursor_index = 0 + self.update() + return + idx = max(0, min(len(pts) - 1, int(index))) + if idx == self._cursor_index: + self.update() + return + self._cursor_index = idx + self.update() + self.cursorIndexChanged.emit(idx) + + def _clamp_cursor(self) -> None: + pts = self.scrub_points() + if not pts: + self._cursor_index = 0 + return + self._cursor_index = max(0, min(len(pts) - 1, self._cursor_index)) + + def _legend_height(self) -> float: + return float(UITheme.scaled(36, 30)) + + def _map_area(self) -> QRectF: + rect = QRectF(self.rect()) + return rect.adjusted(2, 2, -2, -(self._legend_height() + 2)) + + def _visible_point_sets(self) -> Tuple[List[TrackPoint], List[TrackPoint]]: + live = self.visible_points() + cmp_pts = ( + filter_by_visible_laps(self._compare, self._visible_laps) + if self._compare_visible and self._compare + else [] + ) + return live, cmp_pts + + def _color_speed_range(self) -> Optional[Tuple[float, float]]: + """色标取当前会话全部采样;会话为空且显示历史时回退对比轨迹。""" + rng = speed_range(self._points, self._speed_source) + if rng is not None: + return rng + if self._compare_visible: + return speed_range(self._compare, self._speed_source) + return None + + def _view_geometry( + self, map_rect: QRectF + ) -> Optional[Tuple[object, float, float, float, float, float, float, float, float]]: + live, cmp_pts = self._visible_point_sets() + # 几何范围用全量会话点,保证游标在隐藏圈上仍可定位 + all_pts = list(self._points) + cmp_pts + if len(all_pts) < 1: + all_pts = live + cmp_pts + if len(all_pts) < 1: + return None + bounds = compute_bounds(all_pts) + if bounds is None: + return None + margin = UITheme.scaled(14, 10) + base_l, base_t, base_w, base_h = fit_draw_rect( + bounds, map_rect.width(), map_rect.height(), margin + ) + base_l += map_rect.left() + base_t += map_rect.top() + zoom = max(self._ZOOM_MIN, min(self._ZOOM_MAX, float(self._zoom))) + draw_w = base_w * zoom + draw_h = base_h * zoom + cx = base_l + base_w * 0.5 + self._pan_x + cy = base_t + base_h * 0.5 + self._pan_y + left = cx - draw_w * 0.5 + top = cy - draw_h * 0.5 + return bounds, base_l, base_t, base_w, base_h, left, top, draw_w, draw_h + + def wheelEvent(self, event) -> None: + map_rect = self._map_area() + geo = self._view_geometry(map_rect) + if geo is None: + event.ignore() + return + delta = event.angleDelta().y() + if delta == 0: + event.accept() + return + factor = self._ZOOM_STEP if delta > 0 else (1.0 / self._ZOOM_STEP) + old_zoom = max(self._ZOOM_MIN, min(self._ZOOM_MAX, float(self._zoom))) + new_zoom = max(self._ZOOM_MIN, min(self._ZOOM_MAX, old_zoom * factor)) + if abs(new_zoom - old_zoom) < 1e-6: + event.accept() + return + _bounds, base_l, base_t, base_w, base_h, left, top, draw_w, draw_h = geo + pos = event.pos() + mx = float(pos.x()) + my = float(pos.y()) + u = 0.5 if draw_w <= 1e-6 else (mx - left) / draw_w + v = 0.5 if draw_h <= 1e-6 else (my - top) / draw_h + new_w = base_w * new_zoom + new_h = base_h * new_zoom + new_left = mx - u * new_w + new_top = my - v * new_h + self._zoom = new_zoom + self._pan_x = new_left + new_w * 0.5 - (base_l + base_w * 0.5) + self._pan_y = new_top + new_h * 0.5 - (base_t + base_h * 0.5) + if new_zoom <= self._ZOOM_MIN + 1e-6: + self._pan_x = 0.0 + self._pan_y = 0.0 + self._zoom = self._ZOOM_MIN + self.update() + event.accept() + + def mousePressEvent(self, event) -> None: + if event.button() == Qt.LeftButton: + self._dragging = True + self._drag_last = QPointF(event.pos()) + self.setCursor(Qt.ClosedHandCursor) + event.accept() + return + super().mousePressEvent(event) + + def mouseMoveEvent(self, event) -> None: + if self._dragging and self._drag_last is not None: + pos = QPointF(event.pos()) + self._pan_x += pos.x() - self._drag_last.x() + self._pan_y += pos.y() - self._drag_last.y() + self._drag_last = pos + self.update() + event.accept() + return + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event) -> None: + if event.button() == Qt.LeftButton and self._dragging: + self._dragging = False + self._drag_last = None + self.setCursor(Qt.OpenHandCursor) + event.accept() + return + super().mouseReleaseEvent(event) + + def mouseDoubleClickEvent(self, event) -> None: + if event.button() == Qt.LeftButton: + self.reset_view() + event.accept() + return + super().mouseDoubleClickEvent(event) + + def paintEvent(self, _event) -> None: + p = QPainter(self) + p.setRenderHint(QPainter.Antialiasing, True) + rect = QRectF(self.rect()) + p.fillRect(rect, chart_surface_color()) + border = chart_well_border_color() + p.setPen(QPen(border, 1.0)) + p.drawRect(rect.adjusted(0.5, 0.5, -0.5, -0.5)) + + map_rect = self._map_area() + live, cmp_pts = self._visible_point_sets() + self._pix_cache = [] + if not self._points and len(cmp_pts) < 2: + self._paint_empty(p, map_rect) + self._paint_legend(p, rect, None, None) + return + + geo = self._view_geometry(map_rect) + if geo is None: + self._paint_empty(p, map_rect) + self._paint_legend(p, rect, None, None) + return + + bounds, _bl, _bt, _bw, _bh, left, top, draw_w, draw_h = geo + color_rng = self._color_speed_range() + if color_rng is None: + self._paint_empty(p, map_rect) + self._paint_legend(p, rect, None, None) + return + spd_lo, spd_hi = color_rng + + p.save() + p.setClipRect(map_rect) + + grid_pen = QPen(QColor(border.red(), border.green(), border.blue(), 90)) + grid_pen.setWidthF(0.6) + p.setPen(grid_pen) + for i in range(6): + x = left + i * draw_w / 5.0 + p.drawLine(QPointF(x, top), QPointF(x, top + draw_h)) + y = top + i * draw_h / 5.0 + p.drawLine(QPointF(left, y), QPointF(left + draw_w, y)) + + if cmp_pts and len(cmp_pts) >= 2: + self._paint_path( + p, cmp_pts, bounds, left, top, draw_w, draw_h, spd_lo, spd_hi, compare=True + ) + if live and len(live) >= 2: + self._paint_path( + p, live, bounds, left, top, draw_w, draw_h, spd_lo, spd_hi, compare=False + ) + # 游标/浮窗:scrub_points 在关闭「显示历史数据」时不含对比点 + scrub = self.scrub_points() + if scrub: + self._paint_cursor_mark(p, scrub, bounds, left, top, draw_w, draw_h) + + p.restore() + if scrub: + self._paint_float_card_for_cursor(p, scrub, map_rect) + self._paint_legend(p, rect, spd_lo, spd_hi) + + def _paint_empty(self, p: QPainter, area: QRectF) -> None: + p.setPen(QPen(QColor(UITheme.FG_MUTED))) + font = QFont(UITheme.FONT_FAMILY_TECH) + font.setPixelSize(max(10, UITheme.scaled(12, 10))) + p.setFont(font) + p.drawText(area, Qt.AlignCenter, "等待航迹数据") + + def _paint_legend( + self, + p: QPainter, + rect: QRectF, + spd_lo: Optional[float], + spd_hi: Optional[float], + ) -> None: + legend_h = self._legend_height() + legend = QRectF( + rect.left() + 6, + rect.bottom() - legend_h, + rect.width() - 12, + legend_h - 2, + ) + font = QFont(UITheme.FONT_FAMILY_TECH) + font.setPixelSize(max(8, UITheme.scaled(9, 8))) + p.setFont(font) + p.setPen(QPen(QColor(UITheme.FG_MUTED))) + if spd_lo is None or spd_hi is None: + p.drawText(legend, Qt.AlignVCenter | Qt.AlignLeft, "速度 (节)") + return + + # 右下角连续色带 + 速度分隔线(刻度画在色带下方,避免被裁切) + bar_w = min(UITheme.scaled(168, 130), legend.width() * 0.48) + bar_h = UITheme.scaled(10, 8) + bar = QRectF( + legend.right() - bar_w, + legend.top() + UITheme.scaled(4, 3), + bar_w, + bar_h, + ) + grad = QLinearGradient(bar.left(), 0, bar.right(), 0) + grad.setColorAt(0.0, QColor(255, 0, 0)) + grad.setColorAt(0.33, QColor(255, 255, 0)) + grad.setColorAt(0.66, QColor(0, 255, 0)) + grad.setColorAt(1.0, QColor(0, 255, 255)) + p.fillRect(bar, grad) + p.setPen(QPen(chart_well_border_color(), 1.0)) + p.drawRect(bar) + + ticks = speed_tick_marks(spd_lo, spd_hi) + span = max(1e-6, spd_hi - spd_lo) + tick_pen = QPen(QColor("#ffffff")) + tick_pen.setWidthF(1.0) + label_font = QFont(UITheme.FONT_FAMILY_TECH) + label_font.setPixelSize(max(7, UITheme.scaled(8, 7))) + p.setFont(label_font) + for v in ticks: + t = (v - spd_lo) / span + x = bar.left() + t * bar.width() + p.setPen(tick_pen) + p.drawLine(QPointF(x, bar.top()), QPointF(x, bar.bottom())) + + # 两端速度数字标在色带下方 + p.setPen(QPen(QColor(UITheme.FG_MUTED))) + lo_txt = f"{spd_lo:.0f}" + hi_txt = f"{spd_hi:.0f}" + p.drawText( + QRectF(bar.left() - 4, bar.bottom() + 1, 40, legend.bottom() - bar.bottom()), + Qt.AlignLeft | Qt.AlignTop, + lo_txt, + ) + p.drawText( + QRectF(bar.right() - 36, bar.bottom() + 1, 40, legend.bottom() - bar.bottom()), + Qt.AlignRight | Qt.AlignTop, + hi_txt, + ) + + p.setFont(font) + p.setPen(QPen(QColor(UITheme.FG_MUTED))) + p.drawText( + legend.adjusted(0, 0, -(bar_w + 8), 0), + Qt.AlignVCenter | Qt.AlignLeft, + f"速度 {spd_lo:.0f}-{spd_hi:.0f} kt", + ) + + def _paint_path( + self, + p: QPainter, + points: Sequence[TrackPoint], + bounds, + left: float, + top: float, + draw_w: float, + draw_h: float, + spd_lo: float, + spd_hi: float, + *, + compare: bool, + ) -> None: + pix: List[Tuple[float, float, float, int]] = [] + for pt in points: + x, y = project_to_pixel( + pt.longitude, pt.latitude, bounds, left, top, draw_w, draw_h + ) + pix.append((x, y, pt.speed_kts(self._speed_source), int(pt.lap))) + + if not compare: + # 全量点缓存供游标(在外层用 all points 投影) + pass + + opacity = ( + float(getattr(self._compare_style, "opacity", 0.7)) + if compare + else _LIVE_OPACITY + ) + bg_pen = QPen(QColor(42, 52, 80, int(90 * opacity))) + bg_pen.setWidthF(_LINE_WIDTH_BG_COMPARE if compare else _LINE_WIDTH_BG_LIVE) + bg_pen.setCapStyle(Qt.RoundCap) + bg_pen.setJoinStyle(Qt.RoundJoin) + bg_pen.setStyle(Qt.SolidLine) + p.setPen(bg_pen) + for run in _lap_runs(pix): + for i in range(len(run) - 1): + p.drawLine(QPointF(run[i][0], run[i][1]), QPointF(run[i + 1][0], run[i + 1][1])) + + line_w = _LINE_WIDTH_COMPARE if compare else _LINE_WIDTH_LIVE + for run in _lap_runs(pix): + for i in range(len(run) - 1): + x1, y1, s1, _ = run[i] + x2, y2, s2, _ = run[i + 1] + r, g, b = speed_to_rgb(0.5 * (s1 + s2), spd_lo, spd_hi) + c = QColor(r, g, b) + if compare: + c = desaturate_color(c, 0.45) + c.setAlphaF(max(0.35, min(1.0, opacity))) + else: + c.setAlphaF(_LIVE_OPACITY) + pen = QPen(c) + pen.setWidthF(line_w) + pen.setCapStyle(Qt.RoundCap) + pen.setJoinStyle(Qt.RoundJoin) + pen.setStyle(Qt.SolidLine) + p.setPen(pen) + p.drawLine(QPointF(x1, y1), QPointF(x2, y2)) + + if compare or len(pix) < 2: + return + self._draw_marker(p, pix[0][0], pix[0][1], QColor("#4ade80")) + self._draw_marker(p, pix[-1][0], pix[-1][1], QColor("#ff6b6b")) + + def _paint_cursor_mark( + self, + p: QPainter, + points: Sequence[TrackPoint], + bounds, + left: float, + top: float, + draw_w: float, + draw_h: float, + ) -> None: + if not points: + return + idx = max(0, min(len(points) - 1, self._cursor_index)) + pt = points[idx] + cx, cy = project_to_pixel( + pt.longitude, pt.latitude, bounds, left, top, draw_w, draw_h + ) + self._pix_cache = [(cx, cy)] + p.setBrush(Qt.NoBrush) + p.setPen(QPen(QColor("#ffffff"), 2.0)) + p.drawEllipse(QPointF(cx, cy), 8.0, 8.0) + p.setPen(QPen(QColor("#1e90ff"), 1.6)) + p.drawEllipse(QPointF(cx, cy), 8.0, 8.0) + p.drawLine(QPointF(cx - 12, cy), QPointF(cx - 4, cy)) + p.drawLine(QPointF(cx + 4, cy), QPointF(cx + 12, cy)) + p.drawLine(QPointF(cx, cy - 12), QPointF(cx, cy - 4)) + p.drawLine(QPointF(cx, cy + 4), QPointF(cx, cy + 12)) + + def _paint_float_card_for_cursor( + self, + p: QPainter, + points: Sequence[TrackPoint], + map_rect: QRectF, + ) -> None: + if not points or not self._pix_cache: + return + idx = max(0, min(len(points) - 1, self._cursor_index)) + cx, cy = self._pix_cache[0] + pt = points[idx] + self._paint_float_card(p, cx, cy, pt, map_rect) + + def _paint_float_card( + self, + p: QPainter, + cx: float, + cy: float, + pt: TrackPoint, + map_rect: QRectF, + ) -> None: + def _fmt(v: float, pattern: str) -> str: + if v != v: + return "--" + return format(v, pattern) + + lines = [ + f"t {float(pt.timestamp_s):.2f}s", + f"IAS {_fmt(pt.airspeed_kts, '.1f')} kt", + f"GS {_fmt(pt.groundspeed_kts, '.1f')} kt", + f"俯仰 {_fmt(pt.pitch_deg, '+.1f')} deg", + f"坡度 {_fmt(pt.bank_deg, '+.1f')} deg", + f"航向 {_fmt(pt.heading_deg, '.1f')} deg", + f"健康 {_fmt(pt.health_pct, '.1f')} %", + f"LAP {int(pt.lap)}" if pt.lap else "LAP --", + ] + + font = QFont(UITheme.FONT_FAMILY_TECH) + font.setPixelSize(max(9, UITheme.scaled(10, 9))) + p.setFont(font) + fm = p.fontMetrics() + pad = 6.0 + line_h = float(fm.height() + 2) + text_w = max(fm.width(s) for s in lines) + box_w = text_w + pad * 2 + box_h = line_h * len(lines) + pad * 2 + + bx = cx + 14 + by = cy - box_h - 8 + # 相对整图区域避让,避免贴边被切 + area = QRectF(self.rect()).adjusted(4, 4, -4, -(self._legend_height() + 4)) + if bx + box_w > area.right(): + bx = cx - box_w - 14 + if by < area.top(): + by = cy + 14 + if by + box_h > area.bottom(): + by = max(area.top(), area.bottom() - box_h) + bx = max(area.left(), min(bx, area.right() - box_w)) + + box = QRectF(bx, by, box_w, box_h) + bg = QColor(UITheme.SURFACE) + bg.setAlpha(235) + p.setBrush(bg) + p.setPen(QPen(chart_well_border_color(), 1.0)) + p.drawRoundedRect(box, 4, 4) + p.setPen(QPen(QColor(UITheme.FG))) + for i, s in enumerate(lines): + row = QRectF( + box.left() + pad, + box.top() + pad + i * line_h, + box_w - pad * 2, + line_h, + ) + p.drawText(row, Qt.AlignLeft | Qt.AlignVCenter, s) + + @staticmethod + def _draw_marker(p: QPainter, x: float, y: float, fill: QColor) -> None: + p.setBrush(fill) + p.setPen(QPen(QColor("#ffffff"), 1.4)) + p.drawEllipse(QPointF(x, y), 5.5, 5.5) + + +def _lap_runs( + pix: Sequence[Tuple[float, float, float, int]], +) -> List[List[Tuple[float, float, float, int]]]: + """按圈号切分连续段,避免跨圈连线。""" + if not pix: + return [] + runs: List[List[Tuple[float, float, float, int]]] = [[pix[0]]] + for item in pix[1:]: + prev = runs[-1][-1] + if item[3] > 0 and prev[3] > 0 and item[3] != prev[3]: + runs.append([item]) + else: + runs[-1].append(item) + return runs + + +class TrackMapPane(QWidget): + """左下:航迹图 + 圈过滤 + IAS/GS(游标由上层全局进度条驱动)。""" + + visibilityChanged = Signal() + speedSourceChanged = Signal(str) + + def __init__(self, parent=None): + super().__init__(parent) + self.setObjectName("TrackMapPane") + self._canvas = TrackMapCanvas(self) + self._speed_source = SPEED_SOURCE_AIRSPEED + + row = QHBoxLayout() + row.setContentsMargins(0, UITheme.scaled(2, 1), 0, 0) + row.setSpacing(UITheme.scaled(8, 5)) + row.addStretch(1) + self._checks: List[QCheckBox] = [] + for i in range(LAP_COUNT): + cb = QCheckBox(f"显示LAP{i + 1}") + cb.setChecked(i == 0) + cb.setStyleSheet( + f"QCheckBox {{ color: {UITheme.FG}; background: transparent; }}" + ) + cb.toggled.connect(self._on_toggled) + self._checks.append(cb) + row.addWidget(cb) + + ias_lbl = QLabel("IAS") + ias_lbl.setStyleSheet(f"color: {UITheme.FG}; background: transparent;") + ias_lbl.setAlignment(Qt.AlignVCenter | Qt.AlignRight) + gs_lbl = QLabel("GS") + gs_lbl.setStyleSheet(f"color: {UITheme.FG}; background: transparent;") + gs_lbl.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) + self._speed_toggle = SlideToggle(checked=False) + self._speed_toggle.toggled.connect(self._on_speed_toggled) + row.addWidget(ias_lbl) + row.addWidget(self._speed_toggle, 0, Qt.AlignVCenter) + row.addWidget(gs_lbl) + row.addStretch(1) + + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(self._canvas, 1) + layout.addLayout(row) + + def speed_source(self) -> str: + return self._speed_source + + def set_speed_source(self, source: str) -> None: + use_gs = source == SPEED_SOURCE_GROUNDSPEED + self._speed_source = ( + SPEED_SOURCE_GROUNDSPEED if use_gs else SPEED_SOURCE_AIRSPEED + ) + self._speed_toggle.blockSignals(True) + self._speed_toggle.setChecked(use_gs) + self._speed_toggle.blockSignals(False) + self._canvas.set_speed_source(self._speed_source) + + def _on_speed_toggled(self, checked: bool = False) -> None: + self._speed_source = ( + SPEED_SOURCE_GROUNDSPEED if checked else SPEED_SOURCE_AIRSPEED + ) + self._canvas.set_speed_source(self._speed_source) + self.speedSourceChanged.emit(self._speed_source) + + def _on_toggled(self, _checked: bool = False) -> None: + self._canvas.set_visible_laps(self.visible_laps()) + self.visibilityChanged.emit() + + def visible_laps(self) -> List[bool]: + return [cb.isChecked() for cb in self._checks] + + def point_count(self) -> int: + return len(self._canvas.all_points()) + + def compare_point_count(self) -> int: + return len(self._canvas.compare_points()) + + def scrub_point_count(self) -> int: + return len(self._canvas.scrub_points()) + + def cursor_index(self) -> int: + return self._canvas.cursor_index() + + def cursor_point(self) -> Optional[TrackPoint]: + return self._canvas.cursor_point() + + def compare_point_at_time(self, t_rel: float) -> Optional[TrackPoint]: + """对比轨迹中最接近相对时间的采样点。""" + return self._canvas.compare_point_at_time(t_rel) + + def set_cursor_index(self, index: int) -> None: + self._canvas.set_cursor_index(index) + + def set_track_points(self, points: Sequence[TrackPoint]) -> None: + self._canvas.set_points(points) + self._canvas.set_visible_laps(self.visible_laps()) + + def set_compare_track_points(self, points: Optional[Sequence[TrackPoint]]) -> None: + self._canvas.set_compare_points(points) + self._canvas.set_visible_laps(self.visible_laps()) + + def set_compare_style(self, style: CompareStyle) -> None: + self._canvas.set_compare_style(style) + + def reset_filters(self) -> None: + """复位为默认:仅显示 LAP1。""" + for i, cb in enumerate(self._checks): + cb.blockSignals(True) + cb.setChecked(i == 0) + cb.blockSignals(False) + self._on_toggled() + + def clear(self) -> None: + self._canvas.clear() + + def reset_view(self) -> None: + self._canvas.reset_view() diff --git a/config/hardware_monitor.json b/config/hardware_monitor.json new file mode 100644 index 00000000..c57c19c5 --- /dev/null +++ b/config/hardware_monitor.json @@ -0,0 +1,40 @@ +{ + "device": { + "index": 0, + "name": "PXN-F16", + "vendor_id": 4607, + "product_id": 2106, + "guid": "0300482cff1100003a08000000000000" + }, + "axes": { + "pitch": { + "index": 1, + "invert": false, + "raw_min": -1.0, + "raw_max": 1.0, + "raw_neutral": 0.0 + }, + "roll": { + "index": 0, + "invert": false, + "raw_min": -1.0, + "raw_max": 1.0, + "raw_neutral": 0.0 + }, + "yaw": { + "index": 3, + "invert": false, + "raw_min": -1.0, + "raw_max": 1.0, + "raw_neutral": 0.0 + }, + "throttle": { + "index": 2, + "invert": true, + "raw_min": -1.0, + "raw_max": 1.0, + "raw_neutral": 0.0 + } + }, + "calibrated": false +} \ No newline at end of file diff --git a/glass_server.py b/glass_server.py deleted file mode 100644 index f5ff0b5e..00000000 --- a/glass_server.py +++ /dev/null @@ -1,464 +0,0 @@ -from flask import Flask, jsonify, render_template, request -from SimConnect import * -import random - -# -# glass_server.py is an example web app which demonstrates how data can be read and set in the simulator -# -# When run this code will start an http server running on http://localhost:5000/ which can be accessed. It includes both -# an HTML/JS front end which can be accessed through a browser and the ability to read/write datapoints and datasets -# via API requests using JSON -# -# The server runs using Flask: https://flask.palletsprojects.com/en/1.1.x/ -# -# This is intended to be a demonstration of the Python-SimConnect library rather than a fully fledged implementation. -# This code has been forked into more fully worked projects including: -# - MSFS 2020 Cockpit Companion: https://msfs2020.cc/ -# - MSFS Mobile Companion App: https://github.com/mracko/MSFS-Mobile-Companion-App -# - - -app = Flask(__name__) - -# SIMCONNECTION RELATED STARTUPS - -# Create simconnection -sm = SimConnect() -ae = AircraftEvents(sm) -aq = AircraftRequests(sm, _time=10) - -# Create request holders -# These are groups of datapoints which it is convenient to call as a group because they fulfill a specific function -request_location = [ - 'ALTITUDE', - 'LATITUDE', - 'LONGITUDE', - 'KOHLSMAN', -] - -request_airspeed = [ - 'AIRSPEED_TRUE', - 'AIRSPEED_INDICATE', - 'AIRSPEED_TRUE CALIBRATE', - 'AIRSPEED_BARBER POLE', - 'AIRSPEED_MACH', -] - -request_compass = [ - 'WISKEY_COMPASS_INDICATION_DEGREES', - 'PARTIAL_PANEL_COMPASS', - 'ADF_CARD', # ADF compass rose setting - 'MAGNETIC_COMPASS', # Compass reading - 'INDUCTOR_COMPASS_PERCENT_DEVIATION', # Inductor compass deviation reading - 'INDUCTOR_COMPASS_HEADING_REF', # Inductor compass heading -] - -request_vertical_speed = [ - 'VELOCITY_BODY_Y', # True vertical speed, relative to aircraft axis - 'RELATIVE_WIND_VELOCITY_BODY_Y', # Vertical speed relative to wind - 'VERTICAL_SPEED', # Vertical speed indication - 'GPS_WP_VERTICAL_SPEED', # Vertical speed to waypoint -] - -request_fuel = [ - 'FUEL_TANK_CENTER_LEVEL', # Percent of maximum capacity - 'FUEL_TANK_CENTER2_LEVEL', # Percent of maximum capacity - 'FUEL_TANK_CENTER3_LEVEL', # Percent of maximum capacity - 'FUEL_TANK_LEFT_MAIN_LEVEL', # Percent of maximum capacity - 'FUEL_TANK_LEFT_AUX_LEVEL', # Percent of maximum capacity - 'FUEL_TANK_LEFT_TIP_LEVEL', # Percent of maximum capacity - 'FUEL_TANK_RIGHT_MAIN_LEVEL', # Percent of maximum capacity - 'FUEL_TANK_RIGHT_AUX_LEVEL', # Percent of maximum capacity - 'FUEL_TANK_RIGHT_TIP_LEVEL', # Percent of maximum capacity - 'FUEL_TANK_EXTERNAL1_LEVEL', # Percent of maximum capacity - 'FUEL_TANK_EXTERNAL2_LEVEL', # Percent of maximum capacity - 'FUEL_TANK_CENTER_CAPACITY', # Maximum capacity in volume - 'FUEL_TANK_CENTER2_CAPACITY', # Maximum capacity in volume - 'FUEL_TANK_CENTER3_CAPACITY', # Maximum capacity in volume - 'FUEL_TANK_LEFT_MAIN_CAPACITY', # Maximum capacity in volume - 'FUEL_TANK_LEFT_AUX_CAPACITY', # Maximum capacity in volume - 'FUEL_TANK_LEFT_TIP_CAPACITY', # Maximum capacity in volume - 'FUEL_TANK_RIGHT_MAIN_CAPACITY', # Maximum capacity in volume - 'FUEL_TANK_RIGHT_AUX_CAPACITY', # Maximum capacity in volume - 'FUEL_TANK_RIGHT_TIP_CAPACITY', # Maximum capacity in volume - 'FUEL_TANK_EXTERNAL1_CAPACITY', # Maximum capacity in volume - 'FUEL_TANK_EXTERNAL2_CAPACITY', # Maximum capacity in volume - 'FUEL_LEFT_CAPACITY', # Maximum capacity in volume - 'FUEL_RIGHT_CAPACITY', # Maximum capacity in volume - 'FUEL_TANK_CENTER_QUANTITY', # Current quantity in volume - 'FUEL_TANK_CENTER2_QUANTITY', # Current quantity in volume - 'FUEL_TANK_CENTER3_QUANTITY', # Current quantity in volume - 'FUEL_TANK_LEFT_MAIN_QUANTITY', # Current quantity in volume - 'FUEL_TANK_LEFT_AUX_QUANTITY', # Current quantity in volume - 'FUEL_TANK_LEFT_TIP_QUANTITY', # Current quantity in volume - 'FUEL_TANK_RIGHT_MAIN_QUANTITY', # Current quantity in volume - 'FUEL_TANK_RIGHT_AUX_QUANTITY', # Current quantity in volume - 'FUEL_TANK_RIGHT_TIP_QUANTITY', # Current quantity in volume - 'FUEL_TANK_EXTERNAL1_QUANTITY', # Current quantity in volume - 'FUEL_TANK_EXTERNAL2_QUANTITY', # Current quantity in volume - 'FUEL_LEFT_QUANTITY', # Current quantity in volume - 'FUEL_RIGHT_QUANTITY', # Current quantity in volume - 'FUEL_TOTAL_QUANTITY', # Current quantity in volume - 'FUEL_WEIGHT_PER_GALLON', # Fuel weight per gallon - 'FUEL_TOTAL_CAPACITY', # Total capacity of the aircraft - 'FUEL_SELECTED_QUANTITY_PERCENT', # Percent or capacity for selected tank - 'FUEL_SELECTED_QUANTITY', # Quantity of selected tank - 'FUEL_TOTAL_QUANTITY_WEIGHT', # Current total fuel weight of the aircraft - 'NUM_FUEL_SELECTORS', # Number of selectors on the aircraft - 'UNLIMITED_FUEL', # Unlimited fuel flag - 'ESTIMATED_FUEL_FLOW', # Estimated fuel flow at cruise -] - -request_flaps = [ - 'FLAPS_HANDLE_PERCENT', # Percent flap handle extended - 'FLAPS_HANDLE_INDEX', # Index of current flap position - 'FLAPS_NUM_HANDLE_POSITIONS', # Number of flap positions - 'TRAILING_EDGE_FLAPS_LEFT_PERCENT', # Percent left trailing edge flap extended - 'TRAILING_EDGE_FLAPS_RIGHT_PERCENT', # Percent right trailing edge flap extended - 'TRAILING_EDGE_FLAPS_LEFT_ANGLE', # Angle left trailing edge flap extended. Use TRAILING EDGE FLAPS LEFT PERCENT to set a value. - 'TRAILING_EDGE_FLAPS_RIGHT_ANGLE', # Angle right trailing edge flap extended. Use TRAILING EDGE FLAPS RIGHT PERCENT to set a value. - 'LEADING_EDGE_FLAPS_LEFT_PERCENT', # Percent left leading edge flap extended - 'LEADING_EDGE_FLAPS_RIGHT_PERCENT', # Percent right leading edge flap extended - 'LEADING_EDGE_FLAPS_LEFT_ANGLE', # Angle left leading edge flap extended. Use LEADING EDGE FLAPS LEFT PERCENT to set a value. - 'LEADING_EDGE_FLAPS_RIGHT_ANGLE', # Angle right leading edge flap extended. Use LEADING EDGE FLAPS RIGHT PERCENT to set a value. - 'FLAPS_AVAILABLE', # True if flaps available - 'FLAP_DAMAGE_BY_SPEED', # True if flagps are damaged by excessive speed - 'FLAP_SPEED_EXCEEDED', # True if safe speed limit for flaps exceeded -] - -request_throttle = [ - 'AUTOPILOT_THROTTLE_ARM', # Autothrottle armed - 'AUTOPILOT_TAKEOFF_POWER_ACTIVE', # Takeoff / Go Around power mode active - 'AUTOTHROTTLE_ACTIVE', # Auto-throttle active - 'FULL_THROTTLE_THRUST_TO_WEIGHT_RATIO', # Full throttle thrust to weight ratio - 'THROTTLE_LOWER_LIMIT', - 'GENERAL_ENG_THROTTLE_LEVER_POSITION:index', # Percent of max throttle position - 'AUTOPILOT_THROTTLE_ARM', # Autothrottle armed - 'AUTOTHROTTLE_ACTIVE', # Auto-throttle active - 'FULL_THROTTLE_THRUST_TO_WEIGHT_RATIO', # Full throttle thrust to weight ratio -] - -request_gear = [ - 'IS_GEAR_RETRACTABLE', # True if gear can be retracted - 'IS_GEAR_SKIS', # True if landing gear is skis - 'IS_GEAR_FLOATS', # True if landing gear is floats - 'IS_GEAR_SKIDS', # True if landing gear is skids - 'IS_GEAR_WHEELS', # True if landing gear is wheels - 'GEAR_HANDLE_POSITION', # True if gear handle is applied - 'GEAR_HYDRAULIC_PRESSURE', # Gear hydraulic pressure - 'TAILWHEEL_LOCK_ON', # True if tailwheel lock applied - 'GEAR_CENTER_POSITION', # Percent center gear extended - 'GEAR_LEFT_POSITION', # Percent left gear extended - 'GEAR_RIGHT_POSITION', # Percent right gear extended - 'GEAR_TAIL_POSITION', # Percent tail gear extended - 'GEAR_AUX_POSITION', # Percent auxiliary gear extended - 'GEAR_TOTAL_PCT_EXTENDED', # Percent total gear extended - 'AUTO_BRAKE_SWITCH_CB', # Auto brake switch position - 'WATER_RUDDER_HANDLE_POSITION', - 'WATER_LEFT_RUDDER_EXTENDED', # Percent extended - 'WATER_RIGHT_RUDDER_EXTENDED', # Percent extended - 'GEAR_CENTER_STEER_ANGLE', # Center wheel angle, negative to the left, positive to the right. - 'GEAR_LEFT_STEER_ANGLE', # Left wheel angle, negative to the left, positive to the right. - 'GEAR_RIGHT_STEER_ANGLE', # Right wheel angle, negative to the left, positive to the right. - 'GEAR_AUX_STEER_ANGLE', # Aux wheel angle, negative to the left, positive to the right. The aux wheel is the fourth set of gear, sometimes used on helicopters. - 'WATER_LEFT_RUDDER_STEER_ANGLE', # Water left rudder angle, negative to the left, positive to the right. - 'WATER_RIGHT_RUDDER_STEER_ANGLE', # Water right rudder angle, negative to the left, positive to the right. - 'GEAR_CENTER_STEER_ANGLE_PCT', # Center steer angle as a percentage - 'GEAR_LEFT_STEER_ANGLE_PCT', # Left steer angle as a percentage - 'GEAR_RIGHT_STEER_ANGLE_PCT', # Right steer angle as a percentage - 'GEAR_AUX_STEER_ANGLE_PCT', # Aux steer angle as a percentage - 'WATER_LEFT_RUDDER_STEER_ANGLE_PCT', # Water left rudder angle as a percentage - 'WATER_RIGHT_RUDDER_STEER_ANGLE_PCT', # Water right rudder as a percentage - 'CENTER_WHEEL_RPM', # Center landing gear rpm - 'LEFT_WHEEL_RPM', # Left landing gear rpm - 'RIGHT_WHEEL_RPM', # Right landing gear rpm - 'AUX_WHEEL_RPM', # Rpm of fourth set of gear wheels. - 'CENTER_WHEEL_ROTATION_ANGLE', # Center wheel rotation angle - 'LEFT_WHEEL_ROTATION_ANGLE', # Left wheel rotation angle - 'RIGHT_WHEEL_ROTATION_ANGLE', # Right wheel rotation angle - 'AUX_WHEEL_ROTATION_ANGLE', # Aux wheel rotation angle - 'GEAR_EMERGENCY_HANDLE_POSITION', # True if gear emergency handle applied - 'ANTISKID_BRAKES_ACTIVE', # True if antiskid brakes active - 'RETRACT_FLOAT_SWITCH', # True if retract float switch on - 'RETRACT_LEFT_FLOAT_EXTENDED', # If aircraft has retractable floats. - 'RETRACT_RIGHT_FLOAT_EXTENDED', # If aircraft has retractable floats. - 'STEER_INPUT_CONTROL', # Position of steering tiller - 'GEAR_DAMAGE_BY_SPEED', # True if gear has been damaged by excessive speed - 'GEAR_SPEED_EXCEEDED', # True if safe speed limit for gear exceeded - 'NOSEWHEEL_LOCK_ON', # True if the nosewheel lock is engaged. -] - -request_trim = [ - 'ROTOR_LATERAL_TRIM_PCT', # Trim percent - 'ELEVATOR_TRIM_POSITION', # Elevator trim deflection - 'ELEVATOR_TRIM_INDICATOR', - 'ELEVATOR_TRIM_PCT', # Percent elevator trim - 'AILERON_TRIM', # Angle deflection - 'AILERON_TRIM_PCT', # The trim position of the ailerons. Zero is fully retracted. - 'RUDDER_TRIM_PCT', # The trim position of the rudder. Zero is no trim. - 'RUDDER_TRIM', # Angle deflection -] - -request_autopilot = [ - 'AUTOPILOT_MASTER', - 'AUTOPILOT_AVAILABLE', - 'AUTOPILOT_NAV_SELECTED', - 'AUTOPILOT_WING_LEVELER', - 'AUTOPILOT_NAV1_LOCK', - 'AUTOPILOT_HEADING_LOCK', - 'AUTOPILOT_HEADING_LOCK_DIR', - 'AUTOPILOT_ALTITUDE_LOCK', - 'AUTOPILOT_ALTITUDE_LOCK_VAR', - 'AUTOPILOT_ATTITUDE_HOLD', - 'AUTOPILOT_GLIDESLOPE_HOLD', - 'AUTOPILOT_PITCH_HOLD_REF', - 'AUTOPILOT_APPROACH_HOLD', - 'AUTOPILOT_BACKCOURSE_HOLD', - 'AUTOPILOT_VERTICAL_HOLD_VAR', - 'AUTOPILOT_PITCH_HOLD', - 'AUTOPILOT_FLIGHT_DIRECTOR_ACTIVE', - 'AUTOPILOT_FLIGHT_DIRECTOR_PITCH', - 'AUTOPILOT_FLIGHT_DIRECTOR_BANK', - 'AUTOPILOT_AIRSPEED_HOLD', - 'AUTOPILOT_AIRSPEED_HOLD_VAR', - 'AUTOPILOT_MACH_HOLD', - 'AUTOPILOT_MACH_HOLD_VAR', - 'AUTOPILOT_YAW_DAMPER', - 'AUTOPILOT_RPM_HOLD_VAR', - 'AUTOPILOT_THROTTLE_ARM', - 'AUTOPILOT_TAKEOFF_POWER ACTIVE', - 'AUTOTHROTTLE_ACTIVE', - 'AUTOPILOT_VERTICAL_HOLD', - 'AUTOPILOT_RPM_HOLD', - 'AUTOPILOT_MAX_BANK', - 'FLY_BY_WIRE_ELAC_SWITCH', - 'FLY_BY_WIRE_FAC_SWITCH', - 'FLY_BY_WIRE_SEC_SWITCH', - 'FLY_BY_WIRE_ELAC_FAILED', - 'FLY_BY_WIRE_FAC_FAILED', - 'FLY_BY_WIRE_SEC_FAILED' -] - -request_cabin = [ - 'CABIN_SEATBELTS_ALERT_SWITCH', - 'CABIN_NO_SMOKING_ALERT_SWITCH' -] - -# This is a helper function which just adds a comma in the right place for readability, -# for instance converting 30000 to 30,000 -def thousandify(x): - return f"{x:,}" - - -@app.route('/') -def glass(): - return render_template("glass.html") - - -@app.route('/attitude-indicator') -def AttInd(): - return render_template("attitude-indicator/index.html") - - -def get_dataset(data_type): - if data_type == "navigation": request_to_action = request_location - if data_type == "airspeed": request_to_action = request_airspeed - if data_type == "compass": request_to_action = request_compass - if data_type == "vertical_speed": request_to_action = request_vertical_speed - if data_type == "fuel": request_to_action = request_fuel - if data_type == "flaps": request_to_action = request_flaps - if data_type == "throttle": request_to_action = request_throttle - if data_type == "gear": request_to_action = request_gear - if data_type == "trim": request_to_action = request_trim - if data_type == "autopilot": request_to_action = request_autopilot - if data_type == 'cabin': request_to_action = request_cabin - - return request_to_action - - -# In addition to the datapoints which can be pulled individually or as groups via JSON, the UI endpoint returns JSON -# with the datapoints which the HTML / JS uses in a friendly format -@app.route('/ui') -def output_ui_variables(): - - # Initialise dictionary - ui_friendly_dictionary = {} - ui_friendly_dictionary["STATUS"] = "success" - - # Fuel - fuel_percentage = (aq.get("FUEL_TOTAL_QUANTITY") / aq.get("FUEL_TOTAL_CAPACITY")) * 100 - ui_friendly_dictionary["FUEL_PERCENTAGE"] = round(fuel_percentage) - - # Airspeed and altitude - ui_friendly_dictionary["AIRSPEED_INDICATE"] = round(aq.get("AIRSPEED_INDICATED")) - ui_friendly_dictionary["ALTITUDE"] = thousandify(round(aq.get("PLANE_ALTITUDE"))) - - # Control surfaces - if aq.get("GEAR_HANDLE_POSITION") == 1: - ui_friendly_dictionary["GEAR_HANDLE_POSITION"] = "DOWN" - else: - ui_friendly_dictionary["GEAR_HANDLE_POSITION"] = "UP" - ui_friendly_dictionary["FLAPS_HANDLE_PERCENT"] = round(aq.get("FLAPS_HANDLE_PERCENT") * 100) - - ui_friendly_dictionary["ELEVATOR_TRIM_PCT"] = round(aq.get("ELEVATOR_TRIM_PCT") * 100) - ui_friendly_dictionary["RUDDER_TRIM_PCT"] = round(aq.get("RUDDER_TRIM_PCT") * 100) - - # Navigation - ui_friendly_dictionary["LATITUDE"] = aq.get("PLANE_LATITUDE") - ui_friendly_dictionary["LONGITUDE"] = aq.get("PLANE_LONGITUDE") - ui_friendly_dictionary["MAGNETIC_COMPASS"] = round(aq.get("MAGNETIC_COMPASS")) - ui_friendly_dictionary["MAGVAR"] = round(aq.get("MAGVAR")) - ui_friendly_dictionary["VERTICAL_SPEED"] = round(aq.get("VERTICAL_SPEED")) - - # Autopilot - ui_friendly_dictionary["AUTOPILOT_MASTER"] = aq.get("AUTOPILOT_MASTER") - ui_friendly_dictionary["AUTOPILOT_NAV_SELECTED"] = aq.get("AUTOPILOT_NAV_SELECTED") - ui_friendly_dictionary["AUTOPILOT_WING_LEVELER"] = aq.get("AUTOPILOT_WING_LEVELER") - ui_friendly_dictionary["AUTOPILOT_HEADING_LOCK"] = aq.get("AUTOPILOT_HEADING_LOCK") - ui_friendly_dictionary["AUTOPILOT_HEADING_LOCK_DIR"] = round(aq.get("AUTOPILOT_HEADING_LOCK_DIR")) - ui_friendly_dictionary["AUTOPILOT_ALTITUDE_LOCK"] = aq.get("AUTOPILOT_ALTITUDE_LOCK") - ui_friendly_dictionary["AUTOPILOT_ALTITUDE_LOCK_VAR"] = thousandify(round(aq.get("AUTOPILOT_ALTITUDE_LOCK_VAR"))) - ui_friendly_dictionary["AUTOPILOT_ATTITUDE_HOLD"] = aq.get("AUTOPILOT_ATTITUDE_HOLD") - ui_friendly_dictionary["AUTOPILOT_GLIDESLOPE_HOLD"] = aq.get("AUTOPILOT_GLIDESLOPE_HOLD") - ui_friendly_dictionary["AUTOPILOT_APPROACH_HOLD"] = aq.get("AUTOPILOT_APPROACH_HOLD") - ui_friendly_dictionary["AUTOPILOT_BACKCOURSE_HOLD"] = aq.get("AUTOPILOT_BACKCOURSE_HOLD") - ui_friendly_dictionary["AUTOPILOT_VERTICAL_HOLD"] = aq.get("AUTOPILOT_VERTICAL_HOLD") - ui_friendly_dictionary["AUTOPILOT_VERTICAL_HOLD_VAR"] = aq.get("AUTOPILOT_VERTICAL_HOLD_VAR") - ui_friendly_dictionary["AUTOPILOT_PITCH_HOLD"] = aq.get("AUTOPILOT_PITCH_HOLD") - ui_friendly_dictionary["AUTOPILOT_PITCH_HOLD_REF"] = aq.get("AUTOPILOT_PITCH_HOLD_REF") - ui_friendly_dictionary["AUTOPILOT_FLIGHT_DIRECTOR_ACTIVE"] = aq.get("AUTOPILOT_FLIGHT_DIRECTOR_ACTIVE") - ui_friendly_dictionary["AUTOPILOT_AIRSPEED_HOLD"] = aq.get("AUTOPILOT_AIRSPEED_HOLD") - ui_friendly_dictionary["AUTOPILOT_AIRSPEED_HOLD_VAR"] = round(aq.get("AUTOPILOT_AIRSPEED_HOLD_VAR")) - - # Cabin - ui_friendly_dictionary["CABIN_SEATBELTS_ALERT_SWITCH"] = aq.get("CABIN_SEATBELTS_ALERT_SWITCH") - ui_friendly_dictionary["CABIN_NO_SMOKING_ALERT_SWITCH"] = aq.get("CABIN_NO_SMOKING_ALERT_SWITCH") - - return jsonify(ui_friendly_dictionary) - - -@app.route('/dataset//', methods=["GET"]) -def output_json_dataset(dataset_name): - dataset_map = {} - - # This uses get_dataset() to pull in a bunch of different datapoint names into a dictionary which means they can - # then be requested from the sim - data_dictionary = get_dataset(dataset_name) - - for datapoint_name in data_dictionary: - dataset_map[datapoint_name] = aq.get(datapoint_name) - - return jsonify(dataset_map) - - -# This function actually does the work of getting an individual datapoint from the sim -def get_datapoint(datapoint_name, index=None): - - if index is not None and ':index' in datapoint_name: - dp = aq.find(datapoint_name) - if dp is not None: - dp.setIndex(int(index)) - - return aq.get(datapoint_name) - - -# This is the http endpoint wrapper for getting an individual datapoint -@app.route('/datapoint//get', methods=["GET"]) -def get_datapoint_endpoint(datapoint_name): - - ds = request.get_json() if request.is_json else request.form - index = ds.get('index') - - output = get_datapoint(datapoint_name, index) - - if isinstance(output, bytes): - output = output.decode('ascii') - - return jsonify(output) - - -# This function actually does the work of setting an individual datapoint -def set_datapoint(datapoint_name, index=None, value_to_use=None): - - if index is not None and ':index' in datapoint_name: - clas = aq.find(datapoint_name) - if clas is not None: - clas.setIndex(int(index)) - - sent = False - if value_to_use is None: - sent = aq.set(datapoint_name, 0) - else: - sent = aq.set(datapoint_name, int(value_to_use)) - - if sent is True: - status = "success" - else: - status = "Error with sending request: %s" % (datapoint_name) - - return status - - -# This is the http endpoint wrapper for setting a datapoint -@app.route('/datapoint//set', methods=["POST"]) -def set_datapoint_endpoint(datapoint_name): - - ds = request.get_json() if request.is_json else request.form - index = ds.get('index') - value_to_use = ds.get('value_to_use') - - status = set_datapoint (datapoint_name, index, value_to_use) - - return jsonify(status) - - -# This function actually does the work of triggering an event -def trigger_event(event_name, value_to_use = None): - - EVENT_TO_TRIGGER = ae.find(event_name) - if EVENT_TO_TRIGGER is not None: - if value_to_use is None: - EVENT_TO_TRIGGER() - else: - EVENT_TO_TRIGGER(int(value_to_use)) - - status = "success" - else: - status = "Error: %s is not an Event" % (event_name) - - return status - - -# This is the http endpoint wrapper for triggering an event -@app.route('/event//trigger', methods=["POST"]) -def trigger_event_endpoint(event_name): - - ds = request.get_json() if request.is_json else request.form - value_to_use = ds.get('value_to_use') - - status = trigger_event(event_name, value_to_use) - - return jsonify(status) - - -@app.route('/custom_emergency/', methods=["GET", "POST"]) -def custom_emergency(emergency_type): - - text_to_return = "No valid emergency type passed" - - if emergency_type == "random_engine_fire": - # Calculate number of engines - number_of_engines = aq.get("NUMBER_OF_ENGINES") - - if number_of_engines < 0: return "error, no engines found - is sim running?" - engine_to_set_on_fire = random.randint(1,number_of_engines) - - set_datapoint("ENG_ON_FIRE:index", engine_to_set_on_fire, 1) - - text_to_return = "Engine " + str(engine_to_set_on_fire) + " on fire" - - return text_to_return - - -# Main loop to run the flask app -app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/icon.png b/icon.png new file mode 100644 index 00000000..915201a7 Binary files /dev/null and b/icon.png differ diff --git a/local_example.py b/local_example.py deleted file mode 100644 index 54a18496..00000000 --- a/local_example.py +++ /dev/null @@ -1,94 +0,0 @@ -from SimConnect import * -import logging -from SimConnect.Enum import * -from time import sleep - - -logging.basicConfig(level=logging.DEBUG) -LOGGER = logging.getLogger(__name__) -LOGGER.info("START") -# time holder for inline commands -ct_g = millis() - -# creat simconnection and pass used user classes -sm = SimConnect() -aq = AircraftRequests(sm) -ae = AircraftEvents(sm) - - -mc = aq.find("MAGNETIC_COMPASS") -mv = aq.find("MAGVAR") -print(mc.get() + mv.get()) - -sm.exit() -quit() - -# Set pos arund space nedle in WA. -sm.set_pos( - _Altitude=1000.0, - _Latitude=47.614699, - _Longitude=-122.358473, - _Airspeed=130, - _Heading=70.0, - # _Pitch=0.0, - # _Bank=0.0, - # _OnGround=0 -) - -# PARKING_BRAKES = Event(b'PARKING_BRAKES', sm) -# long path -PARKING_BRAKES = ae.Miscellaneous_Systems.PARKING_BRAKES -# using get -GEAR_TOGGLE = ae.Miscellaneous_Systems.get("GEAR_TOGGLE") -# Using find to lookup Event -AP_MASTER = ae.find("AP_MASTER") - -# THROTTLE1 Event -THROTTLE1 = ae.Engine.THROTTLE1_SET - - -# THROTTLE1 Request -Throttle = aq.find('GENERAL_ENG_THROTTLE_LEVER_POSITION:1') - -# If useing -# Throttle = aq.find('GENERAL_ENG_THROTTLE_LEVER_POSITION:index') -# Need to set index befor read/write -# Note to set index 2 vs 1 just re-run -# Throttle.setIndex(1) - - -# print the built in description -# AP_MASTER Toggles AP on/off -print("AP_MASTER", AP_MASTER.description) -# Throttle Percent of max throttle position -print("Throttle", Throttle.description) -# THROTTLE1 Set throttle 1 exactly (0 to 16383) -print("THROTTLE1", THROTTLE1.description) - - -while not sm.quit: - print("Throttle:", Throttle.value) - print("Alt=%f Lat=%f Lon=%f Kohlsman=%.2f" % ( - aq.PositionandSpeedData.get('PLANE_ALTITUDE'), - aq.PositionandSpeedData.get('PLANE_LATITUDE'), - aq.PositionandSpeedData.get('PLANE_LONGITUDE'), - aq.FlightInstrumentationData.get('KOHLSMAN_SETTING_HG') - )) - sleep(2) - - # Send Event with value - # THROTTLE1(1500) - - # Send Event toggle AP_MASTER - # AP_MASTER() - - # PARKING_BRAKES() - - # send new data inine @ 5s - if ct_g + 5000 < millis(): - if Throttle.value < 100: - Throttle.value += 5 - print("THROTTLE SET") - ct_g = millis() - -sm.exit() diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 00000000..148008df --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +""" +模块:models +职责:数据与业务模型包(与 Qt 无关) +""" diff --git a/models/chart.py b/models/chart.py new file mode 100644 index 00000000..2a39ee35 --- /dev/null +++ b/models/chart.py @@ -0,0 +1,387 @@ +# -*- coding: utf-8 -*- +""" +模块:models.chart +职责:图表缓冲、视窗切片、健康色带判定;字段元数据从 data_bridge.FIELDS 派生 +依赖:models.data_bridge(字段模版) +""" + +from __future__ import annotations + +import math +from collections import deque +from dataclasses import dataclass +from enum import Enum +from typing import Dict, List, Optional, Tuple + +HISTORY_LEN = 3000 +VIEW_WINDOW_S = 150.0 +X_AXIS_MAJOR_SEC = 10.0 + +# 健康度色带阈值(%) +HEALTH_BOUND_FLASH = 99.5 +HEALTH_BOUND_GREEN = 97.5 +HEALTH_BOUND_ORANGE = 96.5 + + +class AxisSide(str, Enum): + LEFT = "left" + RIGHT = "right" + + +# 工具栏槽位:Y轴(L)/ Y轴(R)各一 +SERIES_SLOT_COUNT = 2 +SERIES_SLOT_SIDES: Tuple[AxisSide, ...] = (AxisSide.LEFT, AxisSide.RIGHT) +SERIES_SLOT_LABELS: Tuple[str, ...] = ("Y轴(L)", "Y轴(R)") + + +@dataclass(frozen=True) +class ChartFieldSpec: + """单条曲线字段:绑定 Y 轴一侧的刻度范围与标题。""" + + id: str + ymin: float + ymax: float + axis_title: str + side: AxisSide = AxisSide.LEFT + color_key: str = "" + color: str = "" + line_width: Optional[float] = None + line_style: str = "" + health_spans: bool = False + + def ylim(self) -> Tuple[float, float]: + return self.ymin, self.ymax + + +@dataclass(frozen=True) +class ChartSeriesOption: + """系列选项:由 data_bridge.FieldDef 派生,供工具栏使用。""" + + id: str + label: str + ymin: float + ymax: float + axis_title: str + side: AxisSide = AxisSide.LEFT + color_key: str = "" + color: str = "" + line_width: Optional[float] = None + line_style: str = "" + health_spans: bool = False + + def to_field_spec(self) -> ChartFieldSpec: + return ChartFieldSpec( + id=self.id, + ymin=self.ymin, + ymax=self.ymax, + axis_title=self.axis_title, + side=self.side, + color_key=self.color_key or self.id, + color=self.color, + line_width=self.line_width, + line_style=self.line_style, + health_spans=self.health_spans, + ) + + +def _option_from_field_def(fdef) -> ChartSeriesOption: + ymin = 0.0 if fdef.ymin is None else float(fdef.ymin) + ymax = 100.0 if fdef.ymax is None else float(fdef.ymax) + return ChartSeriesOption( + id=fdef.id, + label=fdef.label or fdef.id, + ymin=ymin, + ymax=ymax, + axis_title=fdef.unit or "", + side=AxisSide(fdef.side or "left"), + color_key=fdef.color_key or fdef.id, + color=fdef.color or "", + line_width=fdef.line_width, + line_style=fdef.line_style or "", + health_spans=bool(fdef.health_spans), + ) + + +def _build_catalog() -> Dict[str, ChartSeriesOption]: + from models.data_bridge import FIELDS + + return {fid: _option_from_field_def(fdef) for fid, fdef in FIELDS.items()} + + +# 系列选项库(派生自 data_bridge.FIELDS;增字段只改 FIELDS) +CHART_SERIES_CATALOG: Dict[str, ChartSeriesOption] = _build_catalog() + +# 工具栏默认:Y轴(L)扭矩、Y轴(R)健康(均显示) +DEFAULT_SERIES_SLOTS: Tuple[str, ...] = ("torque", "health") +DEFAULT_SERIES_VISIBLE: Tuple[bool, ...] = (True, True) + + +def catalog_options_for_side(side: AxisSide | str) -> List[ChartSeriesOption]: + """返回指定默认侧的选项(兼容旧调用;工具栏现已共用全库)。""" + side_val = AxisSide(side) + return [opt for opt in CHART_SERIES_CATALOG.values() if opt.side == side_val] + + +def catalog_field_specs() -> Tuple[ChartFieldSpec, ...]: + """选项库全部字段规格(供缓冲录制)。""" + return tuple(opt.to_field_spec() for opt in CHART_SERIES_CATALOG.values()) + + +def series_option(field_id: str) -> Optional[ChartSeriesOption]: + return CHART_SERIES_CATALOG.get(field_id) + + +def field_spec_for(field_id: str) -> Optional[ChartFieldSpec]: + opt = CHART_SERIES_CATALOG.get(field_id) + return opt.to_field_spec() if opt is not None else None + + +@dataclass(frozen=True) +class ChartConfig: + """图表字段组合;缓冲通常含全库字段,显示由工具栏槽位裁剪。""" + + fields: Tuple[ChartFieldSpec, ...] + view_window_s: float = VIEW_WINDOW_S + x_axis_major_sec: float = X_AXIS_MAJOR_SEC + x_axis_title: str = "Time (s)" + + def field_ids(self) -> Tuple[str, ...]: + return tuple(f.id for f in self.fields) + + def field_by_id(self, field_id: str) -> Optional[ChartFieldSpec]: + for field in self.fields: + if field.id == field_id: + return field + return None + + def field_for_side(self, side: AxisSide | str) -> Optional[ChartFieldSpec]: + side_val = AxisSide(side) + for field in self.fields: + if field.side == side_val: + return field + return None + + def span_field(self) -> Optional[ChartFieldSpec]: + for field in self.fields: + if field.health_spans: + return field + return None + + def primary_field(self) -> ChartFieldSpec: + """拖拽/圈标映射用的主字段(优先左侧)。""" + left = self.field_for_side(AxisSide.LEFT) + return left if left is not None else self.fields[0] + + +# 预置字段别名(兼容旧引用) +FIELD_TORQUE = CHART_SERIES_CATALOG["torque"].to_field_spec() +FIELD_HEALTH = CHART_SERIES_CATALOG["health"].to_field_spec() +TRAINING_CHART = ChartConfig(fields=catalog_field_specs()) + +# 兼容旧常量名 +TORQUE_YMIN = FIELD_TORQUE.ymin +TORQUE_YMAX = FIELD_TORQUE.ymax +HEALTH_YMIN = FIELD_HEALTH.ymin +HEALTH_YMAX = FIELD_HEALTH.ymax + + +def health_band(health_pct: Optional[float]) -> Optional[str]: + """ + 说明:按展示精度判定健康色带档位 + 参数: + health_pct — 健康百分比;非法/NaN 返回 None + 返回: + "flash" | "green" | "orange" | "red" | None + """ + if health_pct is None: + return None + try: + value = float(health_pct) + except (TypeError, ValueError): + return None + if value != value: + return None + display = round(value, 1) + if HEALTH_BOUND_FLASH <= display <= 100.0: + return "flash" + if HEALTH_BOUND_GREEN <= display < HEALTH_BOUND_FLASH: + return "green" + if HEALTH_BOUND_ORANGE <= display < HEALTH_BOUND_GREEN: + return "orange" + if display < HEALTH_BOUND_ORANGE: + return "red" + return None + + +def health_span_kind(health_pct: Optional[float]) -> Optional[str]: + """将 health_band 映射为蒙版配色键(flash→blue;green 不着色)。""" + band = health_band(health_pct) + if band == "flash": + return "blue" + if band == "orange": + return "orange" + if band == "red": + return "red" + return None + + +def relative_times(timestamps: List[float]) -> List[float]: + """将绝对时间戳转为相对首点的秒数序列。""" + if not timestamps: + return [] + t0 = float(timestamps[0]) + return [float(t) - t0 for t in timestamps] + + +class ChartHistoryBuffer: + """按 ChartConfig 字段存储时间序列,供趋势图渲染。""" + + def __init__(self, config: ChartConfig = TRAINING_CHART, maxlen: int = HISTORY_LEN): + self.config = config + self.timestamps: deque = deque(maxlen=maxlen) + self._series: Dict[str, deque] = { + field.id: deque(maxlen=maxlen) for field in config.fields + } + + @property + def t_min(self) -> float: + return self.timestamps[0] if self.timestamps else 0.0 + + @property + def t_max(self) -> float: + return self.timestamps[-1] if self.timestamps else 0.0 + + def __len__(self) -> int: + return len(self.timestamps) + + # 兼容旧属性名 + @property + def torque(self) -> deque: + return self._series.get(FIELD_TORQUE.id, deque()) + + @property + def health_pct(self) -> deque: + return self._series.get(FIELD_HEALTH.id, deque()) + + def append(self, timestamp: float, **values: Optional[float]) -> None: + """追加一拍;kwargs 为 data_bridge.extract 产出的 field_id → float。""" + if not values: + return + self.timestamps.append(timestamp) + for field in self.config.fields: + value = values.get(field.id) + if value is None: + self._series[field.id].append(float("nan")) + else: + try: + self._series[field.id].append(float(value)) + except (TypeError, ValueError): + self._series[field.id].append(float("nan")) + + def slice_view( + self, + view_start: float, + view_end: float, + ) -> Tuple[List[float], Dict[str, List[float]]]: + times = relative_times(list(self.timestamps)) + xs: List[float] = [] + out: Dict[str, List[float]] = {field.id: [] for field in self.config.fields} + for i, x in enumerate(times): + if x < view_start or x > view_end: + continue + xs.append(x) + for field in self.config.fields: + series = self._series[field.id] + if i < len(series): + out[field.id].append(float(series[i])) + else: + out[field.id].append(float("nan")) + return xs, out + + def clear(self) -> None: + self.timestamps.clear() + for series in self._series.values(): + series.clear() + + def nearest_index(self, t_rel: float) -> int: + """相对时间 → 最近采样下标;空缓冲返回 -1。""" + times = relative_times(list(self.timestamps)) + if not times: + return -1 + best_i = 0 + best_d = abs(times[0] - float(t_rel)) + for i, x in enumerate(times): + d = abs(x - float(t_rel)) + if d < best_d: + best_d = d + best_i = i + return best_i + + def values_at_relative(self, t_rel: float) -> Dict[str, float]: + """取相对时间最近点的各字段值。""" + idx = self.nearest_index(t_rel) + out: Dict[str, float] = {} + if idx < 0: + return out + for field in self.config.fields: + series = self._series.get(field.id) + if series is None or idx >= len(series): + out[field.id] = float("nan") + else: + try: + out[field.id] = float(series[idx]) + except (TypeError, ValueError): + out[field.id] = float("nan") + return out + + def relative_time_at(self, index: int) -> Optional[float]: + times = relative_times(list(self.timestamps)) + if index < 0 or index >= len(times): + return None + return float(times[index]) + + def torque_ylim(self) -> Tuple[float, float]: + field = self.config.field_by_id(FIELD_TORQUE.id) + return field.ylim() if field else (TORQUE_YMIN, TORQUE_YMAX) + + def health_ylim(self) -> Tuple[float, float]: + field = self.config.field_by_id(FIELD_HEALTH.id) + return field.ylim() if field else (HEALTH_YMIN, HEALTH_YMAX) + + +def view_bounds( + t_max: float, + view_end: Optional[float], + view_window_s: float, +) -> Tuple[float, float]: + """计算滑动视窗 [start, end],宽度固定为 view_window_s。 + + - 直播:end = t_max + - 回溯:end = clamp(view_end, …) + - 数据不足一窗时:钉在 [0, window](右侧可留空) + """ + window = float(view_window_s) if view_window_s and view_window_s > 0 else VIEW_WINDOW_S + t_max = max(0.0, float(t_max)) + if view_end is None: + end = t_max + else: + end = min(max(0.0, float(view_end)), t_max) + start = end - window + if start < 0.0: + start = 0.0 + end = window + else: + end = start + window + if end <= start: + end = start + window + return start, end + + +def series_points(xs: List[float], ys: List[float]) -> List[Tuple[float, float]]: + """过滤 NaN/None,生成 (x, y) 点列供折线渲染。""" + points: List[Tuple[float, float]] = [] + for x, y in zip(xs, ys): + if y is None or (isinstance(y, float) and math.isnan(y)): + continue + points.append((float(x), float(y))) + return points diff --git a/models/chart_overlay.py b/models/chart_overlay.py new file mode 100644 index 00000000..84c22f1f --- /dev/null +++ b/models/chart_overlay.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +""" +模块:models.chart_overlay +职责:兼容旧 import;实现已并入 models.data_bridge +""" + +from __future__ import annotations + +from models.data_bridge import ( + ChartLevelOverlay, + ChartOverlayConfig, + FieldOverlay, + LapMarkerOverlaySpec, + ThresholdSpanOverlaySpec, + build_overlay_config, + kind_resolver, + load_overlay_config, +) + +__all__ = [ + "ChartLevelOverlay", + "ChartOverlayConfig", + "FieldOverlay", + "LapMarkerOverlaySpec", + "ThresholdSpanOverlaySpec", + "build_overlay_config", + "kind_resolver", + "load_overlay_config", +] diff --git a/models/data_bridge.py b/models/data_bridge.py new file mode 100644 index 00000000..33cbf282 --- /dev/null +++ b/models/data_bridge.py @@ -0,0 +1,529 @@ +# -*- coding: utf-8 -*- +""" +模块:models.data_bridge +职责:图表字段模版(出数 + 轴/线型/蒙版元数据)与 CSV/缓存 → 字段值 +约定:缓存/CSV 为时间×列二维表;读用 pandas,算用 numpy +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple + +import numpy as np +import pandas as pd + +from services.simulator import ( + AIRSPEED_VAR, + BANK_VAR, + GROUND_ALT_VAR, + GROUND_SPEED_VAR, + METERS_TO_FEET, + PITCH_VAR, + PLANE_AGL_VAR, + PLANE_ALT_VAR, + PLANE_HEADING_VAR, + PLANE_LAT_VAR, + PLANE_LON_VAR, + optional_float, + rad_to_deg, +) + +# 缓存行关心的列(与 CSV 表头对齐) +ROW_KEYS = ( + "timestamp_s", + "airspeed_kts", + "groundspeed_kts", + "health_pct", + "torque", + "pitch_deg", + "bank_deg", + "lap", +) + +DEFAULT_SPAN_COLORS: Dict[str, str] = { + "orange": "#f39c12", + "blue": "#1e90ff", + "red": "#e74c3c", +} + + +# --------------------------------------------------------------------------- +# 蒙版 / 字段模版 +# --------------------------------------------------------------------------- + +@dataclass(frozen=True) +class FieldOverlay: + """挂在字段上的蒙版;无需要则 overlays=() 不填。""" + + kind: str = "" # "" | threshold_span | … + enabled: bool = False + direction: str = "x" # x | y | xy + alpha: Optional[float] = None + colors: Optional[Dict[str, str]] = None + resolver: str = "" # e.g. health_span_kind + z_order: Optional[int] = None + overlay_id: str = "" # 空则用 {field_id}_{kind} + axis_field_id: str = "" # 空则同字段 id + + +@dataclass(frozen=True) +class ChartLevelOverlay: + """图级蒙版(不绑 Y 字段),如圈完成竖线。""" + + overlay_id: str + kind: str # lap_marker | … + enabled: bool = True + z_order: int = 20 + label_template: str = "LAP {lap} FINISHED" + + +@dataclass +class FieldDef: + """ + 字段模版:有属性则填,无则保持空/None(后续处理用默认)。 + 增字段:在 FIELDS 加一条(或子类 override extract_*)。 + """ + + id: str + csv_key: str = "" + label: str = "" + unit: str = "" + ymin: Optional[float] = None + ymax: Optional[float] = None + side: str = "left" # left | right + color: str = "" # 空 → theme[color_key|id] + color_key: str = "" + line_width: Optional[float] = None + line_style: str = "" # "" | solid | dash | dot | dashdot + overlays: Tuple[FieldOverlay, ...] = () + + @property + def health_spans(self) -> bool: + return any(o.kind == "threshold_span" and o.enabled for o in self.overlays) + + def extract_row(self, row: Mapping[str, Any] | pd.Series) -> float: + """单行透传;无 csv_key 的加工字段请子类 override。""" + if not self.csv_key: + return float("nan") + return _to_float(row.get(self.csv_key) if hasattr(row, "get") else row[self.csv_key]) + + def extract_series(self, df: pd.DataFrame) -> pd.Series: + """整列时间序列;默认取 csv_key。""" + n = len(df) + if not self.csv_key or self.csv_key not in df.columns: + return pd.Series(np.full(n, np.nan, dtype=float), index=df.index, dtype=float) + return pd.to_numeric(df[self.csv_key], errors="coerce").astype(float) + + +class AirspeedField(FieldDef): + """空速:列 NaN 时回退地速。""" + + def extract_row(self, row: Mapping[str, Any] | pd.Series) -> float: + v = _to_float(row.get("airspeed_kts") if hasattr(row, "get") else None) + if v != v: + v = _to_float(row.get("groundspeed_kts") if hasattr(row, "get") else None) + return v + + def extract_series(self, df: pd.DataFrame) -> pd.Series: + ias = ( + pd.to_numeric(df["airspeed_kts"], errors="coerce") + if "airspeed_kts" in df.columns + else pd.Series(np.nan, index=df.index, dtype=float) + ) + gs = ( + pd.to_numeric(df["groundspeed_kts"], errors="coerce") + if "groundspeed_kts" in df.columns + else pd.Series(np.nan, index=df.index, dtype=float) + ) + return ias.fillna(gs).astype(float) + + +def _to_float(value: Any) -> float: + v = optional_float(value) + return float("nan") if v is None else float(v) + + +def _window_diff(values: Sequence[float] | np.ndarray | pd.Series) -> float: + arr = np.asarray(values, dtype=float) + if arr.size < 2: + return float("nan") + d = arr[-1] - arr[-2] + return float(d) if np.isfinite(d) else float("nan") + + +def airspeed_change_rate(data: Sequence[float] | np.ndarray | pd.Series) -> float: + """窗口:空速序列 → 最新一阶差分。""" + return _window_diff(data) + + +def speed_change_rate(data: Sequence[float] | np.ndarray | pd.Series) -> float: + """窗口:地速序列 → 最新一阶差分。""" + return _window_diff(data) + + +# --------------------------------------------------------------------------- +# 字段注册表(唯一真相源) +# --------------------------------------------------------------------------- + +FIELDS: Dict[str, FieldDef] = { + "airspeed": AirspeedField( + id="airspeed", + csv_key="airspeed_kts", + label="空速", + unit="kts", + ymin=0.0, + ymax=300.0, + side="left", + color_key="airspeed", + ), + "groundspeed": FieldDef( + id="groundspeed", + csv_key="groundspeed_kts", + label="地速", + unit="kts", + ymin=0.0, + ymax=300.0, + side="left", + color_key="groundspeed", + ), + "health": FieldDef( + id="health", + csv_key="health_pct", + label="健康值", + unit="%", + ymin=65.0, + ymax=110.0, + side="right", + color_key="health", + overlays=( + FieldOverlay( + kind="threshold_span", + enabled=True, + direction="x", + alpha=0.18, + colors=dict(DEFAULT_SPAN_COLORS), + resolver="health_span_kind", + z_order=10, + overlay_id="health_span", + ), + ), + ), + "torque": FieldDef( + id="torque", + csv_key="torque", + label="扭矩", + unit="lb-ft", + ymin=220.0, + ymax=1200.0, + side="left", + color_key="torque", + ), + "pitch": FieldDef( + id="pitch", + csv_key="pitch_deg", + label="俯仰角", + unit="deg", + ymin=-90.0, + ymax=90.0, + side="right", + color_key="pitch", + ), + "bank": FieldDef( + id="bank", + csv_key="bank_deg", + label="坡度", + unit="deg", + ymin=-180.0, + ymax=180.0, + side="right", + color_key="bank", + ), +} + +# 图级蒙版(不绑字段) +CHART_LEVEL_OVERLAYS: Tuple[ChartLevelOverlay, ...] = ( + ChartLevelOverlay( + overlay_id="lap_marker", + kind="lap_marker", + enabled=True, + z_order=20, + label_template="LAP {lap} FINISHED", + ), +) + + +# --------------------------------------------------------------------------- +# 行 / 帧出入 +# --------------------------------------------------------------------------- + +def as_row(source: Any) -> Dict[str, Any]: + """TrajectorySample / Mapping → 统一缓存行。""" + if isinstance(source, pd.Series): + return source.to_dict() + if isinstance(source, Mapping): + return dict(source) + return {key: getattr(source, key, float("nan")) for key in ROW_KEYS} + + +def read_aq_snapshot( + aq, + *, + torque: Optional[float] = None, + health_ratio: Optional[float] = None, +) -> Dict[str, Any]: + """ + 单拍 SimConnect 快照:图表行与轨迹录制共用同一读出口。 + 缺 aq 时仍返回图表字段(NaN);缺 lat/lon 时位置字段为 None。 + """ + health_pct = ( + float(health_ratio) * 100.0 if health_ratio is not None else float("nan") + ) + torque_v = float(torque) if torque is not None else float("nan") + empty = { + "latitude": None, + "longitude": None, + "altitude_ft": float("nan"), + "agl_ft": float("nan"), + "heading_deg": float("nan"), + "airspeed_kts": float("nan"), + "groundspeed_kts": float("nan"), + "pitch_deg": float("nan"), + "bank_deg": float("nan"), + "health_pct": health_pct, + "torque": torque_v, + } + if not aq: + return empty + + lat = optional_float(aq.get(PLANE_LAT_VAR)) + lon = optional_float(aq.get(PLANE_LON_VAR)) + ground = optional_float(aq.get(GROUND_SPEED_VAR)) + ias = optional_float(aq.get(AIRSPEED_VAR)) + if ias is None: + ias = ground + alt_msl = optional_float(aq.get(PLANE_ALT_VAR), 0.0) or 0.0 + agl = optional_float(aq.get(PLANE_AGL_VAR), 0.0) or 0.0 + if agl <= 0.0 and alt_msl > 0.0: + ground_alt_m = aq.get(GROUND_ALT_VAR) + if ground_alt_m is not None: + ga = optional_float(ground_alt_m, 0.0) or 0.0 + agl = max(0.0, alt_msl - ga * METERS_TO_FEET) + + return { + "latitude": lat, + "longitude": lon, + "altitude_ft": float(alt_msl), + "agl_ft": float(agl), + "heading_deg": rad_to_deg(aq.get(PLANE_HEADING_VAR), float("nan")), + "airspeed_kts": float(ias) if ias is not None else float("nan"), + "groundspeed_kts": float(ground) if ground is not None else float("nan"), + "pitch_deg": rad_to_deg(aq.get(PITCH_VAR), float("nan")), + "bank_deg": rad_to_deg(aq.get(BANK_VAR), float("nan")), + "health_pct": health_pct, + "torque": torque_v, + } + + +def row_from_aq( + aq, + *, + torque: Optional[float] = None, + health_ratio: Optional[float] = None, +) -> Dict[str, Any]: + """从 SimConnect 快照取图表列(列名同 CSV)。""" + snap = read_aq_snapshot(aq, torque=torque, health_ratio=health_ratio) + return { + "airspeed_kts": snap["airspeed_kts"], + "groundspeed_kts": snap["groundspeed_kts"], + "health_pct": snap["health_pct"], + "torque": snap["torque"], + "pitch_deg": snap["pitch_deg"], + "bank_deg": snap["bank_deg"], + } + + +def extract( + source: Any, + field_ids: Optional[Iterable[str]] = None, +) -> Dict[str, float]: + """缓存行 → 图表字段字典(跳过 NaN/Inf)。""" + row = as_row(source) + series = pd.Series(row) + ids = list(field_ids) if field_ids is not None else list(FIELDS.keys()) + out: Dict[str, float] = {} + for fid in ids: + field_def = FIELDS.get(fid) + if field_def is None: + continue + try: + value = float(field_def.extract_row(series)) + except (TypeError, ValueError): + continue + if not np.isfinite(value): + continue + out[fid] = value + return out + + +def samples_to_frame(samples: Sequence[Any]) -> pd.DataFrame: + """采样序列 → DataFrame。""" + if not samples: + return pd.DataFrame(columns=list(ROW_KEYS)) + return pd.DataFrame([as_row(s) for s in samples]) + + +def load_csv_frame(path: Path | str) -> pd.DataFrame: + """轨迹 CSV → DataFrame(列保持字符串表头)。完整载荷请用 load_track_bundle。""" + df = pd.read_csv(path) + for col in df.columns: + if col == "lap": + df[col] = pd.to_numeric(df[col], errors="coerce").fillna(0).astype(int) + else: + df[col] = pd.to_numeric(df[col], errors="coerce") + return df + + +def frame_to_history(df: pd.DataFrame, config=None): + """ + DataFrame → ChartHistoryBuffer(列经 FieldDef.extract_series)。 + config 默认 TRAINING_CHART。 + """ + from models.chart import ChartHistoryBuffer, TRAINING_CHART + + buf = ChartHistoryBuffer(config or TRAINING_CHART) + if df is None or df.empty or "timestamp_s" not in df.columns: + return buf + ts = pd.to_numeric(df["timestamp_s"], errors="coerce").to_numpy(dtype=float) + cols = { + fid: fdef.extract_series(df).to_numpy(dtype=float) + for fid, fdef in FIELDS.items() + } + for i in range(len(df)): + t = ts[i] + if not np.isfinite(t): + continue + values: Dict[str, float] = {} + for fid, arr in cols.items(): + v = float(arr[i]) + if np.isfinite(v): + values[fid] = v + if values: + buf.append(float(t), **values) + return buf + + +def trajectory_to_history(traj) -> Any: + """Trajectory.samples → history(对比与直播同一套 FieldDef)。""" + return frame_to_history(samples_to_frame(traj.samples)) + + +def load_track_bundle(path: Path | str) -> Tuple[Any, Any]: + """ + 轨迹 CSV 一次加载 → (Trajectory, ChartHistoryBuffer)。 + 对比 / 航迹图 / 趋势共用此入口,避免二次读盘。 + """ + from models.chart import ChartHistoryBuffer + from models.shadow_plane import load_trajectory_csv + + traj = load_trajectory_csv(Path(path)) + history = trajectory_to_history(traj) + assert isinstance(history, ChartHistoryBuffer) + return traj, history + + +# --------------------------------------------------------------------------- +# 蒙版收集 → 供 charts.overlay.registry 使用的 Spec +# --------------------------------------------------------------------------- + +@dataclass(frozen=True) +class ThresholdSpanOverlaySpec: + """阈值色带蒙版规格(由 FieldOverlay 展开)。""" + + overlay_id: str + overlay_type: str = "threshold_span" + source_field_id: str = "health" + axis_field_id: str = "health" + direction: str = "x" + kind_resolver_name: str = "health_span_kind" + alpha: float = 0.18 + colors: Dict[str, str] = field(default_factory=lambda: dict(DEFAULT_SPAN_COLORS)) + z_order: int = 10 + enabled: bool = True + + +@dataclass(frozen=True) +class LapMarkerOverlaySpec: + """圈完成竖线/标签蒙版规格。""" + + overlay_id: str = "lap_marker" + overlay_type: str = "lap_marker" + z_order: int = 20 + enabled: bool = True + label_template: str = "LAP {lap} FINISHED" + + +@dataclass +class ChartOverlayConfig: + """蒙版规格列表容器。""" + + overlays: List[object] = field(default_factory=list) + + +def build_overlay_config( + fields: Optional[Mapping[str, FieldDef]] = None, + chart_overlays: Optional[Sequence[ChartLevelOverlay]] = None, +) -> ChartOverlayConfig: + """从 FieldDef.overlays + 图级蒙版组装配置(替代 JSON)。""" + field_map = fields if fields is not None else FIELDS + level = chart_overlays if chart_overlays is not None else CHART_LEVEL_OVERLAYS + specs: List[object] = [] + for fdef in field_map.values(): + for ov in fdef.overlays: + if ov.kind != "threshold_span": + continue + colors = dict(DEFAULT_SPAN_COLORS) + if ov.colors: + colors.update(ov.colors) + oid = ov.overlay_id or f"{fdef.id}_{ov.kind}" + specs.append( + ThresholdSpanOverlaySpec( + overlay_id=oid, + source_field_id=fdef.id, + axis_field_id=ov.axis_field_id or fdef.id, + direction=ov.direction or "x", + kind_resolver_name=ov.resolver or "health_span_kind", + alpha=float(ov.alpha) if ov.alpha is not None else 0.18, + colors=colors, + z_order=int(ov.z_order) if ov.z_order is not None else 10, + enabled=bool(ov.enabled), + ) + ) + for cov in level: + if cov.kind == "lap_marker": + specs.append( + LapMarkerOverlaySpec( + overlay_id=cov.overlay_id, + z_order=int(cov.z_order), + enabled=bool(cov.enabled), + label_template=cov.label_template, + ) + ) + return ChartOverlayConfig(overlays=specs) + + +def kind_resolver(name: str): + """按名称查找阈值→配色键的解析函数。""" + from models.chart import health_span_kind + + resolvers = { + "health_span_kind": health_span_kind, + } + return resolvers.get(name) + + +def load_overlay_config(path=None) -> ChartOverlayConfig: + """从 FieldDef.overlays + CHART_LEVEL_OVERLAYS 组装蒙版配置。""" + _ = path + return build_overlay_config() diff --git a/models/hardware_monitor.py b/models/hardware_monitor.py new file mode 100644 index 00000000..ad8e6c90 --- /dev/null +++ b/models/hardware_monitor.py @@ -0,0 +1,654 @@ +# -*- coding: utf-8 -*- +""" +模块:models.hardware_monitor +职责:USB 游戏控制器枚举、轴映射/校准;对外唯一摇杆取数口(UI + 轨迹 CSV) +依赖:pygame(SDL 摇杆);配置读写 config/hardware_monitor.json +""" + +from __future__ import annotations + +import json +import os +from dataclasses import asdict, dataclass, field +from pathlib import Path +from typing import Any, Dict, List, Optional, Sequence, Tuple + +# --------------------------------------------------------------------------- +# 轴语义(pygame 轴序号由设定页映射) +# --------------------------------------------------------------------------- + +AXIS_ROLES: Tuple[str, ...] = ("pitch", "roll", "yaw", "throttle") +AXIS_ROLE_LABELS = { + "pitch": "俯仰", + "roll": "滚转", + "yaw": "偏航", + "throttle": "油门", +} +# pygame 轴上限;具体设备轴数以枚举结果为准 +PHYSICAL_AXIS_MAX = 16 +# 兼容旧设定页常量名:默认标签 Axis0..Axis15 +PHYSICAL_AXIS_NAMES: Tuple[str, ...] = tuple(f"Axis{i}" for i in range(PHYSICAL_AXIS_MAX)) +PHYSICAL_AXIS_COUNT = PHYSICAL_AXIS_MAX + +# pygame.get_axis() 典型范围 +_DEFAULT_RAW_MIN = -1.0 +_DEFAULT_RAW_MAX = 1.0 +_DEFAULT_RAW_NEUTRAL = 0.0 + +_CONFIG_GENERATION = 0 +_pygame_mod: Any = None +_pygame_ready = False + + +def config_generation() -> int: + return _CONFIG_GENERATION + + +def default_config_path() -> Path: + return Path(__file__).resolve().parent.parent / "config" / "hardware_monitor.json" + + +def physical_axis_label(index: int) -> str: + return f"Axis{int(index)}" + + +# --------------------------------------------------------------------------- +# 数据结构 +# --------------------------------------------------------------------------- + + +@dataclass +class AxisCalibration: + """单轴映射与校准(raw → [-1, 1],0 为中立)。""" + + index: int = 0 + invert: bool = False + raw_min: float = _DEFAULT_RAW_MIN + raw_max: float = _DEFAULT_RAW_MAX + raw_neutral: float = _DEFAULT_RAW_NEUTRAL + + def clamp_index(self, axis_count: Optional[int] = None) -> None: + limit = PHYSICAL_AXIS_MAX - 1 + if axis_count is not None and axis_count > 0: + limit = max(0, int(axis_count) - 1) + self.index = max(0, min(limit, int(self.index))) + + def ensure_order(self) -> None: + lo = min(float(self.raw_min), float(self.raw_max)) + hi = max(float(self.raw_min), float(self.raw_max)) + self.raw_min = lo + self.raw_max = hi + self.raw_neutral = max(lo, min(hi, float(self.raw_neutral))) + + def normalize(self, raw: float) -> float: + """将原始轴值归一到 [-1, 1],中立为 0。""" + self.ensure_order() + v = float(raw) + n = float(self.raw_neutral) + lo = float(self.raw_min) + hi = float(self.raw_max) + if v >= n: + span = hi - n + out = (v - n) / span if span > 1e-6 else 0.0 + else: + span = n - lo + out = (v - n) / span if span > 1e-6 else 0.0 + out = max(-1.0, min(1.0, out)) + if self.invert: + out = -out + return out + + +@dataclass +class DeviceIdentity: + """用于持久化匹配的设备身份。""" + + index: int = 0 + name: str = "" + vendor_id: int = 0 + product_id: int = 0 + guid: str = "" + + def matches(self, other: "DeviceIdentity") -> bool: + if other is None: + return False + if self.guid and other.guid and self.guid == other.guid: + return True + if self.vendor_id and self.product_id: + if self.vendor_id == other.vendor_id and self.product_id == other.product_id: + return True + if self.name and other.name and self.name == other.name: + return True + return False + + +@dataclass +class JoystickInfo: + """枚举得到的设备快照。""" + + index: int + name: str + vendor_id: int + product_id: int + num_axes: int + num_buttons: int + guid: str = "" + axis_raw_ranges: Dict[int, Tuple[float, float]] = field(default_factory=dict) + + def identity(self) -> DeviceIdentity: + return DeviceIdentity( + index=self.index, + name=self.name, + vendor_id=self.vendor_id, + product_id=self.product_id, + guid=self.guid, + ) + + +@dataclass +class StickAxes: + """归一化操纵输出;各轴 [-1, 1],0 为中立。""" + + pitch: float = 0.0 + roll: float = 0.0 + yaw: float = 0.0 + throttle: float = 0.0 + connected: bool = False + device_name: str = "" + + def as_dict(self) -> Dict[str, float]: + return { + "pitch": self.pitch, + "roll": self.roll, + "yaw": self.yaw, + "throttle": self.throttle, + } + + def record_fields(self) -> Dict[str, float]: + """轨迹 CSV / TrajectorySample 字段;未连接返回空 dict(样本保持 nan)。""" + if not self.connected: + return {} + return { + "stick_pitch": float(self.pitch), + "stick_roll": float(self.roll), + "stick_yaw": float(self.yaw), + "stick_throttle": float(self.throttle), + } + + +@dataclass +class HardwareMonitorConfig: + """设定页写入、监控器只读消费的完整配置。""" + + device: DeviceIdentity = field(default_factory=DeviceIdentity) + axes: Dict[str, AxisCalibration] = field(default_factory=dict) + calibrated: bool = False + + def __post_init__(self) -> None: + if not self.axes: + self.axes = default_axis_map() + for role in AXIS_ROLES: + if role not in self.axes: + self.axes[role] = default_axis_map()[role] + cal = self.axes[role] + if isinstance(cal, dict): + # 兼容旧 winmm 配置字段 + payload = dict(cal) + self.axes[role] = AxisCalibration( + index=int(payload.get("index", 0)), + invert=bool(payload.get("invert", False)), + raw_min=float(payload.get("raw_min", _DEFAULT_RAW_MIN)), + raw_max=float(payload.get("raw_max", _DEFAULT_RAW_MAX)), + raw_neutral=float(payload.get("raw_neutral", _DEFAULT_RAW_NEUTRAL)), + ) + self.axes[role].clamp_index() + self.axes[role].ensure_order() + if isinstance(self.device, dict): + d = dict(self.device) + self.device = DeviceIdentity( + index=int(d.get("index", 0)), + name=str(d.get("name", "") or ""), + vendor_id=int(d.get("vendor_id", 0)), + product_id=int(d.get("product_id", 0)), + guid=str(d.get("guid", "") or ""), + ) + + def axis(self, role: str) -> AxisCalibration: + return self.axes[role] + + +def default_axis_map() -> Dict[str, AxisCalibration]: + """常见飞行摇杆默认:0=滚转 1=俯仰 2=油门 3=偏航。""" + return { + "roll": AxisCalibration(index=0), + "pitch": AxisCalibration(index=1), + "throttle": AxisCalibration(index=2, invert=True), + "yaw": AxisCalibration(index=3), + } + + +def default_config() -> HardwareMonitorConfig: + return HardwareMonitorConfig( + device=DeviceIdentity(), + axes=default_axis_map(), + calibrated=False, + ) + + +# --------------------------------------------------------------------------- +# 配置读写 +# --------------------------------------------------------------------------- + + +def load_config(path: Optional[Path] = None) -> HardwareMonitorConfig: + p = Path(path) if path is not None else default_config_path() + if not p.is_file(): + return default_config() + try: + data = json.loads(p.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return default_config() + if not isinstance(data, dict): + return default_config() + device = data.get("device") or {} + axes_raw = data.get("axes") or {} + axes: Dict[str, AxisCalibration] = {} + defaults = default_axis_map() + for role in AXIS_ROLES: + src = axes_raw.get(role) or {} + if not isinstance(src, dict): + src = {} + # 旧 winmm 量程(0..65535)自动迁移为 pygame 量程 + raw_min = float(src.get("raw_min", _DEFAULT_RAW_MIN)) + raw_max = float(src.get("raw_max", _DEFAULT_RAW_MAX)) + raw_neutral = float(src.get("raw_neutral", _DEFAULT_RAW_NEUTRAL)) + if raw_max > 2.0 or raw_min < -2.0: + raw_min, raw_max, raw_neutral = _DEFAULT_RAW_MIN, _DEFAULT_RAW_MAX, _DEFAULT_RAW_NEUTRAL + axes[role] = AxisCalibration( + index=int(src.get("index", defaults[role].index)), + invert=bool(src.get("invert", defaults[role].invert)), + raw_min=raw_min, + raw_max=raw_max, + raw_neutral=raw_neutral, + ) + cfg = HardwareMonitorConfig( + device=DeviceIdentity( + index=int(device.get("index", 0)), + name=str(device.get("name", "") or ""), + vendor_id=int(device.get("vendor_id", 0)), + product_id=int(device.get("product_id", 0)), + guid=str(device.get("guid", "") or ""), + ), + axes=axes, + calibrated=bool(data.get("calibrated", False)), + ) + return cfg + + +def save_config(config: HardwareMonitorConfig, path: Optional[Path] = None) -> Path: + global _CONFIG_GENERATION + p = Path(path) if path is not None else default_config_path() + p.parent.mkdir(parents=True, exist_ok=True) + payload = { + "device": asdict(config.device), + "axes": {role: asdict(config.axis(role)) for role in AXIS_ROLES}, + "calibrated": bool(config.calibrated), + } + p.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") + _CONFIG_GENERATION += 1 + return p + + +# --------------------------------------------------------------------------- +# pygame / SDL 后端 +# --------------------------------------------------------------------------- + + +def _parse_sdl_guid(guid: str) -> Tuple[int, int]: + """从 SDL joystick GUID 解析 vendor/product(USB 布局)。""" + g = (guid or "").strip().lower() + if len(g) < 20: + return 0, 0 + try: + raw = bytes.fromhex(g) + except ValueError: + return 0, 0 + if len(raw) < 10: + return 0, 0 + vendor = int.from_bytes(raw[4:6], "little") + product = int.from_bytes(raw[8:10], "little") + return vendor, product + + +def _ensure_pygame() -> bool: + """初始化 pygame 摇杆;display 用 dummy,保证 event.pump 可用且不闪 SDL 窗。""" + global _pygame_mod, _pygame_ready + if _pygame_ready and _pygame_mod is not None: + return True + try: + # 必须在 import pygame 之前强制写入(setdefault 不够) + os.environ["SDL_VIDEODRIVER"] = "dummy" + os.environ["SDL_AUDIODRIVER"] = "dummy" + import pygame + + # joystick 读轴依赖 event.pump,而 pump 需要已初始化的 video 子系统; + # 用 dummy 驱动做 pygame.init(),避免真开窗,同时让轴值能刷新。 + if not pygame.get_init(): + pygame.init() + if not pygame.joystick.get_init(): + pygame.joystick.init() + _pygame_mod = pygame + _pygame_ready = True + return True + except Exception: + _pygame_mod = None + _pygame_ready = False + return False + + +def _pump_events() -> None: + pg = _pygame_mod + if pg is None: + return + try: + pg.event.pump() + except Exception: + pass + + +def refresh_joystick_subsystem() -> None: + """重新扫描热插拔设备。""" + if not _ensure_pygame(): + return + pg = _pygame_mod + try: + pg.joystick.quit() + except Exception: + pass + try: + pg.joystick.init() + except Exception: + pass + + +def list_joysticks(*, rescan: bool = False) -> List[JoystickInfo]: + """枚举当前 pygame 可见的游戏控制器。""" + if not _ensure_pygame(): + return [] + if rescan: + refresh_joystick_subsystem() + pg = _pygame_mod + _pump_events() + out: List[JoystickInfo] = [] + try: + n = int(pg.joystick.get_count()) + except Exception: + return [] + for i in range(n): + try: + joy = pg.joystick.Joystick(i) + joy.init() + name = str(joy.get_name() or f"Joystick {i}") + num_axes = int(joy.get_numaxes()) + num_buttons = int(joy.get_numbuttons()) + guid = "" + if hasattr(joy, "get_guid"): + try: + guid = str(joy.get_guid() or "") + except Exception: + guid = "" + vendor_id, product_id = _parse_sdl_guid(guid) + ranges = {a: (_DEFAULT_RAW_MIN, _DEFAULT_RAW_MAX) for a in range(num_axes)} + out.append( + JoystickInfo( + index=i, + name=name, + vendor_id=vendor_id, + product_id=product_id, + num_axes=num_axes, + num_buttons=num_buttons, + guid=guid, + axis_raw_ranges=ranges, + ) + ) + except Exception: + continue + return out + + +def resolve_device_index( + identity: DeviceIdentity, + devices: Optional[Sequence[JoystickInfo]] = None, +) -> Optional[int]: + """按配置身份在当前设备列表中解析 index;未在设定页选定设备时返回 None。""" + devs = list(devices) if devices is not None else list_joysticks() + if identity is None: + return None + has_id = ( + bool((identity.guid or "").strip()) + or bool((identity.name or "").strip()) + or (int(identity.vendor_id) and int(identity.product_id)) + ) + if not has_id: + return None + for d in devs: + if identity.matches(d.identity()): + return d.index + for d in devs: + if d.index == identity.index and (not identity.name or identity.name == d.name): + return d.index + return None + + +def open_joystick(device_index: int) -> Any: + """打开并初始化指定 index 的 pygame.Joystick。""" + if not _ensure_pygame(): + return None + pg = _pygame_mod + _pump_events() + try: + if device_index < 0 or device_index >= int(pg.joystick.get_count()): + return None + joy = pg.joystick.Joystick(int(device_index)) + joy.init() + return joy + except Exception: + return None + + +def read_raw_axes(device_index: int, joy: Any = None) -> Optional[List[float]]: + """读取指定设备全部轴原始值(pygame:通常约 [-1, 1])。""" + if not _ensure_pygame(): + return None + _pump_events() + handle = joy + if handle is None: + handle = open_joystick(device_index) + if handle is None: + return None + try: + n = int(handle.get_numaxes()) + return [float(handle.get_axis(i)) for i in range(n)] + except Exception: + return None + + +# --------------------------------------------------------------------------- +# 校准辅助 +# --------------------------------------------------------------------------- + + +def capture_neutral(config: HardwareMonitorConfig, raw: Sequence[float]) -> HardwareMonitorConfig: + """用当前原始值写入各映射轴的中立位。""" + for role in AXIS_ROLES: + cal = config.axis(role) + idx = cal.index + if 0 <= idx < len(raw): + cal.raw_neutral = float(raw[idx]) + cal.ensure_order() + return config + + +def capture_travel_extents( + config: HardwareMonitorConfig, + samples: Sequence[Sequence[float]], +) -> HardwareMonitorConfig: + """根据采样序列确认各映射轴最大行程(min/max)。""" + if not samples: + return config + for role in AXIS_ROLES: + cal = config.axis(role) + idx = cal.index + vals = [float(s[idx]) for s in samples if 0 <= idx < len(s)] + if not vals: + continue + cal.raw_min = min(vals) + cal.raw_max = max(vals) + cal.ensure_order() + config.calibrated = True + return config + + +def seed_ranges_from_device(config: HardwareMonitorConfig, info: JoystickInfo) -> HardwareMonitorConfig: + """用设备能力范围初始化未校准轴的 min/max。""" + for role in AXIS_ROLES: + cal = config.axis(role) + cal.clamp_index(info.num_axes) + rng = info.axis_raw_ranges.get(cal.index) + if rng is None: + cal.raw_min, cal.raw_max = _DEFAULT_RAW_MIN, _DEFAULT_RAW_MAX + else: + cal.raw_min, cal.raw_max = float(rng[0]), float(rng[1]) + cal.raw_neutral = (cal.raw_min + cal.raw_max) * 0.5 + cal.ensure_order() + return config + + +# --------------------------------------------------------------------------- +# 监控器:只接受设定参数,输出归一化数据 +# --------------------------------------------------------------------------- + + +class HardwareMonitor: + """ + 运行时监控器。 + 调用方传入 HardwareMonitorConfig(或已保存配置),poll() 输出 StickAxes。 + """ + + def __init__(self, config: Optional[HardwareMonitorConfig] = None) -> None: + self._config = config or load_config() + self._device_index: Optional[int] = None + self._device_name = "" + self._joy: Any = None + self._config_gen = config_generation() + self._rebind() + + @property + def config(self) -> HardwareMonitorConfig: + return self._config + + def apply_config(self, config: HardwareMonitorConfig) -> None: + """接受设定页参数并重新绑定设备。""" + self._config = config + self._config_gen = config_generation() + self._rebind() + + def reload(self, path: Optional[Path] = None) -> None: + self.apply_config(load_config(path)) + + def _sync_saved_config(self) -> None: + """设定页保存后自动热加载。""" + gen = config_generation() + if gen != self._config_gen: + self.reload() + + def _close_joy(self) -> None: + self._joy = None + + def _rebind(self) -> None: + self._close_joy() + devs = list_joysticks() + idx = resolve_device_index(self._config.device, devs) + self._device_index = idx + self._device_name = "" + if idx is None: + return + for d in devs: + if d.index == idx: + self._device_name = d.name + break + self._joy = open_joystick(idx) + + def connected(self) -> bool: + return self._device_index is not None and self._joy is not None + + def poll_raw(self) -> Optional[List[float]]: + self._sync_saved_config() + if self._device_index is None or self._joy is None: + self._rebind() + if self._device_index is None: + return None + raw = read_raw_axes(self._device_index, self._joy) + if raw is None: + self._device_index = None + self._device_name = "" + self._close_joy() + return None + return raw + + def poll(self) -> StickAxes: + """读取操作系统原始轴值(仅按设定选轴 / 取反,不做量程归一化)。""" + raw = self.poll_raw() + if raw is None: + return StickAxes(connected=False, device_name="") + cfg = self._config + + def _axis_value(role: str) -> float: + cal = cfg.axis(role) + if 0 <= cal.index < len(raw): + v = float(raw[cal.index]) + return -v if cal.invert else v + return 0.0 + + return StickAxes( + pitch=_axis_value("pitch"), + roll=_axis_value("roll"), + yaw=_axis_value("yaw"), + throttle=_axis_value("throttle"), + connected=True, + device_name=self._device_name, + ) + + +# --------------------------------------------------------------------------- +# 对外唯一取数口(UI 显示与轨迹 CSV 录制均经此) +# --------------------------------------------------------------------------- + +_shared_monitor: Optional[HardwareMonitor] = None + + +def shared_monitor() -> HardwareMonitor: + """进程内唯一运行时监控器;推迟创建以避免启动时 pygame 闪窗。""" + global _shared_monitor + if _shared_monitor is None: + _shared_monitor = HardwareMonitor(load_config()) + return _shared_monitor + + +def poll_stick() -> StickAxes: + """摇杆操纵数据唯一对外接口。UI 与 CSV 均从此取数,勿另建 HardwareMonitor 读业务轴。""" + return shared_monitor().poll() + + +def reload_stick(path: Optional[Path] = None) -> StickAxes: + """重新加载硬件设定后立刻采样一次(设定变更 / 开始监测前调用)。""" + mon = shared_monitor() + mon.reload(path) + return mon.poll() + + +def stick_record_fields(axes: Optional[StickAxes] = None) -> Dict[str, float]: + """将轴数据转为轨迹采样字段;未传 axes 时内部调用 poll_stick()。""" + if axes is None: + axes = poll_stick() + return axes.record_fields() diff --git a/models/lap_reference.py b/models/lap_reference.py new file mode 100644 index 00000000..a4fd745e --- /dev/null +++ b/models/lap_reference.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" +模块:models.lap_reference +职责:四圈圈时数据结构与会话条目提取 +依赖:无 +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Optional, Sequence + + +LAP_COUNT = 4 + + +@dataclass +class LapReference: + """四圈圈时参考(对比蛛网用)。""" + + laps_s: List[Optional[float]] + label: str = "" + + def complete(self) -> bool: + """四圈均有有效正数时长。""" + return len(self.laps_s) >= LAP_COUNT and all( + t is not None and t == t and t > 0 for t in self.laps_s[:LAP_COUNT] + ) + + def as_four(self) -> List[Optional[float]]: + """固定长度 4 的圈时列表(不足补 None)。""" + out: List[Optional[float]] = list(self.laps_s[:LAP_COUNT]) + while len(out) < LAP_COUNT: + out.append(None) + return out + + +def laps_from_session(entries: Sequence[dict], rows: int = LAP_COUNT) -> List[Optional[float]]: + """从 app_controller._lap_times 条目生成 [lap1..lapN]。""" + by_lap = {int(x["lap"]): float(x["time_s"]) for x in entries if x.get("time_s") is not None} + return [by_lap.get(i + 1) for i in range(rows)] diff --git a/models/runtime_watch.py b/models/runtime_watch.py new file mode 100644 index 00000000..023c3ac6 --- /dev/null +++ b/models/runtime_watch.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +""" +模块:models.runtime_watch +职责:监测阶段 / 暂停判定 / 坠毁锁存与轮询门控 +依赖:services.simulator.simulation_clock_advancing +""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional + +from services.simulator import simulation_clock_advancing + + +class MonitorPhase(Enum): + """监测会话阶段(三态互斥)。""" + + IDLE = "idle" + WAITING = "waiting" # 已启动监测,等待解除暂停后开始录制 + RECORDING = "recording" + + +class PauseDetector: + """根据 ABSOLUTE TIME 是否推进判定模拟器是否暂停;结果可缓存供同轮只读。""" + + def __init__(self) -> None: + self._last_sim_time: Optional[float] = None + self._paused = False + + @property + def is_paused(self) -> bool: + return self._paused + + def clear(self) -> None: + self._last_sim_time = None + self._paused = False + + def refresh(self, aq, sm) -> bool: + """刷新并返回当前是否暂停(时钟未推进视为暂停)。""" + if not sm: + self._paused = False + return False + advancing, self._last_sim_time = simulation_clock_advancing( + aq, self._last_sim_time + ) + if advancing: + if getattr(sm, "paused", False): + sm.paused = False + self._paused = False + return False + if getattr(sm, "paused", False): + self._paused = True + return True + self._paused = True + return True + + +def aircraft_has_crashed(aq) -> bool: + """读取 CRASH_FLAG / CRASH_SEQUENCE;不含锁存,可每次直接读取。""" + if not aq: + return False + try: + flag = aq.get("CRASH_FLAG") + if flag is not None and float(flag) != 0: + return True + seq = aq.get("CRASH_SEQUENCE") + if seq is not None and float(seq) != 0: + return True + except Exception: + return False + return False + + +class RuntimeWatch: + """运行时状态中枢:业务逻辑围绕 phase / pause_detector / crash_latched 工作。""" + + def __init__(self) -> None: + self.phase = MonitorPhase.IDLE + self.pause_detector = PauseDetector() + self.crash_latched = False + + def is_active(self) -> bool: + """监测会话已启动(WAITING 或 RECORDING)。""" + return self.phase != MonitorPhase.IDLE + + def is_waiting(self) -> bool: + """处于等待解除暂停/开始录制阶段。""" + return self.phase == MonitorPhase.WAITING + + def is_recording(self) -> bool: + """处于 RECORDING 阶段。""" + return self.phase == MonitorPhase.RECORDING + + def enter_waiting(self) -> None: + """进入 WAITING:清空暂停基准,并解除坠毁锁存(新监测周期)。""" + self.phase = MonitorPhase.WAITING + self.pause_detector.clear() + self.crash_latched = False + + def enter_recording(self) -> None: + """进入 RECORDING。""" + self.phase = MonitorPhase.RECORDING + + def return_to_idle(self) -> None: + """结束监测会话回到 IDLE;保留坠毁锁存(避免同一次坠毁重复处理)。""" + self.phase = MonitorPhase.IDLE + self.pause_detector.clear() + + def reset(self) -> None: + """断连或关闭时完整重置(含坠毁锁存)。""" + self.phase = MonitorPhase.IDLE + self.pause_detector.clear() + self.crash_latched = False + + def should_poll(self, shadow_active: bool, live_readouts: bool) -> bool: + """主 poll 定时器是否应运行。""" + return self.is_active() or shadow_active or live_readouts + + def should_refresh_metrics(self, live_readouts: bool) -> bool: + """指标刷新定时器是否应运行。""" + return self.is_active() or live_readouts diff --git a/models/shadow_plane.py b/models/shadow_plane.py new file mode 100644 index 00000000..047509bf --- /dev/null +++ b/models/shadow_plane.py @@ -0,0 +1,1204 @@ +# -*- coding: utf-8 -*- +"""影子机跟飞:轨迹 10Hz 录制/CSV,回放 60Hz 插值刷位置,姿态不完全冻结,滑动航点辅助。""" + +from __future__ import annotations + +import csv +import threading +import time +from dataclasses import dataclass, field +from datetime import datetime +from pathlib import Path +from typing import List, Optional, Sequence, Tuple + +from SimConnect.Constants import SIMCONNECT_OBJECT_ID_USER +from SimConnect.Enum import SIMCONNECT_DATA_WAYPOINT, SIMCONNECT_WAYPOINT_FLAGS + +from services.simulator import ( + AIRSPEED_VAR, + BANK_VAR, + DATA_POLL_HZ, + GROUND_SPEED_VAR, + PITCH_VAR, + PLANE_AGL_VAR, + PLANE_ALT_VAR, + PLANE_HEADING_VAR, + PLANE_LAT_VAR, + PLANE_LON_VAR, + TITLE_VAR, + optional_float, +) + +# SimConnect 变量名(与 RequestList / services.simulator 一致) +VAR_TITLE = TITLE_VAR +VAR_LAT = PLANE_LAT_VAR +VAR_LON = PLANE_LON_VAR +VAR_ALT = PLANE_ALT_VAR +VAR_AGL = PLANE_AGL_VAR +VAR_HEADING = PLANE_HEADING_VAR +VAR_PITCH = PITCH_VAR +VAR_BANK = BANK_VAR +VAR_AIRSPEED = AIRSPEED_VAR +VAR_GROUND_SPEED = GROUND_SPEED_VAR + +CSV_COLUMNS = ( + "timestamp_s", + "latitude", + "longitude", + "altitude_ft", + "agl_ft", + "heading_deg", + "airspeed_kts", + "groundspeed_kts", + "pitch_deg", + "bank_deg", + "torque", + "health_pct", + "stick_pitch", + "stick_roll", + "stick_yaw", + "stick_throttle", + "lap", +) + +DEFAULT_AIRCRAFT_TITLE = "Mooney Bravo" +SPAWN_TIMEOUT_S = 12.0 +RESET_SETTLE_S = 2.0 +MISSION_AI_SETTLE_S = 1.0 +# 连续位姿写入失败次数达到后停止回放(坠毁/Object 失效) +POSE_FAIL_ABORT = 3 +# 轨迹 CSV / 监测采样率(与主线程 DATA_POLL_HZ 同步) +RECORD_HZ = float(DATA_POLL_HZ) +# 回放刷新率 / 航点密化采样率(由 10Hz 原始轨迹插值上采样) +REPLAY_HZ = 60.0 +REPLAY_DENSE_HZ = 60.0 +SMOKE_REFRESH_S = 1.0 +# 滑动窗口航点:前瞻时长、窗口点数、刷新间隔 +WAYPOINT_HORIZON_S = 2.5 +WAYPOINT_WINDOW_COUNT = 12 +WAYPOINT_REFRESH_S = 0.35 +# 影子机轨迹领先量(秒):以 CSV 时间轴为基准,在参考时刻上向前偏移 +SHADOW_LEAD_MIN_S = 0.0 +SHADOW_LEAD_MAX_S = 6.0 +SHADOW_LEAD_DEFAULT_S = 3.0 + + +@dataclass +class TrajectorySample: + timestamp_s: float + latitude: float + longitude: float + altitude_ft: float + heading_deg: float + airspeed_kts: float + groundspeed_kts: float + pitch_deg: float + bank_deg: float + agl_ft: float = 0.0 + torque: float = float("nan") + health_pct: float = float("nan") + # 摇杆操纵量 [-1, 1];未连接或旧 CSV 缺列为 nan + stick_pitch: float = float("nan") + stick_roll: float = float("nan") + stick_yaw: float = float("nan") + stick_throttle: float = float("nan") + lap: int = 0 + + +@dataclass +class Trajectory: + aircraft_title: str + samples: List[TrajectorySample] + recorded_at: str = "" + laps_s: List[Optional[float]] = field(default_factory=list) + + def first(self) -> TrajectorySample: + return self.samples[0] + + def last(self) -> TrajectorySample: + return self.samples[-1] + + @property + def duration_s(self) -> float: + if len(self.samples) < 2: + return 0.0 + return max(0.0, self.samples[-1].timestamp_s - self.samples[0].timestamp_s) + + +def default_trajectory_dir() -> Path: + return Path(__file__).resolve().parent.parent / "track_data" + + +def format_duration_suffix(total_s: Optional[float]) -> str: + """总时长后缀:MMSScc(分/秒/百分秒各两位);无 total 时为 000000。""" + if total_s is None: + return "000000" + try: + value = float(total_s) + except (TypeError, ValueError): + return "000000" + if value <= 0 or value != value: + return "000000" + minutes = min(99, int(value // 60)) + remainder = value - minutes * 60 + seconds = int(remainder) + centis = int(round((remainder - seconds) * 100)) + if centis >= 100: + centis = 0 + seconds += 1 + if seconds >= 60: + seconds -= 60 + minutes = min(99, minutes + 1) + return f"{minutes:02d}{seconds:02d}{centis:02d}" + + +def _safe_float(value, default=0.0) -> float: + v = optional_float(value, default) + return default if v is None else v + + +def lerp(a: float, b: float, t: float) -> float: + return a + (b - a) * t + + +def lerp_angle_deg(a: float, b: float, t: float) -> float: + """最短角插值(度)。""" + delta = (b - a + 180.0) % 360.0 - 180.0 + return (a + delta * t) % 360.0 + + +def smoothstep(t: float) -> float: + t = max(0.0, min(1.0, t)) + return t * t * (3.0 - 2.0 * t) + + +def catmull_rom(p0: float, p1: float, p2: float, p3: float, t: float) -> float: + """标准 Catmull-Rom 样条(过 p1→p2)。""" + t2 = t * t + t3 = t2 * t + return 0.5 * ( + (2.0 * p1) + + (-p0 + p2) * t + + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3 + ) + + +def catmull_rom_angle_deg(a0: float, a1: float, a2: float, a3: float, t: float) -> float: + """航向角 Catmull-Rom:先相对 a1 展开,再插值。""" + def _unwrap(prev: float, cur: float) -> float: + return prev + ((cur - prev + 180.0) % 360.0 - 180.0) + + u1 = a1 + u0 = _unwrap(u1, a0) + u2 = _unwrap(u1, a2) + u3 = _unwrap(u2, a3) + return catmull_rom(u0, u1, u2, u3, t) % 360.0 + + +def sample_is_airborne(sample: TrajectorySample) -> bool: + speed = max(sample.airspeed_kts, sample.groundspeed_kts) + if sample.agl_ft > 0.5: + return sample.agl_ft >= 8.0 or speed >= 40.0 + return speed >= 40.0 or sample.altitude_ft >= 50.0 + + +def read_sample_from_aq(aq) -> Optional[TrajectorySample]: + """从统一 AQ 快照构建轨迹采样;缺经纬则返回 None。""" + from models.data_bridge import read_aq_snapshot + + try: + snap = read_aq_snapshot(aq) + except OSError: + return None + lat = snap.get("latitude") + lon = snap.get("longitude") + if lat is None or lon is None: + return None + return TrajectorySample( + timestamp_s=0.0, + latitude=float(lat), + longitude=float(lon), + altitude_ft=_safe_float(snap.get("altitude_ft")), + agl_ft=_safe_float(snap.get("agl_ft")), + heading_deg=_safe_float(snap.get("heading_deg")), + airspeed_kts=_safe_float(snap.get("airspeed_kts")), + groundspeed_kts=_safe_float(snap.get("groundspeed_kts")), + pitch_deg=_safe_float(snap.get("pitch_deg")), + bank_deg=_safe_float(snap.get("bank_deg")), + ) + + +def sample_from_snapshot( + snap: dict, + *, + timestamp_s: float = 0.0, + lap: int = 0, + stick_pitch: Optional[float] = None, + stick_roll: Optional[float] = None, + stick_yaw: Optional[float] = None, + stick_throttle: Optional[float] = None, +) -> Optional[TrajectorySample]: + """由 read_aq_snapshot 字典组装 TrajectorySample(缺经纬返回 None)。""" + lat = snap.get("latitude") + lon = snap.get("longitude") + if lat is None or lon is None: + return None + sample = TrajectorySample( + timestamp_s=float(timestamp_s), + latitude=float(lat), + longitude=float(lon), + altitude_ft=_safe_float(snap.get("altitude_ft")), + agl_ft=_safe_float(snap.get("agl_ft")), + heading_deg=_safe_float(snap.get("heading_deg")), + airspeed_kts=_safe_float(snap.get("airspeed_kts")), + groundspeed_kts=_safe_float(snap.get("groundspeed_kts")), + pitch_deg=_safe_float(snap.get("pitch_deg")), + bank_deg=_safe_float(snap.get("bank_deg")), + torque=_safe_float(snap.get("torque"), float("nan")), + health_pct=_safe_float(snap.get("health_pct"), float("nan")), + lap=max(0, int(lap)), + ) + for attr, value in ( + ("stick_pitch", stick_pitch), + ("stick_roll", stick_roll), + ("stick_yaw", stick_yaw), + ("stick_throttle", stick_throttle), + ): + if value is None: + continue + try: + setattr(sample, attr, float(value)) + except (TypeError, ValueError): + pass + return sample + + +def _planar_distance_sq(lat1: float, lon1: float, lat2: float, lon2: float) -> float: + dlat = lat2 - lat1 + dlon = lon2 - lon1 + return dlat * dlat + dlon * dlon + + +def find_trajectory_reference_time( + samples: Sequence[TrajectorySample], + lat: float, + lon: float, +) -> float: + """在 CSV 轨迹上找与给定经纬最接近的参考时刻。""" + if not samples: + return 0.0 + best_t = samples[0].timestamp_s + best_d = float("inf") + for sample in samples: + d = _planar_distance_sq(lat, lon, sample.latitude, sample.longitude) + if d < best_d: + best_d = d + best_t = sample.timestamp_s + return best_t + + +class TrajectoryRecorder: + """任务监测期间记录轨迹,停止时写入 CSV。""" + + def __init__(self, output_dir: Optional[Path] = None): + self.output_dir = Path(output_dir or default_trajectory_dir()) + self._active = False + self._start_mono = 0.0 + self._aircraft_title = "" + self._samples: List[TrajectorySample] = [] + self._last_path: Optional[Path] = None + self._aq = None + self._record_stop = threading.Event() + self._record_thread: Optional[threading.Thread] = None + self._paused = False + self._lock = threading.Lock() + + @property + def active(self) -> bool: + return self._active + + @property + def last_saved_path(self) -> Optional[Path]: + return self._last_path + + def start(self, aircraft_title: Optional[str] = None, aq=None, rate_hz: float = RECORD_HZ) -> None: + """开始录制;采样由主线程 poll 循环驱动(DATA_POLL_HZ=10),勿在后台线程读 aq。""" + self.stop_record_thread() + self._active = True + self._paused = False + self._start_mono = time.monotonic() + self._aircraft_title = (aircraft_title or "").strip() + self._samples = [] + self._last_path = None + self._aq = aq + self._record_stop = threading.Event() + + def set_paused(self, paused: bool) -> None: + self._paused = bool(paused) + + @property + def paused(self) -> bool: + return self._paused + + def stop_record_thread(self) -> None: + self._record_stop.set() + thread = self._record_thread + if thread is not None and thread.is_alive() and thread is not threading.current_thread(): + thread.join(timeout=1.5) + self._record_thread = None + + def sample_from_aq( + self, + aq, + *, + torque: Optional[float] = None, + health_ratio: Optional[float] = None, + lap: Optional[int] = None, + stick_pitch: Optional[float] = None, + stick_roll: Optional[float] = None, + stick_yaw: Optional[float] = None, + stick_throttle: Optional[float] = None, + timestamp_s: Optional[float] = None, + snapshot: Optional[dict] = None, + ) -> None: + if not self._active or self._paused: + return + if snapshot is None: + from models.data_bridge import read_aq_snapshot + + try: + snapshot = read_aq_snapshot(aq, torque=torque, health_ratio=health_ratio) + except OSError: + return + if timestamp_s is not None: + try: + ts = float(timestamp_s) + except (TypeError, ValueError): + ts = time.monotonic() - self._start_mono + else: + ts = time.monotonic() - self._start_mono + sample = sample_from_snapshot( + snapshot, + timestamp_s=ts, + lap=int(lap) if lap is not None else 0, + stick_pitch=stick_pitch, + stick_roll=stick_roll, + stick_yaw=stick_yaw, + stick_throttle=stick_throttle, + ) + if sample is None: + return + with self._lock: + self._samples.append(sample) + + @property + def has_pending_samples(self) -> bool: + with self._lock: + return len(self._samples) > 0 + + def samples_snapshot(self) -> List[TrajectorySample]: + """供 UI 航迹图读取的采样副本(不消费录制缓冲)。""" + with self._lock: + return list(self._samples) + + def stop(self) -> None: + """停止录制线程,保留内存中的采样点供后续保存。""" + self._active = False + self._paused = False + self._aq = None + self.stop_record_thread() + + def save( + self, + total_s: Optional[float] = None, + laps_s: Optional[Sequence[Optional[float]]] = None, + ) -> Optional[Path]: + with self._lock: + samples = list(self._samples) + self._samples = [] + if not samples: + return None + self.output_dir.mkdir(parents=True, exist_ok=True) + stamp = datetime.now().strftime("%Y%m%d_%H%M%S") + duration_suffix = format_duration_suffix(total_s) + path = self.output_dir / f"flight_{stamp}_{duration_suffix}.csv" + save_trajectory_csv( + path, + Trajectory( + aircraft_title=self._aircraft_title, + samples=samples, + recorded_at=datetime.now().isoformat(timespec="seconds"), + laps_s=list(laps_s) if laps_s is not None else [], + ), + ) + self._last_path = path + return path + + def stop_and_save( + self, + total_s: Optional[float] = None, + laps_s: Optional[Sequence[Optional[float]]] = None, + ) -> Optional[Path]: + self.stop() + return self.save(total_s=total_s, laps_s=laps_s) + + +def _format_csv_float(value: float, fmt: str) -> str: + if value is None: + return "" + try: + v = float(value) + except (TypeError, ValueError): + return "" + if v != v: + return "" + return format(v, fmt) + + +def _format_laps_header(laps_s: Sequence[Optional[float]]) -> str: + parts: List[str] = [] + for i in range(4): + if i >= len(laps_s) or laps_s[i] is None: + parts.append("") + continue + try: + v = float(laps_s[i]) # type: ignore[arg-type] + except (TypeError, ValueError): + parts.append("") + continue + parts.append("" if v != v or v <= 0 else f"{v:.3f}") + return ",".join(parts) + + +def _parse_laps_header(raw: str) -> List[Optional[float]]: + parts = [p.strip() for p in raw.split(",")] + out: List[Optional[float]] = [] + for i in range(4): + if i >= len(parts) or not parts[i]: + out.append(None) + continue + try: + v = float(parts[i]) + except ValueError: + out.append(None) + continue + out.append(v if v == v and v > 0 else None) + return out + + +def _optional_csv_float(row: dict, key: str) -> float: + raw = row.get(key) + if raw is None or str(raw).strip() == "": + return float("nan") + return _safe_float(raw, float("nan")) + + +def save_trajectory_csv(path: Path, trajectory: Trajectory) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8", newline="") as fp: + if trajectory.aircraft_title: + fp.write(f"# aircraft_title={trajectory.aircraft_title}\n") + if trajectory.recorded_at: + fp.write(f"# recorded_at={trajectory.recorded_at}\n") + if trajectory.laps_s is not None: + fp.write(f"# laps_s={_format_laps_header(trajectory.laps_s)}\n") + writer = csv.DictWriter(fp, fieldnames=CSV_COLUMNS) + writer.writeheader() + for sample in trajectory.samples: + writer.writerow( + { + "timestamp_s": f"{sample.timestamp_s:.3f}", + "latitude": f"{sample.latitude:.8f}", + "longitude": f"{sample.longitude:.8f}", + "altitude_ft": f"{sample.altitude_ft:.2f}", + "agl_ft": f"{sample.agl_ft:.2f}", + "heading_deg": f"{sample.heading_deg:.2f}", + "airspeed_kts": f"{sample.airspeed_kts:.2f}", + "groundspeed_kts": f"{sample.groundspeed_kts:.2f}", + "pitch_deg": f"{sample.pitch_deg:.2f}", + "bank_deg": f"{sample.bank_deg:.2f}", + "torque": _format_csv_float(sample.torque, ".2f"), + "health_pct": _format_csv_float(sample.health_pct, ".2f"), + "stick_pitch": _format_csv_float(sample.stick_pitch, ".4f"), + "stick_roll": _format_csv_float(sample.stick_roll, ".4f"), + "stick_yaw": _format_csv_float(sample.stick_yaw, ".4f"), + "stick_throttle": _format_csv_float(sample.stick_throttle, ".4f"), + "lap": str(int(sample.lap)) if sample.lap else "", + } + ) + + +def load_trajectory_csv(path: Path) -> Trajectory: + aircraft_title = "" + recorded_at = "" + laps_s: List[Optional[float]] = [] + rows: List[TrajectorySample] = [] + with Path(path).open("r", encoding="utf-8-sig", newline="") as fp: + for raw in fp: + line = raw.strip() + if not line: + continue + if line.startswith("#"): + if line.startswith("# aircraft_title="): + aircraft_title = line.split("=", 1)[1].strip() + elif line.startswith("# recorded_at="): + recorded_at = line.split("=", 1)[1].strip() + elif line.startswith("# laps_s="): + laps_s = _parse_laps_header(line.split("=", 1)[1].strip()) + continue + break + else: + raise ValueError("轨迹文件为空") + reader = csv.DictReader([line] + fp.readlines()) + if not reader.fieldnames: + raise ValueError("轨迹文件缺少表头") + for row in reader: + if not row: + continue + lap_raw = (row.get("lap") or "").strip() + try: + lap = int(float(lap_raw)) if lap_raw else 0 + except ValueError: + lap = 0 + rows.append( + TrajectorySample( + timestamp_s=_safe_float(row.get("timestamp_s")), + latitude=_safe_float(row.get("latitude")), + longitude=_safe_float(row.get("longitude")), + altitude_ft=_safe_float(row.get("altitude_ft")), + agl_ft=_safe_float(row.get("agl_ft")), + heading_deg=_safe_float(row.get("heading_deg")), + airspeed_kts=_safe_float(row.get("airspeed_kts")), + groundspeed_kts=_safe_float(row.get("groundspeed_kts")), + pitch_deg=_safe_float(row.get("pitch_deg")), + bank_deg=_safe_float(row.get("bank_deg")), + torque=_optional_csv_float(row, "torque"), + health_pct=_optional_csv_float(row, "health_pct"), + stick_pitch=_optional_csv_float(row, "stick_pitch"), + stick_roll=_optional_csv_float(row, "stick_roll"), + stick_yaw=_optional_csv_float(row, "stick_yaw"), + stick_throttle=_optional_csv_float(row, "stick_throttle"), + lap=lap, + ) + ) + if len(rows) < 2: + raise ValueError("轨迹点过少,至少需要 2 个采样点") + # 时间轴归一到从 0 开始 + t0 = rows[0].timestamp_s + for sample in rows: + sample.timestamp_s = max(0.0, sample.timestamp_s - t0) + return Trajectory( + aircraft_title=aircraft_title, + samples=rows, + recorded_at=recorded_at, + laps_s=laps_s, + ) + + +def _segment_indices(samples: Sequence[TrajectorySample], t: float) -> Tuple[int, int]: + """返回包围时间 t 的样本下标 (lo, hi)。""" + if t <= samples[0].timestamp_s: + return 0, min(1, len(samples) - 1) + if t >= samples[-1].timestamp_s: + n = len(samples) - 1 + return max(0, n - 1), n + lo = 0 + hi = len(samples) - 1 + while lo + 1 < hi: + mid = (lo + hi) // 2 + if samples[mid].timestamp_s <= t: + lo = mid + else: + hi = mid + return lo, hi + + +def interpolate_sample(samples: Sequence[TrajectorySample], t: float) -> TrajectorySample: + """按轨迹时间做 Catmull-Rom / 平滑插值;超出末尾则停在最后一点。""" + n = len(samples) + if n == 0: + raise ValueError("空轨迹") + if n == 1 or t <= samples[0].timestamp_s: + return samples[0] + if t >= samples[-1].timestamp_s: + return samples[-1] + + lo, hi = _segment_indices(samples, t) + a = samples[lo] + b = samples[hi] + dt = b.timestamp_s - a.timestamp_s + u = 0.0 if dt <= 1e-6 else (t - a.timestamp_s) / dt + u = max(0.0, min(1.0, u)) + u_s = smoothstep(u) + + i0 = max(0, lo - 1) + i3 = min(n - 1, hi + 1) + p0, p1, p2, p3 = samples[i0], samples[lo], samples[hi], samples[i3] + + if hi == lo or abs(p2.timestamp_s - p1.timestamp_s) < 1e-4: + return TrajectorySample( + timestamp_s=t, + latitude=lerp(a.latitude, b.latitude, u_s), + longitude=lerp(a.longitude, b.longitude, u_s), + altitude_ft=lerp(a.altitude_ft, b.altitude_ft, u_s), + agl_ft=lerp(a.agl_ft, b.agl_ft, u_s), + heading_deg=lerp_angle_deg(a.heading_deg, b.heading_deg, u_s), + airspeed_kts=lerp(a.airspeed_kts, b.airspeed_kts, u_s), + groundspeed_kts=lerp(a.groundspeed_kts, b.groundspeed_kts, u_s), + pitch_deg=lerp(a.pitch_deg, b.pitch_deg, u_s), + bank_deg=lerp(a.bank_deg, b.bank_deg, u_s), + ) + + return TrajectorySample( + timestamp_s=t, + latitude=catmull_rom(p0.latitude, p1.latitude, p2.latitude, p3.latitude, u), + longitude=catmull_rom(p0.longitude, p1.longitude, p2.longitude, p3.longitude, u), + altitude_ft=catmull_rom(p0.altitude_ft, p1.altitude_ft, p2.altitude_ft, p3.altitude_ft, u), + agl_ft=max(0.0, catmull_rom(p0.agl_ft, p1.agl_ft, p2.agl_ft, p3.agl_ft, u)), + heading_deg=catmull_rom_angle_deg( + p0.heading_deg, p1.heading_deg, p2.heading_deg, p3.heading_deg, u + ), + airspeed_kts=max( + 0.0, + catmull_rom(p0.airspeed_kts, p1.airspeed_kts, p2.airspeed_kts, p3.airspeed_kts, u), + ), + groundspeed_kts=max( + 0.0, + catmull_rom( + p0.groundspeed_kts, p1.groundspeed_kts, p2.groundspeed_kts, p3.groundspeed_kts, u + ), + ), + pitch_deg=catmull_rom(p0.pitch_deg, p1.pitch_deg, p2.pitch_deg, p3.pitch_deg, u), + bank_deg=catmull_rom(p0.bank_deg, p1.bank_deg, p2.bank_deg, p3.bank_deg, u), + ) + + +def densify_samples( + samples: Sequence[TrajectorySample], + target_hz: float = REPLAY_DENSE_HZ, +) -> List[TrajectorySample]: + """把稀疏轨迹上采样到更高时间密度(默认 60Hz),回放与航点共用。""" + if len(samples) < 2: + return list(samples) + duration = samples[-1].timestamp_s - samples[0].timestamp_s + if duration <= 0: + return list(samples) + step = 1.0 / max(10.0, float(target_hz)) + out: List[TrajectorySample] = [] + t = samples[0].timestamp_s + end = samples[-1].timestamp_s + while t < end - 1e-9: + out.append(interpolate_sample(samples, t)) + t += step + out.append(samples[-1]) + return out + + +def resolve_shadow_start( + trajectory: Trajectory, + aq, + lead_s: float, + use_user_reference: bool, +) -> Tuple[float, TrajectorySample, float]: + """根据 CSV 参考时刻 + 领先偏移,计算影子机起始时刻与初始位姿。 + + 领先 0 秒:始终使用 CSV 第一条记录;领先 >0 时按 CSV 时间轴向前偏移。 + ``use_user_reference=True``(当前任务)时参考时刻为用户在 CSV 上的最近点; + ``False``(重置任务)时参考时刻为 CSV 起点。 + """ + samples = trajectory.samples + t0 = samples[0].timestamp_s + t1 = samples[-1].timestamp_s + lead = max(SHADOW_LEAD_MIN_S, min(SHADOW_LEAD_MAX_S, float(lead_s))) + + if lead <= 1e-6: + return t0, samples[0], 0.0 + + if use_user_reference: + user = read_sample_from_aq(aq) + if user is not None: + t_ref = find_trajectory_reference_time(samples, user.latitude, user.longitude) + else: + t_ref = t0 + else: + t_ref = t0 + + if t1 > t0: + lead = min(lead, max(0.0, t1 - t_ref)) + + start_t = max(t0, min(t1, t_ref + lead)) + spawn = interpolate_sample(samples, start_t) + + return start_t, spawn, lead + + +def build_waypoint_window( + samples: Sequence[TrajectorySample], + t_now: float, + horizon_s: float = WAYPOINT_HORIZON_S, + count: int = WAYPOINT_WINDOW_COUNT, +) -> List[SIMCONNECT_DATA_WAYPOINT]: + """从当前时刻向前取滑动窗口航点(在密化轨迹上按 60Hz 语义采样)。""" + if len(samples) < 2 or count < 1: + return [] + t_end = samples[-1].timestamp_s + horizon = max(0.2, float(horizon_s)) + flags_base = ( + SIMCONNECT_WAYPOINT_FLAGS.SIMCONNECT_WAYPOINT_SPEED_REQUESTED + | SIMCONNECT_WAYPOINT_FLAGS.SIMCONNECT_WAYPOINT_COMPUTE_VERTICAL_SPEED + ) + out: List[SIMCONNECT_DATA_WAYPOINT] = [] + for i in range(1, count + 1): + t = min(t_end, t_now + horizon * (i / float(count))) + sample = interpolate_sample(samples, t) + wp = SIMCONNECT_DATA_WAYPOINT() + wp.Latitude = float(sample.latitude) + wp.Longitude = float(sample.longitude) + wp.Altitude = float(sample.altitude_ft) + flags = int(flags_base) + if not sample_is_airborne(sample): + flags |= int(SIMCONNECT_WAYPOINT_FLAGS.SIMCONNECT_WAYPOINT_ON_GROUND) + wp.Flags = flags + wp.ktsSpeed = float(max(0.0, sample.airspeed_kts or sample.groundspeed_kts)) + wp.percentThrottle = 0.0 + out.append(wp) + return out + + +def find_mission_ai_ids(sm) -> List[int]: + keep = {int(SIMCONNECT_OBJECT_ID_USER.value), 0, 1} + try: + ids = sm.enumerate_aircraft_ids(timeout=3.0) + except Exception: + ids = [] + return [int(oid) for oid in ids if int(oid) not in keep] + + +def resolve_aircraft_title(aq, trajectory: Trajectory) -> str: + if trajectory.aircraft_title: + return trajectory.aircraft_title + title = aq.get(VAR_TITLE) + if title: + text = title.decode() if isinstance(title, bytes) else str(title) + text = text.strip("\x00").strip() + if text: + return text + return DEFAULT_AIRCRAFT_TITLE + + +class ShadowPlaneController: + """位置驱动影子机:60Hz 刷位置;姿态不冻结;滑动航点辅助顺滑。""" + + def __init__(self, sm): + self.sm = sm + self.object_id: Optional[int] = None + self.request_id = None + self.adopted = False + self._replay_stop = threading.Event() + self._replay_thread: Optional[threading.Thread] = None + self._replay_paused = False + self._frozen = False + self._smoke_on = False + self._pose_fail_count = 0 + self._pose_poisoned = False # 已要求停止写入(坠毁/脱离) + self._freeze_attitude = False + + def _sim_alive(self) -> bool: + sm = self.sm + if sm is None: + return False + try: + if getattr(sm, "quit", 0): + return False + if not getattr(sm, "ok", False): + return False + except Exception: + return False + return True + + def stop_replay(self, join_timeout: float = 0.6) -> None: + """停止回放线程;短超时,避免任务重置后 join 卡死。""" + self._replay_stop.set() + self._replay_paused = False + thread = self._replay_thread + self._replay_thread = None + if thread is not None and thread.is_alive() and thread is not threading.current_thread(): + thread.join(timeout=max(0.1, float(join_timeout))) + + def signal_stop(self) -> None: + """仅置位停止(可在 UI 线程调用):不 join、不调用 SimConnect。""" + self._replay_stop.set() + self._replay_paused = False + self._pose_poisoned = True + self.object_id = None + + def set_replay_paused(self, paused: bool) -> None: + """模拟器暂停时冻结轨迹回放进度(保持当前位姿)。""" + self._replay_paused = bool(paused) + + def detach(self) -> None: + """本地脱离;可 join 回放线程,勿在 Tk 主线程长时间调用。""" + self.signal_stop() + self.stop_replay(join_timeout=0.35) + self.adopted = False + self._frozen = False + self._smoke_on = False + self._pose_fail_count = 0 + self.request_id = None + + def remove(self, force_delete: bool = False) -> None: + """停止回放;默认不强制删除可能已失效的 Object,避免 SimConnect 卡死。""" + oid = self.object_id + alive = self._sim_alive() and not self._pose_poisoned + self.signal_stop() + self.stop_replay(join_timeout=0.5) + # 任务重置/坠毁后 Object 常已消失:只做本地脱离,除非明确要求删除且连接正常 + if force_delete and alive and oid is not None and not self.adopted: + try: + done = threading.Event() + + def _do_remove(): + try: + self.sm.remove_sim_object(oid) + except Exception: + pass + finally: + done.set() + + threading.Thread(target=_do_remove, daemon=True).start() + done.wait(timeout=0.8) + except Exception: + pass + self.adopted = False + self._frozen = False + self._smoke_on = False + self._pose_fail_count = 0 + self.request_id = None + + def _set_freeze(self, enabled: bool, freeze_attitude: Optional[bool] = None) -> None: + """冻结经纬/高度以保证位置驱动;姿态默认不冻结,由物理+航点更顺滑。""" + if self.object_id is None or not self._sim_alive() or self._pose_poisoned: + return + if freeze_attitude is None: + freeze_attitude = self._freeze_attitude + value = 1 if enabled else 0 + oid = self.object_id + for evt in ( + b"FREEZE_LATITUDE_LONGITUDE_SET", + b"FREEZE_ALTITUDE_SET", + ): + try: + self.sm.send_event_to_object(oid, evt, value) + except Exception: + pass + att_value = 1 if (enabled and freeze_attitude) else 0 + try: + self.sm.send_event_to_object(oid, b"FREEZE_ATTITUDE_SET", att_value) + except Exception: + pass + self._frozen = enabled + self._freeze_attitude = bool(freeze_attitude) + + def _push_waypoint_window(self, samples: Sequence[TrajectorySample], t_now: float) -> None: + """下发当前时刻的滑动窗口航点(辅助姿态/油门意图,位置仍由刷帧主导)。""" + oid = self.object_id + if oid is None or not self._sim_alive() or self._pose_poisoned: + return + if getattr(self.sm, "waypoint_broken", False): + return + waypoints = build_waypoint_window(samples, t_now) + if not waypoints: + return + try: + self.sm.set_ai_waypoints(oid, waypoints) + except Exception: + pass + + def _set_smoke(self, enabled: bool) -> None: + """开启/关闭拉烟(机型需支持,如 Extra);仅用事件,避免 SetData Bool 触发 INVALID_DATA_SIZE。""" + if self.object_id is None or not self._sim_alive() or self._pose_poisoned: + return + value = 1 if enabled else 0 + oid = self.object_id + try: + self.sm.send_event_to_object(oid, b"SMOKE_SET", value) + except Exception: + pass + try: + self.sm.send_event_to_object( + oid, + b"SMOKE_ON" if enabled else b"SMOKE_OFF", + 0, + ) + except Exception: + pass + self._smoke_on = enabled + + def _apply_pose(self, sample: TrajectorySample) -> bool: + """在回放线程内同步写位姿(避免多线程并发打 SimConnect)。""" + oid = self.object_id + if oid is None or not self._sim_alive() or self._pose_poisoned: + return False + if self._replay_stop.is_set(): + return False + airborne = sample_is_airborne(sample) + speed = int(max(0, round(sample.airspeed_kts or sample.groundspeed_kts))) + try: + ok = bool( + self.sm.set_init_position_on_object( + oid, + sample.latitude, + sample.longitude, + sample.altitude_ft, + hdg=sample.heading_deg, + pitch=sample.pitch_deg, + bank=sample.bank_deg, + gnd=0 if airborne else 1, + speed=speed, + ) + ) + except Exception: + ok = False + if ok: + self._pose_fail_count = 0 + else: + self._pose_fail_count += 1 + return ok + + def adopt(self, object_id: int, sample: TrajectorySample) -> int: + self.stop_replay() + oid = int(object_id) + self.object_id = oid + self.adopted = True + self._set_freeze(True, freeze_attitude=False) + time.sleep(0.05) + self._apply_pose(sample) + return oid + + def spawn( + self, + aircraft_title: str, + sample: TrajectorySample, + timeout: float = SPAWN_TIMEOUT_S, + enable_smoke: bool = True, + ) -> int: + # 新建前只做本地脱离,避免删除已失效 Object卡死 + self.detach() + self._pose_poisoned = False + self._replay_stop = threading.Event() + if not self._sim_alive(): + raise RuntimeError("SimConnect 未连接或已断开") + request_id = self.sm.new_request_id() + assigned = threading.Event() + result = {"object_id": None} + + def _on_assigned(_req_id, object_id): + result["object_id"] = int(object_id) + assigned.set() + + self.sm.register_object_id_waiter(request_id, _on_assigned) + self.request_id = request_id + airborne = sample_is_airborne(sample) + speed = int(max(0, round(sample.airspeed_kts or sample.groundspeed_kts))) + try: + ok = self.sm.create_non_atc_aircraft( + aircraft_title, + sample.latitude, + sample.longitude, + request_id, + tail_number="SHADOW1", + hdg=sample.heading_deg, + gnd=0 if airborne else 1, + alt=sample.altitude_ft, + pitch=sample.pitch_deg, + bank=sample.bank_deg, + speed=speed, + ) + except Exception as exc: + self.sm._object_id_waiters.pop( + int(request_id.value if hasattr(request_id, "value") else request_id), + None, + ) + raise RuntimeError(f"创建影子机失败:{exc}") from exc + if not ok: + self.sm._object_id_waiters.pop( + int(request_id.value if hasattr(request_id, "value") else request_id), + None, + ) + raise RuntimeError(f"无法创建影子机:{aircraft_title}") + if not assigned.wait(timeout): + self.sm._object_id_waiters.pop( + int(request_id.value if hasattr(request_id, "value") else request_id), + None, + ) + raise TimeoutError("创建影子机超时(模拟器可能刚重置,请稍后再试)") + object_id = result["object_id"] + if object_id is None: + raise RuntimeError("未获得影子机 Object ID") + self.object_id = object_id + self.adopted = False + self._pose_fail_count = 0 + # 保留 AI 以便滑动航点辅助;冻结经纬/高度,姿态放开 + time.sleep(0.1) + self._set_freeze(True, freeze_attitude=False) + time.sleep(0.05) + self._apply_pose(sample) + if enable_smoke: + self._set_smoke(True) + return object_id + + def start_replay( + self, + samples: Sequence[TrajectorySample], + rate_hz: float = REPLAY_HZ, + enable_smoke: bool = True, + lead_s: float = SHADOW_LEAD_DEFAULT_S, + start_time_s: Optional[float] = None, + initial_sample: Optional[TrajectorySample] = None, + enable_waypoint_assist: bool = False, + ) -> None: + if self.object_id is None: + raise RuntimeError("影子机尚未就绪") + if len(samples) < 2: + raise ValueError("轨迹点不足") + self.stop_replay(join_timeout=0.4) + self._replay_stop = threading.Event() + samples = densify_samples(samples, REPLAY_DENSE_HZ) + t0_traj = samples[0].timestamp_s + t1_traj = samples[-1].timestamp_s + if start_time_s is not None: + start_t = max(t0_traj, min(t1_traj, float(start_time_s))) + else: + duration = max(0.0, t1_traj - t0_traj) + lead = max(SHADOW_LEAD_MIN_S, min(SHADOW_LEAD_MAX_S, float(lead_s))) + lead = min(lead, duration * 0.9) if duration > 0 else 0.0 + start_t = t0_traj + lead + + def _loop(): + period = 1.0 / max(10.0, float(rate_hz)) + replay_elapsed = 0.0 + last_tick = time.monotonic() + try: + self._set_freeze(True, freeze_attitude=False) + if enable_smoke: + self._set_smoke(True) + last_smoke = time.monotonic() + last_wp = 0.0 + start_sample = initial_sample or interpolate_sample(samples, start_t) + self._apply_pose(start_sample) + if enable_waypoint_assist: + self._push_waypoint_window(samples, start_t) + last_wp = time.monotonic() + while not self._replay_stop.is_set(): + if not self._sim_alive(): + break + now = time.monotonic() + dt = now - last_tick + last_tick = now + if self._replay_paused: + # 暂停时冻结进度且不再刷 SetData,避免 INVALID_DATA_SIZE 刷屏 + time.sleep(period) + continue + replay_elapsed += dt + target_t = start_t + replay_elapsed + if target_t >= t1_traj: + self._apply_pose(samples[-1]) + break + pose = interpolate_sample(samples, target_t) + if not self._apply_pose(pose): + if self._pose_fail_count >= POSE_FAIL_ABORT or self._pose_poisoned: + break + wp_ok = enable_waypoint_assist and not getattr(self.sm, "waypoint_broken", False) + if wp_ok and (now - last_wp) >= WAYPOINT_REFRESH_S: + self._push_waypoint_window(samples, target_t) + last_wp = now + if enable_smoke and (now - last_smoke) >= SMOKE_REFRESH_S: + self._set_smoke(True) + last_smoke = now + sleep_s = period - (time.monotonic() - now) + if sleep_s > 0.0005: + time.sleep(sleep_s) + except Exception: + # 回放线程内吞掉异常,避免未捕获异常导致进程退出 + pass + finally: + self._replay_thread = None + + self._replay_thread = threading.Thread(target=_loop, daemon=True) + self._replay_thread.start() + + +def launch_shadow_follow( + sm, + aq, + csv_path, + situation_reset_event=None, + reset_already_applied: bool = False, + reset_settle_s: float = RESET_SETTLE_S, + replace_mission_ai: bool = False, + lead_s: float = SHADOW_LEAD_DEFAULT_S, + enable_smoke: bool = True, + use_user_reference: bool = False, +) -> dict: + """位置驱动跟飞:可选重置任务 → 新建影子机 → 10Hz CSV 轨迹 60Hz 插值回放。""" + if sm is None or getattr(sm, "quit", 0) or not getattr(sm, "ok", False): + raise RuntimeError("SimConnect 未就绪,请确认模拟器已连接") + + trajectory = load_trajectory_csv(Path(csv_path)) + + if situation_reset_event is not None: + try: + situation_reset_event() + except Exception as exc: + raise RuntimeError(f"任务重置失败:{exc}") from exc + time.sleep(max(reset_settle_s, 2.0)) + elif reset_already_applied: + time.sleep(max(reset_settle_s, 2.0)) + else: + time.sleep(0.3) + + time.sleep(MISSION_AI_SETTLE_S) + + if getattr(sm, "quit", 0) or not getattr(sm, "ok", False): + raise RuntimeError("任务重置后连接异常,请稍后重试跟飞") + + # 再等一拍,避免重置动画未结束就 AICreate + time.sleep(0.5) + + start_t, start, lead = resolve_shadow_start( + trajectory, + aq, + lead_s, + use_user_reference=use_user_reference, + ) + initial_sample = start if lead <= 1e-6 else None + + aircraft_title = resolve_aircraft_title(aq, trajectory) + controller = ShadowPlaneController(sm) + object_id = controller.spawn(aircraft_title, start, enable_smoke=enable_smoke) + time.sleep(0.25) + controller.start_replay( + trajectory.samples, + enable_smoke=enable_smoke, + start_time_s=start_t, + initial_sample=initial_sample, + enable_waypoint_assist=False, + ) + # 跟飞准备阶段模拟器通常处于暂停;先冻结,由主线程 _poll 随暂停状态同步 + controller.set_replay_paused(True) + return { + "controller": controller, + "object_id": object_id, + "aircraft_title": aircraft_title, + "sample_count": len(trajectory.samples), + "duration_s": trajectory.duration_s, + "removed_ai": 0, + "mode": "spawn", + "waypoint_count": 0, + "replay_hz": REPLAY_HZ, + "dense_hz": REPLAY_DENSE_HZ, + "waypoint_assist": False, + "smoke": bool(enable_smoke), + "lead_s": lead, + } diff --git a/models/speed_bands.py b/models/speed_bands.py new file mode 100644 index 00000000..a37f30fb --- /dev/null +++ b/models/speed_bands.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +""" +模块:models.speed_bands +职责:速度带定义与按圈 Δt 累计 +依赖:models.lap_reference.LAP_COUNT +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Optional, Sequence, Tuple + +from models.lap_reference import LAP_COUNT + + +@dataclass(frozen=True) +class SpeedBand: + """半开区间 [lo, hi);hi 为 None 表示不低于 lo。""" + + label: str + lo: float + hi: Optional[float] = None + + def contains(self, speed_kts: float) -> bool: + if speed_kts < self.lo: + return False + if self.hi is None: + return True + return speed_kts < self.hi + + +# IAS/GS kts。比较符用 ≤ ≥ >(勿用半角「<」,会被 Qt Charts 当富文本吃掉)。 +DEFAULT_SPEED_BANDS: Tuple[SpeedBand, ...] = ( + SpeedBand("V≤160", 0.0, 160.0), + SpeedBand("180≥V>160", 160.0, 180.0), + SpeedBand("200≥V>180", 180.0, 200.0), + SpeedBand("220≥V>200", 200.0, 220.0), + SpeedBand("240≥V>220", 220.0, 240.0), + SpeedBand("V>240", 240.0, None), +) + +SPEED_SOURCE_AIRSPEED = "airspeed" +SPEED_SOURCE_GROUNDSPEED = "groundspeed" + + +def band_index(speed_kts: float, bands: Sequence[SpeedBand] = DEFAULT_SPEED_BANDS) -> Optional[int]: + """返回速度所属档位下标;非法值返回 None。""" + try: + v = float(speed_kts) + except (TypeError, ValueError): + return None + if v != v: + return None + for i, band in enumerate(bands): + if band.contains(v): + return i + return None + + +class SpeedBandAccumulator: + """按采样间隔把 Δt 记入「当前圈 × 速度带」;四圈齐全后冻结。""" + + def __init__( + self, + bands: Sequence[SpeedBand] = DEFAULT_SPEED_BANDS, + lap_count: int = LAP_COUNT, + ) -> None: + self.bands: Tuple[SpeedBand, ...] = tuple(bands) + self.lap_count = int(lap_count) + self._dwell: List[List[float]] = [ + [0.0] * len(self.bands) for _ in range(self.lap_count) + ] + self._active_lap: int = 1 # 1..lap_count + self._frozen = False + self._last_t: Optional[float] = None + self._last_band: Optional[int] = None + self._last_lap_idx: Optional[int] = None + + @property + def frozen(self) -> bool: + return self._frozen + + def reset(self) -> None: + self._dwell = [[0.0] * len(self.bands) for _ in range(self.lap_count)] + self._active_lap = 1 + self._frozen = False + self._last_t = None + self._last_band = None + self._last_lap_idx = None + + def set_active_lap(self, lap_number: int) -> None: + """设置正在进行的圈号(1-based)。""" + self._active_lap = int(lap_number) + + def freeze(self) -> None: + self._frozen = True + self._last_t = None + self._last_band = None + self._last_lap_idx = None + + def sample(self, timestamp_s: float, speed_kts: Optional[float]) -> None: + """采样一拍:把与上一拍的 Δt 记入上一拍所在圈×速度带。""" + if self._frozen: + return + idx = band_index(speed_kts, self.bands) if speed_kts is not None else None + # Δt 归属上一拍所在圈(圈切换后到下一采样前的区间仍算上一圈) + if ( + self._last_t is not None + and self._last_band is not None + and self._last_lap_idx is not None + and 0 <= self._last_lap_idx < self.lap_count + ): + dt = float(timestamp_s) - self._last_t + if dt > 0: + self._dwell[self._last_lap_idx][self._last_band] += dt + lap_idx = self._active_lap - 1 + self._last_t = float(timestamp_s) + self._last_band = idx + self._last_lap_idx = lap_idx if 0 <= lap_idx < self.lap_count else None + + def dwell_by_lap(self) -> List[List[float]]: + """[lap][band] 秒数。""" + return [list(row) for row in self._dwell] + + def labels(self) -> List[str]: + return [b.label for b in self.bands] diff --git a/models/track_compare.py b/models/track_compare.py new file mode 100644 index 00000000..61b6f5ce --- /dev/null +++ b/models/track_compare.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +""" +模块:models.track_compare +职责:从轨迹 CSV 构建趋势/圈时对比载荷(一次加载,含 Trajectory) +依赖:models.chart、lap_reference、shadow_plane、data_bridge +""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import List, Optional, Sequence + +from models.chart import ChartHistoryBuffer +from models.data_bridge import load_track_bundle +from models.lap_reference import LAP_COUNT, LapReference +from models.shadow_plane import Trajectory, TrajectorySample, default_trajectory_dir + + +@dataclass +class CompareStyle: + """历史对比层样式:颜色取自原系列并降饱和;线宽/线型/透明度固定。""" + + opacity: float = 0.7 + line_width: float = 1.6 + line_style: str = "dash" # solid | dash | dot | dashdot + visible: bool = True + # 兼容旧调用;折线已改为按字段或 LAP 原色降饱和 + color: str = "#9b59b6" + + +@dataclass +class TrackComparePayload: + """一次对比加载结果(轨迹只读一次)。""" + + path: Path + label: str + trajectory: Trajectory + history: ChartHistoryBuffer + laps: LapReference + + @property + def has_trend(self) -> bool: + return len(self.history) > 0 + + @property + def has_laps(self) -> bool: + return any(t is not None for t in self.laps.as_four()) + + +def load_track_compare(path: Path | str) -> TrackComparePayload: + """加载轨迹文件并组装对比载荷(缺圈时尝试从 sample.lap 推断)。""" + p = Path(path) + traj, history = load_track_bundle(p) + laps = LapReference(laps_s=list(traj.laps_s), label=p.name) + if not any(t is not None for t in laps.as_four()): + # 旧文件无圈时头:尝试从 lap 列推断各圈时长(粗略) + inferred = _infer_laps_from_samples(traj.samples) + if any(t is not None for t in inferred): + laps = LapReference(laps_s=inferred, label=p.name) + return TrackComparePayload( + path=p, + label=p.name, + trajectory=traj, + history=history, + laps=laps, + ) + + +def _infer_laps_from_samples(samples: Sequence[TrajectorySample]) -> List[Optional[float]]: + """按 lap 列切换点估算各圈耗时;无法推断则返回空。""" + if not samples: + return [None] * LAP_COUNT + starts = {1: float(samples[0].timestamp_s)} + last_lap = int(samples[0].lap) if samples[0].lap else 0 + for sample in samples[1:]: + lap = int(sample.lap) if sample.lap else last_lap + if lap != last_lap and lap >= 1: + starts[lap] = float(sample.timestamp_s) + last_lap = lap + ends: dict[int, float] = {} + for sample in samples: + lap = int(sample.lap) if sample.lap else 0 + if lap >= 1: + ends[lap] = float(sample.timestamp_s) + out: List[Optional[float]] = [] + for i in range(1, LAP_COUNT + 1): + if i not in starts or i not in ends: + out.append(None) + continue + dt = ends[i] - starts[i] + out.append(dt if dt > 0 else None) + return out + + +def default_compare_dir() -> Path: + """对比文件默认目录(与轨迹落盘目录一致)。""" + return default_trajectory_dir() diff --git a/models/track_map.py b/models/track_map.py new file mode 100644 index 00000000..35af2a07 --- /dev/null +++ b/models/track_map.py @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*- +""" +模块:models.track_map +职责:航迹点数据结构、经纬投影与连续速度着色(供航迹图 UI) +依赖:models.speed_bands +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass +from typing import List, Optional, Sequence, Tuple + +from models.speed_bands import ( + DEFAULT_SPEED_BANDS, + SPEED_SOURCE_AIRSPEED, + SPEED_SOURCE_GROUNDSPEED, +) + + +@dataclass(frozen=True) +class TrackPoint: + """单点航迹:经纬 + 速度 + 姿态 + 健康 + 摇杆 + 圈号。""" + + latitude: float + longitude: float + airspeed_kts: float + groundspeed_kts: float + lap: int = 0 + timestamp_s: float = 0.0 + pitch_deg: float = float("nan") + bank_deg: float = float("nan") + heading_deg: float = float("nan") + health_pct: float = float("nan") + stick_pitch: float = float("nan") + stick_roll: float = float("nan") + stick_yaw: float = float("nan") + stick_throttle: float = float("nan") + + def speed_kts(self, source: str = SPEED_SOURCE_AIRSPEED) -> float: + if source == SPEED_SOURCE_GROUNDSPEED: + return float(self.groundspeed_kts) + return float(self.airspeed_kts) + + def stick_connected(self) -> bool: + return ( + self.stick_pitch == self.stick_pitch + or self.stick_roll == self.stick_roll + or self.stick_yaw == self.stick_yaw + or self.stick_throttle == self.stick_throttle + ) + + +@dataclass(frozen=True) +class TrackBounds: + min_lon: float + max_lon: float + min_lat: float + max_lat: float + + @property + def lon_range(self) -> float: + return max(1e-9, self.max_lon - self.min_lon) + + @property + def lat_range(self) -> float: + return max(1e-9, self.max_lat - self.min_lat) + + @property + def avg_lat(self) -> float: + return 0.5 * (self.min_lat + self.max_lat) + + def ground_aspect(self) -> float: + """地面真实宽高比(经度按 cos(lat) 修正)。""" + lon_scale = 111.0 * math.cos(math.radians(self.avg_lat)) + lat_scale = 111.0 + return (self.lon_range * lon_scale) / (self.lat_range * lat_scale) + + +def points_from_trajectory_samples(samples: Sequence) -> List[TrackPoint]: + """TrajectorySample 序列 → TrackPoint 列表。""" + out: List[TrackPoint] = [] + for s in samples: + try: + lat = float(getattr(s, "latitude", 0.0)) + lon = float(getattr(s, "longitude", 0.0)) + except (TypeError, ValueError): + continue + if lat != lat or lon != lon: + continue + out.append( + TrackPoint( + latitude=lat, + longitude=lon, + airspeed_kts=_safe_float(getattr(s, "airspeed_kts", 0.0)), + groundspeed_kts=_safe_float(getattr(s, "groundspeed_kts", 0.0)), + lap=int(getattr(s, "lap", 0) or 0), + timestamp_s=_safe_float(getattr(s, "timestamp_s", 0.0)), + pitch_deg=_optional_float(getattr(s, "pitch_deg", None)), + bank_deg=_optional_float(getattr(s, "bank_deg", None)), + heading_deg=_optional_float(getattr(s, "heading_deg", None)), + health_pct=_optional_float(getattr(s, "health_pct", None)), + stick_pitch=_optional_float(getattr(s, "stick_pitch", None)), + stick_roll=_optional_float(getattr(s, "stick_roll", None)), + stick_yaw=_optional_float(getattr(s, "stick_yaw", None)), + stick_throttle=_optional_float(getattr(s, "stick_throttle", None)), + ) + ) + return out + + +def filter_by_visible_laps( + points: Sequence[TrackPoint], + visible_laps: Sequence[bool], +) -> List[TrackPoint]: + """按 LAP1..N 勾选过滤;无圈号点在任一勾选时保留。""" + if not points: + return [] + n = len(visible_laps) + if n <= 0 or all(visible_laps): + return list(points) + allowed = {i + 1 for i, on in enumerate(visible_laps) if on} + if not allowed: + return [] + out: List[TrackPoint] = [] + for p in points: + lap = int(p.lap) + if lap <= 0 or lap in allowed: + out.append(p) + return out + + +def compute_bounds(points: Sequence[TrackPoint]) -> Optional[TrackBounds]: + if not points: + return None + min_lon = min(p.longitude for p in points) + max_lon = max(p.longitude for p in points) + min_lat = min(p.latitude for p in points) + max_lat = max(p.latitude for p in points) + if max_lon - min_lon < 1e-9: + max_lon = min_lon + 1e-4 + if max_lat - min_lat < 1e-9: + max_lat = min_lat + 1e-4 + return TrackBounds(min_lon, max_lon, min_lat, max_lat) + + +# 色标下限(与航迹图.html 一致:低于此速仍按最低色绘制) +COLOR_SPEED_FLOOR_KTS = 120.0 + + +def speed_range( + points: Sequence[TrackPoint], + source: str = SPEED_SOURCE_AIRSPEED, +) -> Optional[Tuple[float, float]]: + """全量样本速度范围,供统一色标;无有效样本返回 None。 + + 色标下限固定不低于 COLOR_SPEED_FLOOR_KTS(默认 120 kt)。 + """ + if not points: + return None + speeds = [p.speed_kts(source) for p in points] + if not speeds: + return None + lo = float(min(speeds)) + hi = float(max(speeds)) + if hi < lo: + lo, hi = hi, lo + lo = max(COLOR_SPEED_FLOOR_KTS, lo) + if hi < lo: + hi = lo + # 单点或几乎等速:略扩上限,避免图例显示成「N–N」 + if hi - lo < 1e-3: + pad = max(10.0, abs(lo) * 0.05) + return lo, hi + pad + return lo, hi + + +def speed_tick_marks(speed_min: float, speed_max: float) -> List[float]: + """色带上的速度分隔刻度:两端 + 落在范围内的速度带边界。""" + ticks = {float(speed_min), float(speed_max)} + for band in DEFAULT_SPEED_BANDS: + for edge in (band.lo, band.hi): + if edge is None: + continue + v = float(edge) + if speed_min < v < speed_max: + ticks.add(v) + return sorted(ticks) + + +def speed_to_rgb( + speed: float, + speed_min: float, + speed_max: float, +) -> Tuple[int, int, int]: + """红(慢) → 黄 → 绿 → 青(快);按给定总范围归一化。""" + span = max(1e-6, speed_max - speed_min) + t = max(0.0, min(1.0, (float(speed) - speed_min) / span)) + if t < 0.33: + u = t / 0.33 + r, g, b = 1.0, u, 0.0 + elif t < 0.66: + u = (t - 0.33) / 0.33 + r, g, b = 1.0 - u, 1.0, 0.0 + else: + u = (t - 0.66) / 0.34 + r, g, b = 0.0, 1.0, u + return int(round(r * 255)), int(round(g * 255)), int(round(b * 255)) + + +def project_to_pixel( + lon: float, + lat: float, + bounds: TrackBounds, + draw_left: float, + draw_top: float, + draw_w: float, + draw_h: float, +) -> Tuple[float, float]: + """经纬 → 像素(北上、西左)。""" + x = draw_left + ((lon - bounds.min_lon) / bounds.lon_range) * draw_w + y = draw_top + ((bounds.max_lat - lat) / bounds.lat_range) * draw_h + return x, y + + +def fit_draw_rect( + bounds: TrackBounds, + avail_w: float, + avail_h: float, + margin: float, +) -> Tuple[float, float, float, float]: + """在可用区域内按地面比例放置绘图矩形,返回 (left, top, w, h)。""" + inner_w = max(1.0, avail_w - 2.0 * margin) + inner_h = max(1.0, avail_h - 2.0 * margin) + aspect = max(0.05, min(20.0, bounds.ground_aspect())) + if aspect >= inner_w / inner_h: + w = inner_w + h = w / aspect + else: + h = inner_h + w = h * aspect + left = margin + 0.5 * (inner_w - w) + top = margin + 0.5 * (inner_h - h) + return left, top, w, h + + +def _safe_float(value) -> float: + try: + v = float(value) + except (TypeError, ValueError): + return 0.0 + return 0.0 if v != v else v + + +def _optional_float(value) -> float: + if value is None: + return float("nan") + try: + v = float(value) + except (TypeError, ValueError): + return float("nan") + return v diff --git a/requirements.txt b/requirements.txt index d38605d3..003e1344 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,10 @@ --i https://pypi.org/simple - +-i https://mirrors.aliyun.com/pypi/simple/ +pymysql>=1.0,<2 +passlib>=1.7,<2 +# sim_connect:Python 3.10 win32;勿装 numpy 2.5(仅 cp314/amd64) +numpy>=1.21,<1.25 +pandas>=2.0,<2.1 +# SimConnect.dll 为 32 位;需 32 位 Python,Qt 使用同架构最高兼容版本 +PySide2==5.15.2.1; platform_machine == "x86" +# 手柄枚举/读轴(SDL);sim_connect 为 win32 cp310 +pygame>=2.1,<2.6 diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index 03a64296..00000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,15 +0,0 @@ --i https://pypi.org/simple -atomicwrites==1.4.0; sys_platform == 'win32' -attrs==20.1.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -colorama==0.4.3; sys_platform == 'win32' -coverage==5.2.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4' -iniconfig==1.0.1 -more-itertools==8.5.0; python_version >= '3.5' -packaging==20.4; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -pluggy==0.13.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -py==1.9.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -pyparsing==2.4.7; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2' -pytest-cov==2.10.1 -pytest==6.0.1; python_version >= '3.5' -six==1.15.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2' -toml==0.10.1 diff --git a/resources/fonts/NotoSansSC-Bold.ttf b/resources/fonts/NotoSansSC-Bold.ttf new file mode 100644 index 00000000..1decf2bf Binary files /dev/null and b/resources/fonts/NotoSansSC-Bold.ttf differ diff --git a/resources/fonts/NotoSansSC-Regular.ttf b/resources/fonts/NotoSansSC-Regular.ttf new file mode 100644 index 00000000..92714869 Binary files /dev/null and b/resources/fonts/NotoSansSC-Regular.ttf differ diff --git a/resources/fonts/Orbitron-Bold.ttf b/resources/fonts/Orbitron-Bold.ttf new file mode 100644 index 00000000..374f46f8 Binary files /dev/null and b/resources/fonts/Orbitron-Bold.ttf differ diff --git a/resources/fonts/Orbitron-Medium.ttf b/resources/fonts/Orbitron-Medium.ttf new file mode 100644 index 00000000..e84885a2 Binary files /dev/null and b/resources/fonts/Orbitron-Medium.ttf differ diff --git a/resources/fonts/README.txt b/resources/fonts/README.txt new file mode 100644 index 00000000..30475e1b --- /dev/null +++ b/resources/fonts/README.txt @@ -0,0 +1,13 @@ +Fonts bundled with TALENT ACADEMY flight training assistant. + +1) Noto Sans SC (Regular / Bold) + Copyright 2015+ Google Inc. / Adobe + License: SIL Open Font License 1.1 + Source: https://fonts.google.com/noto/specimen/Noto+Sans+SC + +2) Orbitron (Medium / Bold) + Copyright 2018 The Orbitron Project Authors + License: SIL Open Font License 1.1 + Source: https://fonts.google.com/specimen/Orbitron + +These files are redistributed under the OFL so the app UI does not depend on OS-installed fonts. diff --git a/resources/icons/combo_down_arrow.png b/resources/icons/combo_down_arrow.png new file mode 100644 index 00000000..49f44435 Binary files /dev/null and b/resources/icons/combo_down_arrow.png differ diff --git a/resources/icons/combo_down_arrow.svg b/resources/icons/combo_down_arrow.svg new file mode 100644 index 00000000..eb3a4294 --- /dev/null +++ b/resources/icons/combo_down_arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 00000000..44a25439 --- /dev/null +++ b/services/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +""" +模块:services +职责:模拟器连接、鉴权等无 UI 服务 +""" diff --git a/services/auth.py b/services/auth.py new file mode 100644 index 00000000..cc12cff9 --- /dev/null +++ b/services/auth.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +""" +模块:services.auth +职责:外网/鉴权服务器探测与 MySQL premium 登录校验 +依赖:passlib、pymysql(运行时按需导入) +""" + +import socket +import subprocess +import sys +import urllib.parse + +CHECK_TIMEOUT_S = 3.0 +AUTH_LOGIN_TIMEOUT_S = 8.0 +CHECK_NETWORK_URL = "https://baidu.com" +AUTH_SERVER_URL = "http://quizbase.cn:3306" +AUTH_DB_USER = "flight_assistant" +AUTH_DB_PASSWORD = "1q2w3e4r" +AUTH_DB_NAME = "talent_auth_info_schema" +AUTH_USER_TABLE = "aero_quiz_base_user_info" + + +def _tcp_reachable(host, port, timeout=CHECK_TIMEOUT_S): + try: + with socket.create_connection((host, int(port)), timeout=timeout): + return True + except (OSError, ValueError, TypeError): + return False + + +def check_network_online(timeout=CHECK_TIMEOUT_S): + """curl -I 探测外网连通性。""" + sec = max(1, int(timeout)) + cmd = [ + "curl", + "-I", + "-sS", + "--connect-timeout", + str(sec), + "--max-time", + str(sec), + CHECK_NETWORK_URL, + ] + flags = getattr(subprocess, "CREATE_NO_WINDOW", 0) if sys.platform == "win32" else 0 + try: + result = subprocess.run( + cmd, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + timeout=timeout + 1, + creationflags=flags, + ) + return result.returncode == 0 + except (OSError, subprocess.TimeoutExpired): + return False + + +def parse_auth_server(url=AUTH_SERVER_URL): + """解析鉴权 URL 为 (host, port)。""" + parsed = urllib.parse.urlparse(url) + host = parsed.hostname or url + if parsed.port: + port = parsed.port + elif parsed.scheme == "http": + port = 80 + else: + port = 443 + return host, int(port) + + +def check_auth_server_online(url=AUTH_SERVER_URL, timeout=CHECK_TIMEOUT_S): + """TCP 探测鉴权服务器是否可达。""" + host, port = parse_auth_server(url) + return _tcp_reachable(host, port, timeout) + + +class AuthFailed(Exception): + """用户名不存在、非 premium 或密码不匹配。""" + + +def _pwd_context(): + try: + from passlib.context import CryptContext + except ImportError as exc: + raise RuntimeError("缺少 passlib,请先安装") from exc + return CryptContext( + schemes=["pbkdf2_sha256"], + default="pbkdf2_sha256", + pbkdf2_sha256__rounds=200_000, + deprecated="auto", + ) + + +_PWD_CONTEXT = None + + +def _get_pwd_context(): + global _PWD_CONTEXT + if _PWD_CONTEXT is None: + _PWD_CONTEXT = _pwd_context() + return _PWD_CONTEXT + + +def _coerce_stored_hash(value): + if value is None: + return "" + if isinstance(value, (bytes, bytearray, memoryview)): + return bytes(value).decode("utf-8", errors="ignore").strip() + return str(value).strip() + + +def verify_password(plain_password, hashed_password): + """PBKDF2-SHA256 校验明文密码与库内哈希。""" + stored = _coerce_stored_hash(hashed_password) + if not stored: + return False + try: + return bool(_get_pwd_context().verify(plain_password, stored)) + except Exception: + return False + + +def _open_auth_db(timeout=AUTH_LOGIN_TIMEOUT_S): + try: + import pymysql + except ImportError as exc: + raise RuntimeError("缺少 pymysql,请先安装") from exc + host, port = parse_auth_server() + sec = max(1, int(timeout)) + kwargs = { + "host": host, + "port": port, + "user": AUTH_DB_USER, + "password": AUTH_DB_PASSWORD, + "database": AUTH_DB_NAME, + "connect_timeout": sec, + "read_timeout": sec, + "write_timeout": sec, + "charset": "utf8mb4", + "autocommit": True, + } + try: + return pymysql.connect(**kwargs, ssl_disabled=True) + except TypeError: + return pymysql.connect(**kwargs) + + +def try_auth_login(username, password, timeout=AUTH_LOGIN_TIMEOUT_S): + """仅在 premium=yes 的用户中核用户名/密码(PBKDF2-SHA256)。""" + conn = _open_auth_db(timeout) + try: + with conn.cursor() as cur: + cur.execute( + f"SELECT password_hash FROM `{AUTH_DB_NAME}`.`{AUTH_USER_TABLE}` " + "WHERE user_name = %s AND premium = %s LIMIT 1", + (username.strip(), "yes"), + ) + row = cur.fetchone() + if not row or not verify_password(password, row[0]): + raise AuthFailed("用户名或密码错误") + finally: + conn.close() diff --git a/services/simulator.py b/services/simulator.py new file mode 100644 index 00000000..9e04551f --- /dev/null +++ b/services/simulator.py @@ -0,0 +1,292 @@ +# -*- coding: utf-8 -*- +""" +模块:services.simulator +职责:SimConnect 连接、仿真变量读写、.flt 解析与单位换算 +依赖:vendored SimConnect/ +""" + +import sys +from pathlib import Path +from typing import Optional, Tuple + +from SimConnect import AircraftRequests, Event, SimConnect + +# --------------------------------------------------------------------------- +# 仿真变量名与运行参数 +# --------------------------------------------------------------------------- + +CHT_VAR = "RECIP_ENG_CYLINDER_HEAD_TEMPERATURE:1" +TORQUE_VAR = "ENG_TORQUE:1" +DAMAGE_VAR = "GENERAL_ENG_DAMAGE_PERCENT:1" +AIRSPEED_VAR = "AIRSPEED_INDICATED" +GROUND_SPEED_VAR = "GROUND_VELOCITY" +PITCH_VAR = "PLANE_PITCH_DEGREES" +BANK_VAR = "PLANE_BANK_DEGREES" +SIM_RATE_VAR = "SIMULATION_RATE" +ABSOLUTE_TIME_VAR = "ABSOLUTE_TIME" +PLANE_LAT_VAR = "PLANE_LATITUDE" +PLANE_LON_VAR = "PLANE_LONGITUDE" +PLANE_ALT_VAR = "PLANE_ALTITUDE" +PLANE_AGL_VAR = "PLANE_ALT_ABOVE_GROUND" +PLANE_HEADING_VAR = "PLANE_HEADING_DEGREES_TRUE" +GROUND_ALT_VAR = "GROUND_ALTITUDE" +TITLE_VAR = "TITLE" +SIM_CLOCK_MIN_DELTA_S = 0.001 +RAD_TO_DEG = 180.0 / 3.141592653589793 +DEG_TO_RAD = 3.141592653589793 / 180.0 +METERS_TO_FEET = 3.280839895 + +TARGET_CHT_F = 52.0 +TARGET_SIM_RATE = 2.0 +SIM_RATE_STEP_MS = 120 +SIM_RATE_MAX_STEPS = 10 +DATA_POLL_HZ = 10 +DATA_POLL_MS = 1000 // DATA_POLL_HZ +MOTION_LAT_LON_EPS_DEG = 1e-5 +CONNECT_TIMEOUT_S = 5.0 +CHECK_ITEM_MIN_S = 0.6 +COLD_CABIN_DELAY_MS = 400 +HEALTH_FLASH_MS = 400 + + +def c_to_f(celsius): + """摄氏度转华氏度。""" + return celsius * 9 / 5 + 32 + + +def f_to_c(fahrenheit): + """华氏度转摄氏度(写入 CHT 前使用)。""" + return (fahrenheit - 32) * 5 / 9 + + +def optional_float(value, default=None): + """安全转 float;失败返回 default。""" + if value is None: + return default + try: + return float(value) + except (TypeError, ValueError): + return default + + +def rad_to_deg(value, default=None): + """SimConnect 姿态角(弧度)→ 度。""" + v = optional_float(value) + if v is None: + return default + return v * RAD_TO_DEG + + +def is_plane_moved( + aq, + baseline_lat: Optional[float], + baseline_lon: Optional[float], + eps_deg: float = MOTION_LAT_LON_EPS_DEG, +) -> Tuple[bool, Optional[float], Optional[float]]: + """判断飞机是否相对基准经纬发生位移。 + + 返回 ``(是否移动, 基准纬度, 基准经度)``。尚无基准时用当前读数初始化并返回 ``(False, lat, lon)``。 + """ + if aq is None: + return False, baseline_lat, baseline_lon + lat = aq.get(PLANE_LAT_VAR) + lon = aq.get(PLANE_LON_VAR) + if lat is None or lon is None: + return False, baseline_lat, baseline_lon + lat = float(lat) + lon = float(lon) + if baseline_lat is None or baseline_lon is None: + return False, lat, lon + moved = ( + abs(lat - baseline_lat) >= eps_deg + or abs(lon - baseline_lon) >= eps_deg + ) + return moved, baseline_lat, baseline_lon + + +def read_simulation_rate(aq) -> Optional[float]: + """读取 SIMULATION_RATE;失败返回 None。""" + if aq is None: + return None + value = aq.get(SIM_RATE_VAR) + if value is None: + return None + try: + return float(value) + except (TypeError, ValueError): + return None + + +def read_simulation_time_s(aq) -> Optional[float]: + """读取模拟器 ABSOLUTE TIME(秒);失败返回 None。""" + if aq is None: + return None + current = aq.get(ABSOLUTE_TIME_VAR) + if current is None: + return None + try: + return float(current) + except (TypeError, ValueError): + return None + + +def chart_elapsed_time_s( + aq, + origin: Optional[float], +) -> Tuple[Optional[float], Optional[float]]: + """基于 ABSOLUTE TIME 计算相对任务起点的时间(模拟暂停时不推进)。 + + 返回 ``(elapsed_s, origin)``;读数失败时 ``elapsed_s`` 为 None。 + """ + current = read_simulation_time_s(aq) + if current is None: + return None, origin + if origin is None: + origin = current + return max(0.0, current - origin), origin + + +def simulation_clock_advancing( + aq, + last_clock: Optional[float], + min_delta_s: float = SIM_CLOCK_MIN_DELTA_S, +) -> Tuple[bool, Optional[float]]: + """根据 ABSOLUTE TIME 是否推进判断模拟是否在运行。 + + 返回 ``(是否推进, 本次读数)``;首次读数仅建立基准并返回 ``(False, t)``。 + """ + if aq is None: + return False, last_clock + current = aq.get(ABSOLUTE_TIME_VAR) + if current is None: + return False, last_clock + try: + now = float(current) + except (TypeError, ValueError): + return False, last_clock + if last_clock is None: + return False, now + return (now - last_clock) > min_delta_s, now + + +def resource_root() -> Path: + """打包运行返回 _MEIPASS,否则返回项目根目录。""" + if getattr(sys, "frozen", False): + return Path(sys._MEIPASS) + return Path(__file__).resolve().parent.parent + + +def get_simconnect_dll_path(): + """返回 SimConnect.dll 的绝对路径。""" + return str(resource_root() / "SimConnect" / "SimConnect.dll") + + +def safe_disconnect_simulator(sm): + """安全断开 SimConnect,忽略清理过程中的异常。""" + if sm is None: + return + try: + sm.exit() + except Exception: + pass + + +def try_connect_simulator(timeout=CONNECT_TIMEOUT_S): + """尝试连接模拟器并完成握手验证;失败时释放资源并抛出异常。""" + sm = None + try: + sm = SimConnect(library_path=get_simconnect_dll_path(), auto_connect=False) + sm.connect(timeout=timeout) + if not sm.ok: + raise ConnectionError("SimConnect 握手未完成") + aq = AircraftRequests(sm, _time=0) + sim_rate_incr = Event(b"SIM_RATE_INCR", sm) + sim_rate_decr = Event(b"SIM_RATE_DECR", sm) + return sm, aq, sim_rate_incr, sim_rate_decr + except Exception: + safe_disconnect_simulator(sm) + raise + + +def parse_flt_main_fields(flt_path: str) -> dict: + """解析 .flt 文件 [Main] 段键值。""" + try: + path = Path(flt_path) + if not path.is_file(): + return {} + section = "" + main_fields = {} + for raw in path.read_text(encoding="utf-8", errors="ignore").splitlines(): + line = raw.strip() + if not line or line.startswith(";"): + continue + if line.startswith("[") and line.endswith("]"): + section = line[1:-1].strip() + continue + if section != "Main" or "=" not in line: + continue + key, value = line.split("=", 1) + main_fields[key.strip()] = value.strip() + return main_fields + except Exception: + return {} + + +def _resolve_flt_path(path_text: str, base: Optional[Path] = None) -> Path: + candidate = Path(path_text) + if candidate.is_file(): + return candidate + if base is not None: + relative = base.parent / path_text + if relative.is_file(): + return relative + return candidate + + +def _is_placeholder_flt_title(title: str) -> bool: + lower = title.lower() + return any( + marker in lower + for marker in ("previous flight", "vorheriger flug", "上次", "previous flug") + ) + + +def parse_flt_mission_type(flt_path: str) -> str: + """从 .flt 文件 [Main] 段读取 MissionType,默认 FreeFlight。""" + main_fields = parse_flt_main_fields(flt_path) + if not main_fields: + return "FreeFlight" + for key in ("MissionType", "FlightType"): + text = main_fields.get(key, "") + if text: + return text + lower = str(flt_path).lower() + if "missions" in lower or "mission" in Path(flt_path).stem.lower(): + return "Mission" + return "FreeFlight" + + +def parse_mission_title(flt_path: str, _depth: int = 0) -> str: + """从 .flt 的 [Main].Title 读取任务菜单标题;必要时沿 OriginalFlight 追溯。""" + if _depth > 3: + return "" + main_fields = parse_flt_main_fields(flt_path) + if not main_fields: + return "" + mission_type = (main_fields.get("MissionType") or main_fields.get("FlightType") or "FreeFlight").strip() + title = main_fields.get("Title", "").strip() + if mission_type.lower() != "freeflight": + if title and not _is_placeholder_flt_title(title): + return title + original = main_fields.get("OriginalFlight", "").strip() + if original: + resolved = _resolve_flt_path(original, Path(flt_path)) + if resolved.is_file(): + nested = parse_mission_title(str(resolved), _depth + 1) + if nested: + return nested + lower = str(flt_path).lower() + if title and not _is_placeholder_flt_title(title): + if "missions" in lower or "mission" in Path(flt_path).stem.lower(): + return title + return "" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 213897f2..00000000 --- a/setup.cfg +++ /dev/null @@ -1,49 +0,0 @@ -[metadata] -name = SimConnect -version = attr: SimConnect.__version__ -description = Adds a pythonic wrapper for SimConnect SDK. -long_description = file: README.md -long_description_content_type = text/markdown -url = https://github.com/odwdinc/Python-SimConnect -author = Anthony Pray -author_email = anthony.pray@gmail.com -maintainer = Anthony Pray -maintainer_email = anthony.pray@gmail.com -keywords = ctypes, FlightSim, SimConnect, Flight, Simulator -license = AGPL 3.0 -classifiers = - Development Status :: 2 - Pre-Alpha - Environment :: Console - Intended Audience :: Developers - License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+) - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: Implementation :: CPython - - -[options] -packages = SimConnect -python_requires = >=3.6 -include_package_data=True -test_suite="tests" - -[flake8] -exclude = .venv,.tox,dist,docs,build,*.egg,env,venv,.undodir - -[bdist_wheel] -universal = 1 - -[build-system] -requires = ["setuptools >= 40.6.0", "wheel"] -build-backend = "setuptools.build_meta" - -[tool:pytest] -minversion = 6.0 -testpaths = - tests -log_level = DEBUG -log_cli_level = DEBUG \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 17ebdb49..00000000 --- a/setup.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python -from setuptools import setup -import distutils.sysconfig - -with open("requirements.txt") as f: - install_requires = f.read()[1:].splitlines()[1:] - -with open("requirements_dev.txt") as f: - tests_require = f.read().splitlines()[1:] - -setup( - install_requires=install_requires, - tests_require=tests_require, - package_data={"": ["*.dll"]} -) \ No newline at end of file diff --git a/static/css/attind_style.css b/static/css/attind_style.css deleted file mode 100644 index 81b89e62..00000000 --- a/static/css/attind_style.css +++ /dev/null @@ -1,332 +0,0 @@ -.casing { - background: #111 url("../img/metal.jpg") repeat; - border: 1px solid #aaa; - border-radius: 10px; - box-shadow: 0 0 10px #000; - padding: 20px; - height: 334px; - width: 334px; -} - -.inner-case { - background: #222; - border: 2px solid #333; - border-radius: 165px; - box-shadow: 0 0 10px #000; - height: 300px; - overflow: hidden; - padding: 15px; -} - -.case-control { - box-shadow: 0px 2px 0px #333; - border-bottom: 1px solid #555; - height: 230px; - overflow: hidden; -} - -/* panel */ -.panel { - box-shadow: 0 0 10px #111; - border-radius: 30px 30px 300px 300px; - background: #333; - color: #aaa; - font-family: Arial; - margin: 13px 0 0 55px; - width: 190px; - height: 40px; -} -.panel ul { - padding: 0; -} -.panel li { - list-style-type: none; - float: left; -} - -/* labels */ -.labels li { - margin: 3px 8px; -} -.labels li:nth-child(3) { - margin-left: 25px; -} - -/* lights */ -.light { - background: #555; - border-radius: 10px; - height: 10px; - margin: 0 23px; - width: 10px; -} -.light:nth-child(3) { - margin-left: 42px; -} -.light.on { - box-shadow: 0 0 8px #3f3; - background: #6f6; -} -.light.off { - box-shadow: 0 0 8px #f33; - background: #f66; -} - -.weight { - position: absolute; - height: 300px; - width: 300px; - z-index: 3; -} - -.aircraft div { - background: #e98219; - border-radius: 5px; - box-shadow: 0 0 6px #000; - position: absolute; - top: 150px; - height: 5px; -} - -/* up-chevron */ -.up-chevron { - position: relative; - top: 50px; - left: 140px; -} -.up-chevron div { - background: #e98219; - border-radius: 5px; - box-shadow: 0 0 6px #000; - position: absolute; - width: 5px; -} -.up-chevron div:nth-child(1), -.up-chevron div:nth-child(2) { - height: 35px; -} -.up-chevron div:nth-child(3) { - height: 25px; -} -.up-chevron div:nth-child(1) { - box-shadow: 2px 0px 2px #555; - transform: rotate(-20deg); - top: 0; - left: 11px; -} -.up-chevron div:nth-child(2) { - box-shadow: -2px 0px 2px #555; - transform: rotate(20deg); - top: 0; - left: 0; -} -.up-chevron div:nth-child(3) { - box-shadow: 2px 0px 2px #555; - transform: rotate(90deg); - top: 19px; - left: 5px; -} - -.left { - left: 80px; - width: 50px; -} - -.centre { - left: 150px; - width: 5px; -} - -.right { - left: 175px; - width: 50px; -} - -.sky { - background: #558ebb; -} - -.terrain { - background: #503723; -} - -.mechanism { - transform: rotate(0deg); /* dynamic */ -} - -/* back */ -.back.sky { - border-radius: 150px 150px 0 0; - height: 150px; - width: 300px; -} -.back.terrain { - border-radius: 0 0 150px 150px; - height: 150px; - width: 300px; -} - -.roll { - border-top: 50px solid #558ebb; - border-right: 50px solid #558ebb; - border-bottom: 50px solid #503723; - border-left: 50px solid #503723; - border-radius: 200px; - box-shadow: inset 0 0 20px #000; - transform: rotate(-45deg); - position: absolute; - top: 0; - left: 0; - height: 200px; - width: 200px; - z-index: 2; -} - -/* roll-lines */ -.roll-lines { - transform: rotate(45deg); - position: absolute; -} -.roll-lines div { - background: #fff; - position: absolute; - height: 4px; - width: 40px; -} - -#ninety-left, -#ninety-right { - width: 50px; - top: -1px; -} -#ninety-left { - left: -9px; -} -#ninety-right { - left: 242px; -} - -#zero { - background: none; - border-left: 15px solid transparent; - border-right: 15px solid transparent; - border-top: 40px solid #fff; - top: -140px; - left: 126px; - height: 0; - width: 0; -} - -#fortyfive-left, -#fortyfive-right { - background: none; - border-left: 5px solid transparent; - border-right: 5px solid transparent; - border-top: 15px solid #fff; - top: -81px; - left: 57px; - height: 0; - width: 0; -} -#fortyfive-left { - transform: rotate(-45deg); -} -#fortyfive-right { - transform: rotate(45deg); - left: 215px; -} -#thirty-left, -#thirty-right { - top: -60px; -} -#thirty-left { - transform: rotate(30deg); - left: 16px; -} -#thirty-right { - transform: rotate(-30deg); - left: 227px; -} -#sixty-left, -#sixty-right { - top: -105px; -} -#sixty-left { - transform: rotate(60deg); - left: 60px; -} -#sixty-right { - transform: rotate(-60deg); - left: 183px; -} - -.ball { - position: absolute; - border-radius: 75px; - box-shadow: 0 0 10px #000; - left: 50px; - top: 75px; /* dynamic */ - z-index: 1; -} - -/* pitch-lines */ -.pitch-lines { - position: absolute; - padding: 8px 70px; -} -.pitch-lines div { - background: #fff; - margin-top: 12px; - height: 2px; - width: 25px; -} -.pitch-lines .small { - margin-left: 18px; -} - -#ten, -#minus-ten { - margin-left: 10px; - width: 40px; -} -#twenty, -#minus-twenty { - width: 60px; -} -#minus-five { - margin-top: 24px; -} - -/* ball */ -.ball .sky { - width: 200px; - height: 75px; - background-image: linear-gradient(top, #558ebb, #9ccbe5); - border-radius: 100px 100px 0 0; - border-bottom: 2px solid #fff; -} -.ball .terrain { - width: 200px; - height: 75px; - background-image: linear-gradient(top, #553a29, #3d2618); - border-radius: 0 0 100px 100px; -} - -.cage-toggle { - background: #222; - border: 2px solid #333; - border-radius: 40px; - box-shadow: 0 0 10px #000; - color: #aaa; - font-family: Arial; - font-size: 10px; - height: 40px; - line-height: 13px; - left: 315px; - padding: 5px; - position: absolute; - top: 315px; - text-align: center; - transform: rotate(-34deg); - width: 40px; -} diff --git a/static/img/metal.jpg b/static/img/metal.jpg deleted file mode 100644 index 9658e28e..00000000 Binary files a/static/img/metal.jpg and /dev/null differ diff --git a/static/img/plane.png b/static/img/plane.png deleted file mode 100644 index e67f2e5b..00000000 Binary files a/static/img/plane.png and /dev/null differ diff --git a/static/js/attind_script.js b/static/js/attind_script.js deleted file mode 100644 index 38da78f2..00000000 --- a/static/js/attind_script.js +++ /dev/null @@ -1,62 +0,0 @@ -// Cache DOM elements -const standby = document.querySelector(".standby"); -const power = document.querySelector(".power"); -const test = document.querySelector(".test"); - -const mechanism = document.querySelector(".mechanism"); -const ball = document.querySelector(".ball"); -let roll; -let pitch; - -standby.classList.remove("off"); -power.classList.add("on"); - -function handleOrientation(event) { - var doc = document, - x = Math.round(event.beta), - z = Math.round(event.gamma); - - var roll = -1*z; - var pitch = 75+x; - - mechanism.style.transform = "rotate(" + roll + "deg)"; - ball.style.top = pitch + "px"; -} - -var temp = false; - -function getSimulatorData(){ - $.getJSON($SCRIPT_ROOT + '/datapoint/PLANE_BANK_DEGREES/get', {}, function(data) { - var z = Math.round(data * (180/Math.PI)); - if ((z > 100) || (z < -100)){ - return; - } - roll = z; - mechanism.style.transform = "rotate(" + roll + "deg)"; - - }); - $.getJSON($SCRIPT_ROOT + '/datapoint/PLANE_PITCH_DEGREES/get', {}, function(data) { - var x = Math.round(data * (180/Math.PI)); - if ((x > 10) || (x < -10)){ - return; - } - pitch = 75+(-5*x); - ball.style.top = pitch + "px"; - }); -} - -function displayData(){ - if (temp){ - temp = false; - //test.classList.remove("on"); - }else{ - temp = true; - //test.classList.add("on"); - } - -} - -window.setInterval(function(){ - getSimulatorData(); - displayData(); -}, 500); diff --git a/static/js/custom/getSimData.js b/static/js/custom/getSimData.js deleted file mode 100644 index 1d34d2d5..00000000 --- a/static/js/custom/getSimData.js +++ /dev/null @@ -1,260 +0,0 @@ -let altitude; -let fuel_percentage; -let vertical_speed; -let compass; -let airspeed; -let latitude; -let longitude; - -let autopilot_master; -let autopilot_nav_selected; -let autopilot_wing_leveler; -let autopilot_heading_lock; -let autopilot_heading_lock_dir; -let autopilot_altitude_lock; -let autopilot_altitude_lock_var; -let autopilot_attitude_hold; -let autopilot_glidescope_hold; -let autopilot_approach_hold; -let autopilot_backcourse_hold; -let autopilot_vertical_hold; -let autopilot_vertical_hold_var; -let autopilot_pitch_hold; -let autopilot_pitch_hold_ref; -let autopilot_flight_director_active; -let autopilot_airspeed_hold; -let autopilot_airspeed_hold_var; - -let gear_handle_position; -let elevator_trim_pct; -let elevator_trim_pct_reversed; -let rudder_trim_pct; -let flaps_handle_pct; -let flaps_handle_pct_reversed; - -let cabin_seatbelts_alert_switch; -let cabin_no_smoking_alert_switch; - -window.setInterval(function(){ - getSimulatorData(); - displayData() - updateMap() -}, 2000); - - -function getSimulatorData() { - $.getJSON($SCRIPT_ROOT + '/ui', {}, function(data) { - - //Navigation - altitude = data.ALTITUDE; - vertical_speed = data.VERTICAL_SPEED; - compass = data.MAGNETIC_COMPASS + data.MAGVAR; - airspeed = data.AIRSPEED_INDICATE; - latitude = data.LATITUDE; - longitude = data.LONGITUDE; - - //Fuel - fuel_percentage = data.FUEL_PERCENTAGE; - - //Autopilot - autopilot_master = data.AUTOPILOT_MASTER; - autopilot_nav_selected = data.AUTOPILOT_NAV_SELECTED; - autopilot_wing_leveler = data.AUTOPILOT_WING_LEVELER; - autopilot_heading_lock = data.AUTOPILOT_HEADING_LOCK; - autopilot_heading_lock_dir = data.AUTOPILOT_HEADING_LOCK_DIR; - autopilot_altitude_lock = data.AUTOPILOT_ALTITUDE_LOCK; - autopilot_altitude_lock_var = data.AUTOPILOT_ALTITUDE_LOCK_VAR; - autopilot_attitude_hold = data.AUTOPILOT_ATTITUDE_HOLD; - autopilot_glidescope_hold = data.AUTOPILOT_GLIDESLOPE_HOLD; - autopilot_approach_hold = data.AUTOPILOT_APPROACH_HOLD; - autopilot_backcourse_hold = data.AUTOPILOT_BACKCOURSE_HOLD; - autopilot_vertical_hold = data.AUTOPILOT_VERTICAL_HOLD - autopilot_vertical_hold_var = data.AUTOPILOT_VERTICAL_HOLD_VAR; - autopilot_pitch_hold = data.AUTOPILOT_PITCH_HOLD; - autopilot_pitch_hold_ref = data.AUTOPILOT_PITCH_HOLD_REF; - autopilot_flight_director_active = data.AUTOPILOT_FLIGHT_DIRECTOR_ACTIVE; - autopilot_airspeed_hold = data.AUTOPILOT_AIRSPEED_HOLD; - autopilot_airspeed_hold_var = data.AUTOPILOT_AIRSPEED_HOLD_VAR; - - //Control surfaces - gear_handle_position = data.GEAR_HANDLE_POSITION; - elevator_trim_pct = data.ELEVATOR_TRIM_PCT; - elevator_trim_pct_reversed = - elevator_trim_pct - //rudder_trim_pct = data.RUDDER_TRIM_PCT; - flaps_handle_pct = data.FLAPS_HANDLE_PERCENT; - flaps_handle_pct_reversed = - flaps_handle_pct; - - //Cabin - cabin_no_smoking_alert_switch = data.CABIN_NO_SMOKING_ALERT_SWITCH; - cabin_seatbelts_alert_switch = data.CABIN_SEATBELTS_ALERT_SWITCH; - - }); - return false; -} - - -function displayData() { - //Navigation - $("#altitude").text(altitude); - $("#compass").text(compass); - $("#vertical-speed").text(vertical_speed); - $("#airspeed").text(airspeed); - - //Fuel - $("#fuel-percentage").text(fuel_percentage); - $("#fuel-percentage-bar").css("width", fuel_percentage+"%"); - - //Autopilot - checkAndUpdateButton("#autopilot-master", autopilot_master, "Engaged", "Disengaged"); - checkAndUpdateButton("#autopilot-wing-leveler", autopilot_wing_leveler); - checkAndUpdateButton("#autopilot-heading-lock", autopilot_heading_lock); - checkAndUpdateButton("#autopilot-altitude-lock", autopilot_altitude_lock); - checkAndUpdateButton("#autopilot-airspeed-hold", autopilot_airspeed_hold); - checkAndUpdateButton("#autopilot-attitude-hold", autopilot_attitude_hold); - checkAndUpdateButton("#autopilot-backcourse-hold", autopilot_backcourse_hold); - checkAndUpdateButton("#autopilot-approach-hold", autopilot_approach_hold); - checkAndUpdateButton("#autopilot-vertical-hold", autopilot_vertical_hold); - - $("#autopilot-heading-lock-dir").attr('placeholder', autopilot_heading_lock_dir); - $("#autopilot-altitude-lock-var").attr('placeholder', autopilot_altitude_lock_var); - $("#autopilot-airspeed-hold-var").attr('placeholder', autopilot_airspeed_hold_var); - $("#autopilot-pitch-hold-ref").attr('placeholder', autopilot_pitch_hold_ref); - $("#autopilot-vertical-hold-ref").attr('placeholder', autopilot_vertical_hold_var); - - //Control surfaces - $("#gear-handle-position").html(gear_handle_position); - if (gear_handle_position === "UP"){ - $("#gear-handle-position").removeClass("btn-success").addClass("btn-danger"); - } else { - $("#gear-handle-position").removeClass("btn-danger").addClass("btn-success"); - } - - $("#flaps-handle-pct").text(flaps_handle_pct); - $("#flaps-slider").slider({values: [flaps_handle_pct_reversed]}) - - $("#elevator-trim-pct").text(elevator_trim_pct); - $("#elevator-trim-slider").slider({values: [elevator_trim_pct_reversed]}) - - //$("#rudder-trim-pct").text(rudder_trim_pct); - //$("#rudder-trim-slider").slider({values: [rudder_trim_pct]}) - - //Cabin - if (cabin_seatbelts_alert_switch === 1){ - $("#seatbelt-sign").removeClass("btn-outline-danger").addClass("btn-danger").html("Seatbelt sign on"); - } else { - $("#seatbelt-sign").removeClass("btn-danger").addClass("btn-outline-danger").html("Seatbelt sign off"); - } - - if (cabin_no_smoking_alert_switch === 1){ - $("#no-smoking-sign").removeClass("btn-outline-danger").addClass("btn-danger").html("No smoking sign on"); - } else { - $("#no-smoking-sign").removeClass("btn-danger").addClass("btn-outline-danger").html("No smoking sign off"); - }} - -function checkAndUpdateButton(buttonName, variableToCheck, onText="On", offText="Off") { - if (variableToCheck === 1) { - $(buttonName).removeClass("btn-danger").addClass("btn-success").html(onText); - } else { - $(buttonName).removeClass("btn-success").addClass("btn-danger").html(offText); - } -} - - -function toggleFollowPlane() { - followPlane = !followPlane; - if (followPlane === true) { - $("#followMode").text("Moving map enabled") - $("#followModeButton").removeClass("btn-outline-danger").addClass("btn-primary") - } - if (followPlane === false) { - $("#followMode").text("Moving map disabled") - $("#followModeButton").removeClass("btn-primary").addClass("btn-outline-danger") - } -} - -function updateMap() { - var pos = L.latLng(latitude, longitude); - - marker.slideTo( pos, { - duration: 1500, - }); - marker.setRotationAngle(compass); - - if (followPlane === true) { - map.panTo(pos); - } -} - -function setSimDatapoint(datapointToSet, valueToUse) { - url_to_call = "/datapoint/"+datapointToSet+"/set"; - $.post( url_to_call, { value_to_use: valueToUse } ); -} - -function triggerSimEvent(eventToTrigger, valueToUse, hideAlert = false){ - url_to_call = "/event/"+eventToTrigger+"/trigger"; - $.post( url_to_call, { value_to_use: valueToUse } ); - - if (!hideAlert) { - temporaryAlert('', "Sending instruction", "success") - } -} - -function triggerSimEventFromField(eventToTrigger, fieldToUse, messageToDisplay = null){ - // Get the field and the value in there - fieldToUse = "#" + fieldToUse - valueToUse = $(fieldToUse).val(); - - // Pass it to the API - url_to_call = "/event/"+eventToTrigger+"/trigger"; - $.post( url_to_call, { value_to_use: valueToUse } ); - - // Clear the field so it can be repopulated with the placeholder - $(fieldToUse).val("") - - if (messageToDisplay) { - temporaryAlert('', messageToDisplay + " to " + valueToUse, "success") - } - -} - -function triggerCustomEmergency(emergency_type) { - url_to_call = "/custom_emergency/" + emergency_type - $.post (url_to_call) - - if (emergency_type === "random_engine_fire") { - temporaryAlert("Fire!", "Random engine fire trigger sent", "error") - } -} - - -function temporaryAlert(title, message, icon) { - let timerInterval - - Swal.fire({ - title: title, - html: message, - icon: icon, - timer: 2000, - timerProgressBar: true, - onBeforeOpen: () => { - Swal.showLoading() - timerInterval = setInterval(() => { - const content = Swal.getContent() - if (content) { - const b = content.querySelector('b') - if (b) { - b.textContent = Swal.getTimerLeft() - } - } - }, 100) - }, - onClose: () => { - clearInterval(timerInterval) - } - }).then((result) => { - /* Read more about handling dismissals below */ - if (result.dismiss === Swal.DismissReason.timer) { - console.log('I was closed by the timer') - } - }) -} \ No newline at end of file diff --git a/static/js/custom/plane.png b/static/js/custom/plane.png deleted file mode 100644 index e67f2e5b..00000000 Binary files a/static/js/custom/plane.png and /dev/null differ diff --git a/static/vendor/bootstrap/bootstrap.min.css b/static/vendor/bootstrap/bootstrap.min.css deleted file mode 100644 index 21d10bad..00000000 --- a/static/vendor/bootstrap/bootstrap.min.css +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Bootstrap v4.5.2 (https://getbootstrap.com/) - * Copyright 2011-2020 The Bootstrap Authors - * Copyright 2011-2020 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#0069d9;border-color:#0062cc;box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#138496;border-color:#117a8b;box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{color:#212529;background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{color:#212529;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;z-index:1;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before,.custom-control-input[disabled]~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;-ms-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item,.nav-fill>.nav-link{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{-ms-flex:1 0 0%;flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion{overflow-anchor:none}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item{display:-ms-flexbox;display:flex}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;line-height:0;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{-ms-flex-preferred-size:350px;flex-basis:350px;max-width:350px;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:-webkit-min-content;height:-moz-min-content;height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:-webkit-min-content;height:-moz-min-content;height:min-content}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;-ms-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;overflow-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} -/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/static/vendor/bootstrap/bootstrap.min.js b/static/vendor/bootstrap/bootstrap.min.js deleted file mode 100644 index ef4d9cbd..00000000 --- a/static/vendor/bootstrap/bootstrap.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Bootstrap v4.5.2 (https://getbootstrap.com/) - * Copyright 2011-2020 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap={},t.jQuery,t.Popper)}(this,(function(t,e,n){"use strict";function i(t,e){for(var n=0;n=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}};a.jQueryDetection(),e.fn.emulateTransitionEnd=r,e.event.special[a.TRANSITION_END]={bindType:"transitionend",delegateType:"transitionend",handle:function(t){if(e(t.target).is(this))return t.handleObj.handler.apply(this,arguments)}};var l="alert",c=e.fn[l],h=function(){function t(t){this._element=t}var n=t.prototype;return n.close=function(t){var e=this._element;t&&(e=this._getRootElement(t)),this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},n.dispose=function(){e.removeData(this._element,"bs.alert"),this._element=null},n._getRootElement=function(t){var n=a.getSelectorFromElement(t),i=!1;return n&&(i=document.querySelector(n)),i||(i=e(t).closest(".alert")[0]),i},n._triggerCloseEvent=function(t){var n=e.Event("close.bs.alert");return e(t).trigger(n),n},n._removeElement=function(t){var n=this;if(e(t).removeClass("show"),e(t).hasClass("fade")){var i=a.getTransitionDurationFromElement(t);e(t).one(a.TRANSITION_END,(function(e){return n._destroyElement(t,e)})).emulateTransitionEnd(i)}else this._destroyElement(t)},n._destroyElement=function(t){e(t).detach().trigger("closed.bs.alert").remove()},t._jQueryInterface=function(n){return this.each((function(){var i=e(this),o=i.data("bs.alert");o||(o=new t(this),i.data("bs.alert",o)),"close"===n&&o[n](this)}))},t._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},o(t,null,[{key:"VERSION",get:function(){return"4.5.2"}}]),t}();e(document).on("click.bs.alert.data-api",'[data-dismiss="alert"]',h._handleDismiss(new h)),e.fn[l]=h._jQueryInterface,e.fn[l].Constructor=h,e.fn[l].noConflict=function(){return e.fn[l]=c,h._jQueryInterface};var u=e.fn.button,d=function(){function t(t){this._element=t}var n=t.prototype;return n.toggle=function(){var t=!0,n=!0,i=e(this._element).closest('[data-toggle="buttons"]')[0];if(i){var o=this._element.querySelector('input:not([type="hidden"])');if(o){if("radio"===o.type)if(o.checked&&this._element.classList.contains("active"))t=!1;else{var s=i.querySelector(".active");s&&e(s).removeClass("active")}t&&("checkbox"!==o.type&&"radio"!==o.type||(o.checked=!this._element.classList.contains("active")),e(o).trigger("change")),o.focus(),n=!1}}this._element.hasAttribute("disabled")||this._element.classList.contains("disabled")||(n&&this._element.setAttribute("aria-pressed",!this._element.classList.contains("active")),t&&e(this._element).toggleClass("active"))},n.dispose=function(){e.removeData(this._element,"bs.button"),this._element=null},t._jQueryInterface=function(n){return this.each((function(){var i=e(this).data("bs.button");i||(i=new t(this),e(this).data("bs.button",i)),"toggle"===n&&i[n]()}))},o(t,null,[{key:"VERSION",get:function(){return"4.5.2"}}]),t}();e(document).on("click.bs.button.data-api",'[data-toggle^="button"]',(function(t){var n=t.target,i=n;if(e(n).hasClass("btn")||(n=e(n).closest(".btn")[0]),!n||n.hasAttribute("disabled")||n.classList.contains("disabled"))t.preventDefault();else{var o=n.querySelector('input:not([type="hidden"])');if(o&&(o.hasAttribute("disabled")||o.classList.contains("disabled")))return void t.preventDefault();("LABEL"!==i.tagName||o&&"checkbox"!==o.type)&&d._jQueryInterface.call(e(n),"toggle")}})).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',(function(t){var n=e(t.target).closest(".btn")[0];e(n).toggleClass("focus",/^focus(in)?$/.test(t.type))})),e(window).on("load.bs.button.data-api",(function(){for(var t=[].slice.call(document.querySelectorAll('[data-toggle="buttons"] .btn')),e=0,n=t.length;e0,this._pointerEvent=Boolean(window.PointerEvent||window.MSPointerEvent),this._addEventListeners()}var n=t.prototype;return n.next=function(){this._isSliding||this._slide("next")},n.nextWhenVisible=function(){!document.hidden&&e(this._element).is(":visible")&&"hidden"!==e(this._element).css("visibility")&&this.next()},n.prev=function(){this._isSliding||this._slide("prev")},n.pause=function(t){t||(this._isPaused=!0),this._element.querySelector(".carousel-item-next, .carousel-item-prev")&&(a.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},n.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},n.to=function(t){var n=this;this._activeElement=this._element.querySelector(".active.carousel-item");var i=this._getItemIndex(this._activeElement);if(!(t>this._items.length-1||t<0))if(this._isSliding)e(this._element).one("slid.bs.carousel",(function(){return n.to(t)}));else{if(i===t)return this.pause(),void this.cycle();var o=t>i?"next":"prev";this._slide(o,this._items[t])}},n.dispose=function(){e(this._element).off(g),e.removeData(this._element,"bs.carousel"),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},n._getConfig=function(t){return t=s({},p,t),a.typeCheckConfig(f,t,_),t},n._handleSwipe=function(){var t=Math.abs(this.touchDeltaX);if(!(t<=40)){var e=t/this.touchDeltaX;this.touchDeltaX=0,e>0&&this.prev(),e<0&&this.next()}},n._addEventListeners=function(){var t=this;this._config.keyboard&&e(this._element).on("keydown.bs.carousel",(function(e){return t._keydown(e)})),"hover"===this._config.pause&&e(this._element).on("mouseenter.bs.carousel",(function(e){return t.pause(e)})).on("mouseleave.bs.carousel",(function(e){return t.cycle(e)})),this._config.touch&&this._addTouchEventListeners()},n._addTouchEventListeners=function(){var t=this;if(this._touchSupported){var n=function(e){t._pointerEvent&&v[e.originalEvent.pointerType.toUpperCase()]?t.touchStartX=e.originalEvent.clientX:t._pointerEvent||(t.touchStartX=e.originalEvent.touches[0].clientX)},i=function(e){t._pointerEvent&&v[e.originalEvent.pointerType.toUpperCase()]&&(t.touchDeltaX=e.originalEvent.clientX-t.touchStartX),t._handleSwipe(),"hover"===t._config.pause&&(t.pause(),t.touchTimeout&&clearTimeout(t.touchTimeout),t.touchTimeout=setTimeout((function(e){return t.cycle(e)}),500+t._config.interval))};e(this._element.querySelectorAll(".carousel-item img")).on("dragstart.bs.carousel",(function(t){return t.preventDefault()})),this._pointerEvent?(e(this._element).on("pointerdown.bs.carousel",(function(t){return n(t)})),e(this._element).on("pointerup.bs.carousel",(function(t){return i(t)})),this._element.classList.add("pointer-event")):(e(this._element).on("touchstart.bs.carousel",(function(t){return n(t)})),e(this._element).on("touchmove.bs.carousel",(function(e){return function(e){e.originalEvent.touches&&e.originalEvent.touches.length>1?t.touchDeltaX=0:t.touchDeltaX=e.originalEvent.touches[0].clientX-t.touchStartX}(e)})),e(this._element).on("touchend.bs.carousel",(function(t){return i(t)})))}},n._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case 37:t.preventDefault(),this.prev();break;case 39:t.preventDefault(),this.next()}},n._getItemIndex=function(t){return this._items=t&&t.parentNode?[].slice.call(t.parentNode.querySelectorAll(".carousel-item")):[],this._items.indexOf(t)},n._getItemByDirection=function(t,e){var n="next"===t,i="prev"===t,o=this._getItemIndex(e),s=this._items.length-1;if((i&&0===o||n&&o===s)&&!this._config.wrap)return e;var r=(o+("prev"===t?-1:1))%this._items.length;return-1===r?this._items[this._items.length-1]:this._items[r]},n._triggerSlideEvent=function(t,n){var i=this._getItemIndex(t),o=this._getItemIndex(this._element.querySelector(".active.carousel-item")),s=e.Event("slide.bs.carousel",{relatedTarget:t,direction:n,from:o,to:i});return e(this._element).trigger(s),s},n._setActiveIndicatorElement=function(t){if(this._indicatorsElement){var n=[].slice.call(this._indicatorsElement.querySelectorAll(".active"));e(n).removeClass("active");var i=this._indicatorsElement.children[this._getItemIndex(t)];i&&e(i).addClass("active")}},n._slide=function(t,n){var i,o,s,r=this,l=this._element.querySelector(".active.carousel-item"),c=this._getItemIndex(l),h=n||l&&this._getItemByDirection(t,l),u=this._getItemIndex(h),d=Boolean(this._interval);if("next"===t?(i="carousel-item-left",o="carousel-item-next",s="left"):(i="carousel-item-right",o="carousel-item-prev",s="right"),h&&e(h).hasClass("active"))this._isSliding=!1;else if(!this._triggerSlideEvent(h,s).isDefaultPrevented()&&l&&h){this._isSliding=!0,d&&this.pause(),this._setActiveIndicatorElement(h);var f=e.Event("slid.bs.carousel",{relatedTarget:h,direction:s,from:c,to:u});if(e(this._element).hasClass("slide")){e(h).addClass(o),a.reflow(h),e(l).addClass(i),e(h).addClass(i);var g=parseInt(h.getAttribute("data-interval"),10);g?(this._config.defaultInterval=this._config.defaultInterval||this._config.interval,this._config.interval=g):this._config.interval=this._config.defaultInterval||this._config.interval;var m=a.getTransitionDurationFromElement(l);e(l).one(a.TRANSITION_END,(function(){e(h).removeClass(i+" "+o).addClass("active"),e(l).removeClass("active "+o+" "+i),r._isSliding=!1,setTimeout((function(){return e(r._element).trigger(f)}),0)})).emulateTransitionEnd(m)}else e(l).removeClass("active"),e(h).addClass("active"),this._isSliding=!1,e(this._element).trigger(f);d&&this.cycle()}},t._jQueryInterface=function(n){return this.each((function(){var i=e(this).data("bs.carousel"),o=s({},p,e(this).data());"object"==typeof n&&(o=s({},o,n));var r="string"==typeof n?n:o.slide;if(i||(i=new t(this,o),e(this).data("bs.carousel",i)),"number"==typeof n)i.to(n);else if("string"==typeof r){if("undefined"==typeof i[r])throw new TypeError('No method named "'+r+'"');i[r]()}else o.interval&&o.ride&&(i.pause(),i.cycle())}))},t._dataApiClickHandler=function(n){var i=a.getSelectorFromElement(this);if(i){var o=e(i)[0];if(o&&e(o).hasClass("carousel")){var r=s({},e(o).data(),e(this).data()),l=this.getAttribute("data-slide-to");l&&(r.interval=!1),t._jQueryInterface.call(e(o),r),l&&e(o).data("bs.carousel").to(l),n.preventDefault()}}},o(t,null,[{key:"VERSION",get:function(){return"4.5.2"}},{key:"Default",get:function(){return p}}]),t}();e(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",b._dataApiClickHandler),e(window).on("load.bs.carousel.data-api",(function(){for(var t=[].slice.call(document.querySelectorAll('[data-ride="carousel"]')),n=0,i=t.length;n0&&(this._selector=r,this._triggerArray.push(s))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var n=t.prototype;return n.toggle=function(){e(this._element).hasClass("show")?this.hide():this.show()},n.show=function(){var n,i,o=this;if(!this._isTransitioning&&!e(this._element).hasClass("show")&&(this._parent&&0===(n=[].slice.call(this._parent.querySelectorAll(".show, .collapsing")).filter((function(t){return"string"==typeof o._config.parent?t.getAttribute("data-parent")===o._config.parent:t.classList.contains("collapse")}))).length&&(n=null),!(n&&(i=e(n).not(this._selector).data("bs.collapse"))&&i._isTransitioning))){var s=e.Event("show.bs.collapse");if(e(this._element).trigger(s),!s.isDefaultPrevented()){n&&(t._jQueryInterface.call(e(n).not(this._selector),"hide"),i||e(n).data("bs.collapse",null));var r=this._getDimension();e(this._element).removeClass("collapse").addClass("collapsing"),this._element.style[r]=0,this._triggerArray.length&&e(this._triggerArray).removeClass("collapsed").attr("aria-expanded",!0),this.setTransitioning(!0);var l="scroll"+(r[0].toUpperCase()+r.slice(1)),c=a.getTransitionDurationFromElement(this._element);e(this._element).one(a.TRANSITION_END,(function(){e(o._element).removeClass("collapsing").addClass("collapse show"),o._element.style[r]="",o.setTransitioning(!1),e(o._element).trigger("shown.bs.collapse")})).emulateTransitionEnd(c),this._element.style[r]=this._element[l]+"px"}}},n.hide=function(){var t=this;if(!this._isTransitioning&&e(this._element).hasClass("show")){var n=e.Event("hide.bs.collapse");if(e(this._element).trigger(n),!n.isDefaultPrevented()){var i=this._getDimension();this._element.style[i]=this._element.getBoundingClientRect()[i]+"px",a.reflow(this._element),e(this._element).addClass("collapsing").removeClass("collapse show");var o=this._triggerArray.length;if(o>0)for(var s=0;s0},i._getOffset=function(){var t=this,e={};return"function"==typeof this._config.offset?e.fn=function(e){return e.offsets=s({},e.offsets,t._config.offset(e.offsets,t._element)||{}),e}:e.offset=this._config.offset,e},i._getPopperConfig=function(){var t={placement:this._getPlacement(),modifiers:{offset:this._getOffset(),flip:{enabled:this._config.flip},preventOverflow:{boundariesElement:this._config.boundary}}};return"static"===this._config.display&&(t.modifiers.applyStyle={enabled:!1}),s({},t,this._config.popperConfig)},t._jQueryInterface=function(n){return this.each((function(){var i=e(this).data("bs.dropdown");if(i||(i=new t(this,"object"==typeof n?n:null),e(this).data("bs.dropdown",i)),"string"==typeof n){if("undefined"==typeof i[n])throw new TypeError('No method named "'+n+'"');i[n]()}}))},t._clearMenus=function(n){if(!n||3!==n.which&&("keyup"!==n.type||9===n.which))for(var i=[].slice.call(document.querySelectorAll('[data-toggle="dropdown"]')),o=0,s=i.length;o0&&r--,40===n.which&&rdocument.documentElement.clientHeight;i||(this._element.style.overflowY="hidden"),this._element.classList.add("modal-static");var o=a.getTransitionDurationFromElement(this._dialog);e(this._element).off(a.TRANSITION_END),e(this._element).one(a.TRANSITION_END,(function(){t._element.classList.remove("modal-static"),i||e(t._element).one(a.TRANSITION_END,(function(){t._element.style.overflowY=""})).emulateTransitionEnd(t._element,o)})).emulateTransitionEnd(o),this._element.focus()}else this.hide()},n._showElement=function(t){var n=this,i=e(this._element).hasClass("fade"),o=this._dialog?this._dialog.querySelector(".modal-body"):null;this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),e(this._dialog).hasClass("modal-dialog-scrollable")&&o?o.scrollTop=0:this._element.scrollTop=0,i&&a.reflow(this._element),e(this._element).addClass("show"),this._config.focus&&this._enforceFocus();var s=e.Event("shown.bs.modal",{relatedTarget:t}),r=function(){n._config.focus&&n._element.focus(),n._isTransitioning=!1,e(n._element).trigger(s)};if(i){var l=a.getTransitionDurationFromElement(this._dialog);e(this._dialog).one(a.TRANSITION_END,r).emulateTransitionEnd(l)}else r()},n._enforceFocus=function(){var t=this;e(document).off("focusin.bs.modal").on("focusin.bs.modal",(function(n){document!==n.target&&t._element!==n.target&&0===e(t._element).has(n.target).length&&t._element.focus()}))},n._setEscapeEvent=function(){var t=this;this._isShown?e(this._element).on("keydown.dismiss.bs.modal",(function(e){t._config.keyboard&&27===e.which?(e.preventDefault(),t.hide()):t._config.keyboard||27!==e.which||t._triggerBackdropTransition()})):this._isShown||e(this._element).off("keydown.dismiss.bs.modal")},n._setResizeEvent=function(){var t=this;this._isShown?e(window).on("resize.bs.modal",(function(e){return t.handleUpdate(e)})):e(window).off("resize.bs.modal")},n._hideModal=function(){var t=this;this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._showBackdrop((function(){e(document.body).removeClass("modal-open"),t._resetAdjustments(),t._resetScrollbar(),e(t._element).trigger("hidden.bs.modal")}))},n._removeBackdrop=function(){this._backdrop&&(e(this._backdrop).remove(),this._backdrop=null)},n._showBackdrop=function(t){var n=this,i=e(this._element).hasClass("fade")?"fade":"";if(this._isShown&&this._config.backdrop){if(this._backdrop=document.createElement("div"),this._backdrop.className="modal-backdrop",i&&this._backdrop.classList.add(i),e(this._backdrop).appendTo(document.body),e(this._element).on("click.dismiss.bs.modal",(function(t){n._ignoreBackdropClick?n._ignoreBackdropClick=!1:t.target===t.currentTarget&&n._triggerBackdropTransition()})),i&&a.reflow(this._backdrop),e(this._backdrop).addClass("show"),!t)return;if(!i)return void t();var o=a.getTransitionDurationFromElement(this._backdrop);e(this._backdrop).one(a.TRANSITION_END,t).emulateTransitionEnd(o)}else if(!this._isShown&&this._backdrop){e(this._backdrop).removeClass("show");var s=function(){n._removeBackdrop(),t&&t()};if(e(this._element).hasClass("fade")){var r=a.getTransitionDurationFromElement(this._backdrop);e(this._backdrop).one(a.TRANSITION_END,s).emulateTransitionEnd(r)}else s()}else t&&t()},n._adjustDialog=function(){var t=this._element.scrollHeight>document.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},n._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},n._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=Math.round(t.left+t.right)
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:L,popperConfig:null},K={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},X=function(){function t(t,e){if("undefined"==typeof n)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var i=t.prototype;return i.enable=function(){this._isEnabled=!0},i.disable=function(){this._isEnabled=!1},i.toggleEnabled=function(){this._isEnabled=!this._isEnabled},i.toggle=function(t){if(this._isEnabled)if(t){var n=this.constructor.DATA_KEY,i=e(t.currentTarget).data(n);i||(i=new this.constructor(t.currentTarget,this._getDelegateConfig()),e(t.currentTarget).data(n,i)),i._activeTrigger.click=!i._activeTrigger.click,i._isWithActiveTrigger()?i._enter(null,i):i._leave(null,i)}else{if(e(this.getTipElement()).hasClass("show"))return void this._leave(null,this);this._enter(null,this)}},i.dispose=function(){clearTimeout(this._timeout),e.removeData(this.element,this.constructor.DATA_KEY),e(this.element).off(this.constructor.EVENT_KEY),e(this.element).closest(".modal").off("hide.bs.modal",this._hideModalHandler),this.tip&&e(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},i.show=function(){var t=this;if("none"===e(this.element).css("display"))throw new Error("Please use show on visible elements");var i=e.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){e(this.element).trigger(i);var o=a.findShadowRoot(this.element),s=e.contains(null!==o?o:this.element.ownerDocument.documentElement,this.element);if(i.isDefaultPrevented()||!s)return;var r=this.getTipElement(),l=a.getUID(this.constructor.NAME);r.setAttribute("id",l),this.element.setAttribute("aria-describedby",l),this.setContent(),this.config.animation&&e(r).addClass("fade");var c="function"==typeof this.config.placement?this.config.placement.call(this,r,this.element):this.config.placement,h=this._getAttachment(c);this.addAttachmentClass(h);var u=this._getContainer();e(r).data(this.constructor.DATA_KEY,this),e.contains(this.element.ownerDocument.documentElement,this.tip)||e(r).appendTo(u),e(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new n(this.element,r,this._getPopperConfig(h)),e(r).addClass("show"),"ontouchstart"in document.documentElement&&e(document.body).children().on("mouseover",null,e.noop);var d=function(){t.config.animation&&t._fixTransition();var n=t._hoverState;t._hoverState=null,e(t.element).trigger(t.constructor.Event.SHOWN),"out"===n&&t._leave(null,t)};if(e(this.tip).hasClass("fade")){var f=a.getTransitionDurationFromElement(this.tip);e(this.tip).one(a.TRANSITION_END,d).emulateTransitionEnd(f)}else d()}},i.hide=function(t){var n=this,i=this.getTipElement(),o=e.Event(this.constructor.Event.HIDE),s=function(){"show"!==n._hoverState&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute("aria-describedby"),e(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),t&&t()};if(e(this.element).trigger(o),!o.isDefaultPrevented()){if(e(i).removeClass("show"),"ontouchstart"in document.documentElement&&e(document.body).children().off("mouseover",null,e.noop),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,e(this.tip).hasClass("fade")){var r=a.getTransitionDurationFromElement(i);e(i).one(a.TRANSITION_END,s).emulateTransitionEnd(r)}else s();this._hoverState=""}},i.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},i.isWithContent=function(){return Boolean(this.getTitle())},i.addAttachmentClass=function(t){e(this.getTipElement()).addClass("bs-tooltip-"+t)},i.getTipElement=function(){return this.tip=this.tip||e(this.config.template)[0],this.tip},i.setContent=function(){var t=this.getTipElement();this.setElementContent(e(t.querySelectorAll(".tooltip-inner")),this.getTitle()),e(t).removeClass("fade show")},i.setElementContent=function(t,n){"object"!=typeof n||!n.nodeType&&!n.jquery?this.config.html?(this.config.sanitize&&(n=Q(n,this.config.whiteList,this.config.sanitizeFn)),t.html(n)):t.text(n):this.config.html?e(n).parent().is(t)||t.empty().append(n):t.text(e(n).text())},i.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},i._getPopperConfig=function(t){var e=this;return s({},{placement:t,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:".arrow"},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}},this.config.popperConfig)},i._getOffset=function(){var t=this,e={};return"function"==typeof this.config.offset?e.fn=function(e){return e.offsets=s({},e.offsets,t.config.offset(e.offsets,t.element)||{}),e}:e.offset=this.config.offset,e},i._getContainer=function(){return!1===this.config.container?document.body:a.isElement(this.config.container)?e(this.config.container):e(document).find(this.config.container)},i._getAttachment=function(t){return V[t.toUpperCase()]},i._setListeners=function(){var t=this;this.config.trigger.split(" ").forEach((function(n){if("click"===n)e(t.element).on(t.constructor.Event.CLICK,t.config.selector,(function(e){return t.toggle(e)}));else if("manual"!==n){var i="hover"===n?t.constructor.Event.MOUSEENTER:t.constructor.Event.FOCUSIN,o="hover"===n?t.constructor.Event.MOUSELEAVE:t.constructor.Event.FOCUSOUT;e(t.element).on(i,t.config.selector,(function(e){return t._enter(e)})).on(o,t.config.selector,(function(e){return t._leave(e)}))}})),this._hideModalHandler=function(){t.element&&t.hide()},e(this.element).closest(".modal").on("hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=s({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},i._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},i._enter=function(t,n){var i=this.constructor.DATA_KEY;(n=n||e(t.currentTarget).data(i))||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),e(t.currentTarget).data(i,n)),t&&(n._activeTrigger["focusin"===t.type?"focus":"hover"]=!0),e(n.getTipElement()).hasClass("show")||"show"===n._hoverState?n._hoverState="show":(clearTimeout(n._timeout),n._hoverState="show",n.config.delay&&n.config.delay.show?n._timeout=setTimeout((function(){"show"===n._hoverState&&n.show()}),n.config.delay.show):n.show())},i._leave=function(t,n){var i=this.constructor.DATA_KEY;(n=n||e(t.currentTarget).data(i))||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),e(t.currentTarget).data(i,n)),t&&(n._activeTrigger["focusout"===t.type?"focus":"hover"]=!1),n._isWithActiveTrigger()||(clearTimeout(n._timeout),n._hoverState="out",n.config.delay&&n.config.delay.hide?n._timeout=setTimeout((function(){"out"===n._hoverState&&n.hide()}),n.config.delay.hide):n.hide())},i._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},i._getConfig=function(t){var n=e(this.element).data();return Object.keys(n).forEach((function(t){-1!==M.indexOf(t)&&delete n[t]})),"number"==typeof(t=s({},this.constructor.Default,n,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),a.typeCheckConfig(B,t,this.constructor.DefaultType),t.sanitize&&(t.template=Q(t.template,t.whiteList,t.sanitizeFn)),t},i._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},i._cleanTipClass=function(){var t=e(this.getTipElement()),n=t.attr("class").match(U);null!==n&&n.length&&t.removeClass(n.join(""))},i._handlePopperPlacementChange=function(t){this.tip=t.instance.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},i._fixTransition=function(){var t=this.getTipElement(),n=this.config.animation;null===t.getAttribute("x-placement")&&(e(t).removeClass("fade"),this.config.animation=!1,this.hide(),this.show(),this.config.animation=n)},t._jQueryInterface=function(n){return this.each((function(){var i=e(this).data("bs.tooltip"),o="object"==typeof n&&n;if((i||!/dispose|hide/.test(n))&&(i||(i=new t(this,o),e(this).data("bs.tooltip",i)),"string"==typeof n)){if("undefined"==typeof i[n])throw new TypeError('No method named "'+n+'"');i[n]()}}))},o(t,null,[{key:"VERSION",get:function(){return"4.5.2"}},{key:"Default",get:function(){return z}},{key:"NAME",get:function(){return B}},{key:"DATA_KEY",get:function(){return"bs.tooltip"}},{key:"Event",get:function(){return K}},{key:"EVENT_KEY",get:function(){return".bs.tooltip"}},{key:"DefaultType",get:function(){return W}}]),t}();e.fn[B]=X._jQueryInterface,e.fn[B].Constructor=X,e.fn[B].noConflict=function(){return e.fn[B]=H,X._jQueryInterface};var Y="popover",$=e.fn[Y],J=new RegExp("(^|\\s)bs-popover\\S+","g"),G=s({},X.Default,{placement:"right",trigger:"click",content:"",template:''}),Z=s({},X.DefaultType,{content:"(string|element|function)"}),tt={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"},et=function(t){var n,i;function s(){return t.apply(this,arguments)||this}i=t,(n=s).prototype=Object.create(i.prototype),n.prototype.constructor=n,n.__proto__=i;var r=s.prototype;return r.isWithContent=function(){return this.getTitle()||this._getContent()},r.addAttachmentClass=function(t){e(this.getTipElement()).addClass("bs-popover-"+t)},r.getTipElement=function(){return this.tip=this.tip||e(this.config.template)[0],this.tip},r.setContent=function(){var t=e(this.getTipElement());this.setElementContent(t.find(".popover-header"),this.getTitle());var n=this._getContent();"function"==typeof n&&(n=n.call(this.element)),this.setElementContent(t.find(".popover-body"),n),t.removeClass("fade show")},r._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},r._cleanTipClass=function(){var t=e(this.getTipElement()),n=t.attr("class").match(J);null!==n&&n.length>0&&t.removeClass(n.join(""))},s._jQueryInterface=function(t){return this.each((function(){var n=e(this).data("bs.popover"),i="object"==typeof t?t:null;if((n||!/dispose|hide/.test(t))&&(n||(n=new s(this,i),e(this).data("bs.popover",n)),"string"==typeof t)){if("undefined"==typeof n[t])throw new TypeError('No method named "'+t+'"');n[t]()}}))},o(s,null,[{key:"VERSION",get:function(){return"4.5.2"}},{key:"Default",get:function(){return G}},{key:"NAME",get:function(){return Y}},{key:"DATA_KEY",get:function(){return"bs.popover"}},{key:"Event",get:function(){return tt}},{key:"EVENT_KEY",get:function(){return".bs.popover"}},{key:"DefaultType",get:function(){return Z}}]),s}(X);e.fn[Y]=et._jQueryInterface,e.fn[Y].Constructor=et,e.fn[Y].noConflict=function(){return e.fn[Y]=$,et._jQueryInterface};var nt="scrollspy",it=e.fn[nt],ot={offset:10,method:"auto",target:""},st={offset:"number",method:"string",target:"(string|element)"},rt=function(){function t(t,n){var i=this;this._element=t,this._scrollElement="BODY"===t.tagName?window:t,this._config=this._getConfig(n),this._selector=this._config.target+" .nav-link,"+this._config.target+" .list-group-item,"+this._config.target+" .dropdown-item",this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,e(this._scrollElement).on("scroll.bs.scrollspy",(function(t){return i._process(t)})),this.refresh(),this._process()}var n=t.prototype;return n.refresh=function(){var t=this,n=this._scrollElement===this._scrollElement.window?"offset":"position",i="auto"===this._config.method?n:this._config.method,o="position"===i?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),[].slice.call(document.querySelectorAll(this._selector)).map((function(t){var n,s=a.getSelectorFromElement(t);if(s&&(n=document.querySelector(s)),n){var r=n.getBoundingClientRect();if(r.width||r.height)return[e(n)[i]().top+o,s]}return null})).filter((function(t){return t})).sort((function(t,e){return t[0]-e[0]})).forEach((function(e){t._offsets.push(e[0]),t._targets.push(e[1])}))},n.dispose=function(){e.removeData(this._element,"bs.scrollspy"),e(this._scrollElement).off(".bs.scrollspy"),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},n._getConfig=function(t){if("string"!=typeof(t=s({},ot,"object"==typeof t&&t?t:{})).target&&a.isElement(t.target)){var n=e(t.target).attr("id");n||(n=a.getUID(nt),e(t.target).attr("id",n)),t.target="#"+n}return a.typeCheckConfig(nt,t,st),t},n._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},n._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},n._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},n._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var o=this._offsets.length;o--;){this._activeTarget!==this._targets[o]&&t>=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t li > .active":".active";i=(i=e.makeArray(e(o).find(r)))[i.length-1]}var l=e.Event("hide.bs.tab",{relatedTarget:this._element}),c=e.Event("show.bs.tab",{relatedTarget:i});if(i&&e(i).trigger(l),e(this._element).trigger(c),!c.isDefaultPrevented()&&!l.isDefaultPrevented()){s&&(n=document.querySelector(s)),this._activate(this._element,o);var h=function(){var n=e.Event("hidden.bs.tab",{relatedTarget:t._element}),o=e.Event("shown.bs.tab",{relatedTarget:i});e(i).trigger(n),e(t._element).trigger(o)};n?this._activate(n,n.parentNode,h):h()}}},n.dispose=function(){e.removeData(this._element,"bs.tab"),this._element=null},n._activate=function(t,n,i){var o=this,s=(!n||"UL"!==n.nodeName&&"OL"!==n.nodeName?e(n).children(".active"):e(n).find("> li > .active"))[0],r=i&&s&&e(s).hasClass("fade"),l=function(){return o._transitionComplete(t,s,i)};if(s&&r){var c=a.getTransitionDurationFromElement(s);e(s).removeClass("show").one(a.TRANSITION_END,l).emulateTransitionEnd(c)}else l()},n._transitionComplete=function(t,n,i){if(n){e(n).removeClass("active");var o=e(n.parentNode).find("> .dropdown-menu .active")[0];o&&e(o).removeClass("active"),"tab"===n.getAttribute("role")&&n.setAttribute("aria-selected",!1)}if(e(t).addClass("active"),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),a.reflow(t),t.classList.contains("fade")&&t.classList.add("show"),t.parentNode&&e(t.parentNode).hasClass("dropdown-menu")){var s=e(t).closest(".dropdown")[0];if(s){var r=[].slice.call(s.querySelectorAll(".dropdown-toggle"));e(r).addClass("active")}t.setAttribute("aria-expanded",!0)}i&&i()},t._jQueryInterface=function(n){return this.each((function(){var i=e(this),o=i.data("bs.tab");if(o||(o=new t(this),i.data("bs.tab",o)),"string"==typeof n){if("undefined"==typeof o[n])throw new TypeError('No method named "'+n+'"');o[n]()}}))},o(t,null,[{key:"VERSION",get:function(){return"4.5.2"}}]),t}();e(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',(function(t){t.preventDefault(),lt._jQueryInterface.call(e(this),"show")})),e.fn.tab=lt._jQueryInterface,e.fn.tab.Constructor=lt,e.fn.tab.noConflict=function(){return e.fn.tab=at,lt._jQueryInterface};var ct=e.fn.toast,ht={animation:"boolean",autohide:"boolean",delay:"number"},ut={animation:!0,autohide:!0,delay:500},dt=function(){function t(t,e){this._element=t,this._config=this._getConfig(e),this._timeout=null,this._setListeners()}var n=t.prototype;return n.show=function(){var t=this,n=e.Event("show.bs.toast");if(e(this._element).trigger(n),!n.isDefaultPrevented()){this._clearTimeout(),this._config.animation&&this._element.classList.add("fade");var i=function(){t._element.classList.remove("showing"),t._element.classList.add("show"),e(t._element).trigger("shown.bs.toast"),t._config.autohide&&(t._timeout=setTimeout((function(){t.hide()}),t._config.delay))};if(this._element.classList.remove("hide"),a.reflow(this._element),this._element.classList.add("showing"),this._config.animation){var o=a.getTransitionDurationFromElement(this._element);e(this._element).one(a.TRANSITION_END,i).emulateTransitionEnd(o)}else i()}},n.hide=function(){if(this._element.classList.contains("show")){var t=e.Event("hide.bs.toast");e(this._element).trigger(t),t.isDefaultPrevented()||this._close()}},n.dispose=function(){this._clearTimeout(),this._element.classList.contains("show")&&this._element.classList.remove("show"),e(this._element).off("click.dismiss.bs.toast"),e.removeData(this._element,"bs.toast"),this._element=null,this._config=null},n._getConfig=function(t){return t=s({},ut,e(this._element).data(),"object"==typeof t&&t?t:{}),a.typeCheckConfig("toast",t,this.constructor.DefaultType),t},n._setListeners=function(){var t=this;e(this._element).on("click.dismiss.bs.toast",'[data-dismiss="toast"]',(function(){return t.hide()}))},n._close=function(){var t=this,n=function(){t._element.classList.add("hide"),e(t._element).trigger("hidden.bs.toast")};if(this._element.classList.remove("show"),this._config.animation){var i=a.getTransitionDurationFromElement(this._element);e(this._element).one(a.TRANSITION_END,n).emulateTransitionEnd(i)}else n()},n._clearTimeout=function(){clearTimeout(this._timeout),this._timeout=null},t._jQueryInterface=function(n){return this.each((function(){var i=e(this),o=i.data("bs.toast");if(o||(o=new t(this,"object"==typeof n&&n),i.data("bs.toast",o)),"string"==typeof n){if("undefined"==typeof o[n])throw new TypeError('No method named "'+n+'"');o[n](this)}}))},o(t,null,[{key:"VERSION",get:function(){return"4.5.2"}},{key:"DefaultType",get:function(){return ht}},{key:"Default",get:function(){return ut}}]),t}();e.fn.toast=dt._jQueryInterface,e.fn.toast.Constructor=dt,e.fn.toast.noConflict=function(){return e.fn.toast=ct,dt._jQueryInterface},t.Alert=h,t.Button=d,t.Carousel=b,t.Collapse=C,t.Dropdown=I,t.Modal=P,t.Popover=et,t.Scrollspy=rt,t.Tab=lt,t.Toast=dt,t.Tooltip=X,t.Util=a,Object.defineProperty(t,"__esModule",{value:!0})})); -//# sourceMappingURL=bootstrap.min.js.map \ No newline at end of file diff --git a/static/vendor/jquery-ui-slider/jquery-ui-slider-pips.css b/static/vendor/jquery-ui-slider/jquery-ui-slider-pips.css deleted file mode 100644 index 94a1755b..00000000 --- a/static/vendor/jquery-ui-slider/jquery-ui-slider-pips.css +++ /dev/null @@ -1,326 +0,0 @@ -/*! jQuery-ui-Slider-Pips - v1.11.4 - 2016-09-04 -* Copyright (c) 2016 Simon Goellner ; Licensed MIT */ - -/* HORIZONTAL */ -/* increase bottom margin to fit the pips */ -.ui-slider-horizontal.ui-slider-pips { - margin-bottom: 1.4em; -} - -/* default hide the labels and pips that arnt visible */ -/* we just use css to hide incase we want to show certain */ -/* labels/pips individually later */ -.ui-slider-pips .ui-slider-label, -.ui-slider-pips .ui-slider-pip-hide { - display: none; -} - -/* now we show any labels that we've set to show in the options */ -.ui-slider-pips .ui-slider-pip-label .ui-slider-label { - display: block; -} - -/* PIP/LABEL WRAPPER */ -/* position each pip absolutely just below the default slider */ -/* and also prevent accidental selection */ -.ui-slider-pips .ui-slider-pip { - width: 2em; - height: 1em; - line-height: 1em; - position: absolute; - font-size: 0.8em; - color: #999; - overflow: visible; - text-align: center; - top: 20px; - left: 20px; - margin-left: -1em; - cursor: pointer; - -webkit-touch-callout: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - -.ui-state-disabled.ui-slider-pips .ui-slider-pip { - cursor: default; -} - -/* little pip/line position & size */ -.ui-slider-pips .ui-slider-line { - background: #999; - width: 1px; - height: 3px; - position: absolute; - left: 50%; -} - -/* the text label postion & size */ -/* it overflows so no need for width to be accurate */ -.ui-slider-pips .ui-slider-label { - position: absolute; - top: 5px; - left: 50%; - margin-left: -1em; - width: 2em; -} - -/* make it easy to see when we hover a label */ -.ui-slider-pips:not(.ui-slider-disabled) .ui-slider-pip:hover .ui-slider-label { - color: black; - font-weight: bold; -} - -/* VERTICAL */ -/* vertical slider needs right-margin, not bottom */ -.ui-slider-vertical.ui-slider-pips { - margin-bottom: 1em; - margin-right: 2em; -} - -/* align vertical pips left and to right of the slider */ -.ui-slider-vertical.ui-slider-pips .ui-slider-pip { - text-align: left; - top: auto; - left: 20px; - margin-left: 0; - margin-bottom: -0.5em; -} - -/* vertical line/pip should be horizontal instead */ -.ui-slider-vertical.ui-slider-pips .ui-slider-line { - width: 3px; - height: 1px; - position: absolute; - top: 50%; - left: 0; -} - -.ui-slider-vertical.ui-slider-pips .ui-slider-label { - top: 50%; - left: 0.5em; - margin-left: 0; - margin-top: -0.5em; - width: 2em; -} - -/* FLOATING HORIZTONAL TOOLTIPS */ -/* remove the godawful looking focus outline on handle and float */ -.ui-slider-float .ui-slider-handle:focus, -.ui-slider-float .ui-slider-handle.ui-state-focus .ui-slider-tip-label, -.ui-slider-float .ui-slider-handle:focus .ui-slider-tip, -.ui-slider-float .ui-slider-handle.ui-state-focus .ui-slider-tip-label, -.ui-slider-float .ui-slider-handle:focus .ui-slider-tip-label -.ui-slider-float .ui-slider-handle.ui-state-focus .ui-slider-tip-label { - outline: none; -} - -/* style tooltips on handles and on labels */ -/* also has a nice transition */ -.ui-slider-float .ui-slider-tip, -.ui-slider-float .ui-slider-tip-label { - position: absolute; - visibility: hidden; - top: -40px; - display: block; - width: 34px; - margin-left: -18px; - left: 50%; - height: 20px; - line-height: 20px; - background: white; - border-radius: 3px; - border: 1px solid #888; - text-align: center; - font-size: 12px; - opacity: 0; - color: #333; - -webkit-transition-property: opacity, top, visibility; - transition-property: opacity, top, visibility; - -webkit-transition-timing-function: ease-in; - transition-timing-function: ease-in; - -webkit-transition-duration: 200ms, 200ms, 0ms; - transition-duration: 200ms, 200ms, 0ms; - -webkit-transition-delay: 0ms, 0ms, 200ms; - transition-delay: 0ms, 0ms, 200ms; -} - -/* show the tooltip on hover or focus */ -/* also switch transition delay around */ -.ui-slider-float .ui-slider-handle:hover .ui-slider-tip, -.ui-slider-float .ui-slider-handle.ui-state-hover .ui-slider-tip, -.ui-slider-float .ui-slider-handle:focus .ui-slider-tip, -.ui-slider-float .ui-slider-handle.ui-state-focus .ui-slider-tip, -.ui-slider-float .ui-slider-handle.ui-state-active .ui-slider-tip, -.ui-slider-float .ui-slider-pip:hover .ui-slider-tip-label { - opacity: 1; - top: -30px; - visibility: visible; - -webkit-transition-timing-function: ease-out; - transition-timing-function: ease-out; - -webkit-transition-delay: 200ms, 200ms, 0ms; - transition-delay: 200ms, 200ms, 0ms; -} - -/* put label tooltips below slider */ -.ui-slider-float .ui-slider-pip .ui-slider-tip-label { - top: 42px; -} - -.ui-slider-float .ui-slider-pip:hover .ui-slider-tip-label { - top: 32px; - font-weight: normal; -} - -/* give the tooltip a css triangle arrow */ -.ui-slider-float .ui-slider-tip:after, -.ui-slider-float .ui-slider-pip .ui-slider-tip-label:after { - content: " "; - width: 0; - height: 0; - border: 5px solid rgba(255, 255, 255, 0); - border-top-color: white; - position: absolute; - bottom: -10px; - left: 50%; - margin-left: -5px; -} - -/* put a 1px border on the tooltip arrow to match tooltip border */ -.ui-slider-float .ui-slider-tip:before, -.ui-slider-float .ui-slider-pip .ui-slider-tip-label:before { - content: " "; - width: 0; - height: 0; - border: 5px solid rgba(255, 255, 255, 0); - border-top-color: #888; - position: absolute; - bottom: -11px; - left: 50%; - margin-left: -5px; -} - -/* switch the arrow to top on labels */ -.ui-slider-float .ui-slider-pip .ui-slider-tip-label:after { - border: 5px solid rgba(255, 255, 255, 0); - border-bottom-color: white; - top: -10px; -} - -.ui-slider-float .ui-slider-pip .ui-slider-tip-label:before { - border: 5px solid rgba(255, 255, 255, 0); - border-bottom-color: #888; - top: -11px; -} - -/* FLOATING VERTICAL TOOLTIPS */ -/* tooltip floats to left of handle */ -.ui-slider-vertical.ui-slider-float .ui-slider-tip, -.ui-slider-vertical.ui-slider-float .ui-slider-tip-label { - top: 50%; - margin-top: -11px; - width: 34px; - margin-left: 0px; - left: -60px; - color: #333; - -webkit-transition-duration: 200ms, 200ms, 0; - transition-duration: 200ms, 200ms, 0; - -webkit-transition-property: opacity, left, visibility; - transition-property: opacity, left, visibility; - -webkit-transition-delay: 0, 0, 200ms; - transition-delay: 0, 0, 200ms; -} - -.ui-slider-vertical.ui-slider-float .ui-slider-handle:hover .ui-slider-tip, -.ui-slider-vertical.ui-slider-float .ui-slider-handle.ui-state-hover .ui-slider-tip, -.ui-slider-vertical.ui-slider-float .ui-slider-handle:focus .ui-slider-tip, -.ui-slider-vertical.ui-slider-float .ui-slider-handle.ui-state-focus .ui-slider-tip, -.ui-slider-vertical.ui-slider-float .ui-slider-handle.ui-state-active .ui-slider-tip, -.ui-slider-vertical.ui-slider-float .ui-slider-pip:hover .ui-slider-tip-label { - top: 50%; - margin-top: -11px; - left: -50px; -} - -/* put label tooltips to right of slider */ -.ui-slider-vertical.ui-slider-float .ui-slider-pip .ui-slider-tip-label { - left: 47px; -} - -.ui-slider-vertical.ui-slider-float .ui-slider-pip:hover .ui-slider-tip-label { - left: 37px; -} - -/* give the tooltip a css triangle arrow */ -.ui-slider-vertical.ui-slider-float .ui-slider-tip:after, -.ui-slider-vertical.ui-slider-float .ui-slider-pip .ui-slider-tip-label:after { - border: 5px solid rgba(255, 255, 255, 0); - border-left-color: white; - border-top-color: transparent; - position: absolute; - bottom: 50%; - margin-bottom: -5px; - right: -10px; - margin-left: 0; - top: auto; - left: auto; -} - -.ui-slider-vertical.ui-slider-float .ui-slider-tip:before, -.ui-slider-vertical.ui-slider-float .ui-slider-pip .ui-slider-tip-label:before { - border: 5px solid rgba(255, 255, 255, 0); - border-left-color: #888; - border-top-color: transparent; - position: absolute; - bottom: 50%; - margin-bottom: -5px; - right: -11px; - margin-left: 0; - top: auto; - left: auto; -} - -.ui-slider-vertical.ui-slider-float .ui-slider-pip .ui-slider-tip-label:after { - border: 5px solid rgba(255, 255, 255, 0); - border-right-color: white; - right: auto; - left: -10px; -} - -.ui-slider-vertical.ui-slider-float .ui-slider-pip .ui-slider-tip-label:before { - border: 5px solid rgba(255, 255, 255, 0); - border-right-color: #888; - right: auto; - left: -11px; -} - -/* SELECTED STATES */ -/* Comment out this chuck of code if you don't want to have - the new label colours shown */ -.ui-slider-pips [class*=ui-slider-pip-initial] { - font-weight: bold; - color: #14CA82; -} - -.ui-slider-pips .ui-slider-pip-initial-2 { - color: #1897C9; -} - -.ui-slider-pips [class*=ui-slider-pip-selected] { - font-weight: bold; - color: #FF7A00; -} - -.ui-slider-pips .ui-slider-pip-inrange { - color: black; -} - -.ui-slider-pips .ui-slider-pip-selected-2 { - color: #E70081; -} - -.ui-slider-pips [class*=ui-slider-pip-selected] .ui-slider-line, -.ui-slider-pips .ui-slider-pip-inrange .ui-slider-line { - background: black; -} diff --git a/static/vendor/jquery-ui-slider/jquery-ui-slider-pips.js b/static/vendor/jquery-ui-slider/jquery-ui-slider-pips.js deleted file mode 100644 index 8a5c6952..00000000 --- a/static/vendor/jquery-ui-slider/jquery-ui-slider-pips.js +++ /dev/null @@ -1,812 +0,0 @@ -/*! jQuery-ui-Slider-Pips - v1.11.4 - 2016-09-04 -* Copyright (c) 2016 Simon Goellner ; Licensed MIT */ - -(function($) { - - "use strict"; - - var extensionMethods = { - - - - - - // pips - - pips: function( settings ) { - - var slider = this, - i, j, p, - collection = "", - mousedownHandlers, - min = slider._valueMin(), - max = slider._valueMax(), - pips = ( max - min ) / slider.options.step, - $handles = slider.element.find(".ui-slider-handle"), - $pips; - - var options = { - - first: "label", - /* "label", "pip", false */ - - last: "label", - /* "label", "pip", false */ - - rest: "pip", - /* "label", "pip", false */ - - labels: false, - /* [array], { first: "string", rest: [array], last: "string" }, false */ - - prefix: "", - /* "", string */ - - suffix: "", - /* "", string */ - - step: ( pips > 100 ) ? Math.floor( pips * 0.05 ) : 1, - /* number */ - - formatLabel: function(value) { - return this.prefix + value + this.suffix; - } - /* function - must return a value to display in the pip labels */ - - }; - - if ( $.type( settings ) === "object" || $.type( settings ) === "undefined" ) { - - $.extend( options, settings ); - slider.element.data("pips-options", options ); - - } else { - - if ( settings === "destroy" ) { - - destroy(); - - } else if ( settings === "refresh" ) { - - slider.element.slider( "pips", slider.element.data("pips-options") ); - - } - - return; - - } - - - // we don't want the step ever to be a floating point or negative - // (or 0 actually, so we'll set it to 1 in that case). - slider.options.pipStep = Math.abs( Math.round( options.step ) ) || 1; - - // get rid of all pips that might already exist. - slider.element - .off( ".selectPip" ) - .addClass("ui-slider-pips") - .find(".ui-slider-pip") - .remove(); - - // small object with functions for marking pips as selected. - - var selectPip = { - - single: function(value) { - - this.resetClasses(); - - $pips - .filter(".ui-slider-pip-" + this.classLabel(value) ) - .addClass("ui-slider-pip-selected"); - - if ( slider.options.range ) { - - $pips.each(function(k, v) { - - var pipVal = $(v).children(".ui-slider-label").data("value"); - - if (( slider.options.range === "min" && pipVal < value ) || - ( slider.options.range === "max" && pipVal > value )) { - - $(v).addClass("ui-slider-pip-inrange"); - - } - - }); - - } - - }, - - range: function(values) { - - this.resetClasses(); - - for ( i = 0; i < values.length; i++ ) { - - $pips - .filter(".ui-slider-pip-" + this.classLabel(values[i]) ) - .addClass("ui-slider-pip-selected-" + ( i + 1 ) ); - - } - - if ( slider.options.range ) { - - $pips.each(function(k, v) { - - var pipVal = $(v).children(".ui-slider-label").data("value"); - - if ( pipVal > values[0] && pipVal < values[1] ) { - - $(v).addClass("ui-slider-pip-inrange"); - - } - - }); - - } - - }, - - classLabel: function(value) { - - return value.toString().replace(".", "-"); - - }, - - resetClasses: function() { - - var regex = /(^|\s*)(ui-slider-pip-selected|ui-slider-pip-inrange)(-{1,2}\d+|\s|$)/gi; - - $pips.removeClass( function(index, css) { - return ( css.match(regex) || [] ).join(" "); - }); - - } - - }; - - function getClosestHandle( val ) { - - var h, k, - sliderVals, - comparedVals, - closestVal, - tempHandles = [], - closestHandle = 0; - - if ( slider.values() && slider.values().length ) { - - // get the current values of the slider handles - sliderVals = slider.values(); - - // find the offset value from the `val` for each - // handle, and store it in a new array - comparedVals = $.map( sliderVals, function(v) { - return Math.abs( v - val ); - }); - - // figure out the closest handles to the value - closestVal = Math.min.apply( Math, comparedVals ); - - // if a comparedVal is the closestVal, then - // set the value accordingly, and set the closest handle. - for ( h = 0; h < comparedVals.length; h++ ) { - if ( comparedVals[h] === closestVal ) { - tempHandles.push(h); - } - } - - // set the closest handle to the first handle in array, - // just incase we have no _lastChangedValue to compare to. - closestHandle = tempHandles[0]; - - // now we want to find out if any of the closest handles were - // the last changed handle, if so we specify that handle to change - for ( k = 0; k < tempHandles.length; k++ ) { - if ( slider._lastChangedValue === tempHandles[k] ) { - closestHandle = tempHandles[k]; - } - } - - if ( slider.options.range && tempHandles.length === 2 ) { - - if ( val > sliderVals[1] ) { - - closestHandle = tempHandles[1]; - - } else if ( val < sliderVals[0] ) { - - closestHandle = tempHandles[0]; - - } - - } - - } - - return closestHandle; - - } - - function destroy() { - - slider.element - .off(".selectPip") - .on("mousedown.slider", slider.element.data("mousedown-original") ) - .removeClass("ui-slider-pips") - .find(".ui-slider-pip") - .remove(); - - } - - // when we click on a label, we want to make sure the - // slider's handle actually goes to that label! - // so we check all the handles and see which one is closest - // to the label we clicked. If 2 handles are equidistant then - // we move both of them. We also want to trigger focus on the - // handle. - - // without this method the label is just treated like a part - // of the slider and there's no accuracy in the selected value - - function labelClick( label, e ) { - - if (slider.option("disabled")) { - return; - } - - var val = $(label).data("value"), - indexToChange = getClosestHandle( val ); - - if ( slider.values() && slider.values().length ) { - - slider.options.values[ indexToChange ] = slider._trimAlignValue( val ); - - } else { - - slider.options.value = slider._trimAlignValue( val ); - - } - - slider._refreshValue(); - slider._change( e, indexToChange ); - - } - - // method for creating a pip. We loop this for creating all - // the pips. - - function createPip( which ) { - - var label, - percent, - number = which, - classes = "ui-slider-pip", - css = "", - value = slider.value(), - values = slider.values(), - labelValue, - classLabel, - labelIndex; - - if ( which === "first" ) { - - number = 0; - - } else if ( which === "last" ) { - - number = pips; - - } - - // labelValue is the actual value of the pip based on the min/step - labelValue = min + ( slider.options.step * number ); - - // classLabel replaces any decimals with hyphens - classLabel = labelValue.toString().replace(".", "-"); - - // get the index needed for selecting labels out of the array - labelIndex = ( number + min ) - min; - - // we need to set the human-readable label to either the - // corresponding element in the array, or the appropriate - // item in the object... or an empty string. - - if ( $.type(options.labels) === "array" ) { - - label = options.labels[ labelIndex ] || ""; - - } else if ( $.type( options.labels ) === "object" ) { - - if ( which === "first" ) { - - // set first label - label = options.labels.first || ""; - - } else if ( which === "last" ) { - - // set last label - label = options.labels.last || ""; - - } else if ( $.type( options.labels.rest ) === "array" ) { - - // set other labels, but our index should start at -1 - // because of the first pip. - label = options.labels.rest[ labelIndex - 1 ] || ""; - - } else { - - // urrggh, the options must be f**ked, just show nothing. - label = labelValue; - - } - - } else { - - label = labelValue; - - } - - - - - if ( which === "first" ) { - - // first Pip on the Slider - percent = "0%"; - - classes += " ui-slider-pip-first"; - classes += ( options.first === "label" ) ? " ui-slider-pip-label" : ""; - classes += ( options.first === false ) ? " ui-slider-pip-hide" : ""; - - } else if ( which === "last" ) { - - // last Pip on the Slider - percent = "100%"; - - classes += " ui-slider-pip-last"; - classes += ( options.last === "label" ) ? " ui-slider-pip-label" : ""; - classes += ( options.last === false ) ? " ui-slider-pip-hide" : ""; - - } else { - - // all other Pips - percent = (( 100 / pips ) * which ).toFixed(4) + "%"; - - classes += ( options.rest === "label" ) ? " ui-slider-pip-label" : ""; - classes += ( options.rest === false ) ? " ui-slider-pip-hide" : ""; - - } - - classes += " ui-slider-pip-" + classLabel; - - - // add classes for the initial-selected values. - if ( values && values.length ) { - - for ( i = 0; i < values.length; i++ ) { - - if ( labelValue === values[i] ) { - - classes += " ui-slider-pip-initial-" + ( i + 1 ); - classes += " ui-slider-pip-selected-" + ( i + 1 ); - - } - - } - - if ( slider.options.range ) { - - if ( labelValue > values[0] && - labelValue < values[1] ) { - - classes += " ui-slider-pip-inrange"; - - } - - } - - } else { - - if ( labelValue === value ) { - - classes += " ui-slider-pip-initial"; - classes += " ui-slider-pip-selected"; - - } - - if ( slider.options.range ) { - - if (( slider.options.range === "min" && labelValue < value ) || - ( slider.options.range === "max" && labelValue > value )) { - - classes += " ui-slider-pip-inrange"; - - } - - } - - } - - - - css = ( slider.options.orientation === "horizontal" ) ? - "left: " + percent : - "bottom: " + percent; - - - // add this current pip to the collection - return "" + - "" + - "" + options.formatLabel(label) + "" + - ""; - - } - - // create our first pip - collection += createPip("first"); - - // for every stop in the slider where we need a pip; create one. - for ( p = slider.options.pipStep; p < pips; p += slider.options.pipStep ) { - collection += createPip( p ); - } - - // create our last pip - collection += createPip("last"); - - // append the collection of pips. - slider.element.append( collection ); - - // store the pips for setting classes later. - $pips = slider.element.find(".ui-slider-pip"); - - - - // store the mousedown handlers for later, just in case we reset - // the slider, the handler would be lost! - - if ( $._data( slider.element.get(0), "events").mousedown && - $._data( slider.element.get(0), "events").mousedown.length ) { - - mousedownHandlers = $._data( slider.element.get(0), "events").mousedown; - - } else { - - mousedownHandlers = slider.element.data("mousedown-handlers"); - - } - - slider.element.data("mousedown-handlers", mousedownHandlers.slice() ); - - // loop through all the mousedown handlers on the slider, - // and store the original namespaced (.slider) event handler so - // we can trigger it later. - for ( j = 0; j < mousedownHandlers.length; j++ ) { - if ( mousedownHandlers[j].namespace === "slider" ) { - slider.element.data("mousedown-original", mousedownHandlers[j].handler ); - } - } - - // unbind the mousedown.slider event, because it interferes with - // the labelClick() method (stops smooth animation), and decide - // if we want to trigger the original event based on which element - // was clicked. - slider.element - .off("mousedown.slider") - .on("mousedown.selectPip", function(e) { - - var $target = $(e.target), - closest = getClosestHandle( $target.data("value") ), - $handle = $handles.eq( closest ); - - $handle.addClass("ui-state-active"); - - if ( $target.is(".ui-slider-label") ) { - - labelClick( $target, e ); - - slider.element - .one("mouseup.selectPip", function() { - - $handle - .removeClass("ui-state-active") - .focus(); - - }); - - } else { - - var originalMousedown = slider.element.data("mousedown-original"); - originalMousedown(e); - - } - - }); - - - - - slider.element.on( "slide.selectPip slidechange.selectPip", function(e, ui) { - - var $slider = $(this), - value = $slider.slider("value"), - values = $slider.slider("values"); - - if ( ui ) { - - value = ui.value; - values = ui.values; - - } - - if ( slider.values() && slider.values().length ) { - - selectPip.range( values ); - - } else { - - selectPip.single( value ); - - } - - }); - - - - - }, - - - - - - - - - // floats - - float: function( settings ) { - - var i, - slider = this, - min = slider._valueMin(), - max = slider._valueMax(), - value = slider._value(), - values = slider._values(), - tipValues = [], - $handles = slider.element.find(".ui-slider-handle"); - - var options = { - - handle: true, - /* false */ - - pips: false, - /* true */ - - labels: false, - /* [array], { first: "string", rest: [array], last: "string" }, false */ - - prefix: "", - /* "", string */ - - suffix: "", - /* "", string */ - - event: "slidechange slide", - /* "slidechange", "slide", "slidechange slide" */ - - formatLabel: function(value) { - return this.prefix + value + this.suffix; - } - /* function - must return a value to display in the floats */ - - }; - - if ( $.type( settings ) === "object" || $.type( settings ) === "undefined" ) { - - $.extend( options, settings ); - slider.element.data("float-options", options ); - - } else { - - if ( settings === "destroy" ) { - - destroy(); - - } else if ( settings === "refresh" ) { - - slider.element.slider( "float", slider.element.data("float-options") ); - - } - - return; - - } - - - - - if ( value < min ) { - value = min; - } - - if ( value > max ) { - value = max; - } - - if ( values && values.length ) { - - for ( i = 0; i < values.length; i++ ) { - - if ( values[i] < min ) { - values[i] = min; - } - - if ( values[i] > max ) { - values[i] = max; - } - - } - - } - - // add a class for the CSS - slider.element - .addClass("ui-slider-float") - .find(".ui-slider-tip, .ui-slider-tip-label") - .remove(); - - - - function destroy() { - - slider.element - .off(".sliderFloat") - .removeClass("ui-slider-float") - .find(".ui-slider-tip, .ui-slider-tip-label") - .remove(); - - } - - - function getPipLabels( values ) { - - // when checking the array we need to divide - // by the step option, so we store those values here. - - var vals = [], - steppedVals = $.map( values, function(v) { - return Math.ceil(( v - min ) / slider.options.step); - }); - - // now we just get the values we need to return - // by looping through the values array and assigning the - // label if it exists. - - if ( $.type( options.labels ) === "array" ) { - - for ( i = 0; i < values.length; i++ ) { - - vals[i] = options.labels[ steppedVals[i] ] || values[i]; - - } - - } else if ( $.type( options.labels ) === "object" ) { - - for ( i = 0; i < values.length; i++ ) { - - if ( values[i] === min ) { - - vals[i] = options.labels.first || min; - - } else if ( values[i] === max ) { - - vals[i] = options.labels.last || max; - - } else if ( $.type( options.labels.rest ) === "array" ) { - - vals[i] = options.labels.rest[ steppedVals[i] - 1 ] || values[i]; - - } else { - - vals[i] = values[i]; - - } - - } - - } else { - - for ( i = 0; i < values.length; i++ ) { - - vals[i] = values[i]; - - } - - } - - return vals; - - } - - // apply handle tip if settings allows. - if ( options.handle ) { - - // we need to set the human-readable label to either the - // corresponding element in the array, or the appropriate - // item in the object... or an empty string. - - tipValues = ( slider.values() && slider.values().length ) ? - getPipLabels( values ) : - getPipLabels( [ value ] ); - - for ( i = 0; i < tipValues.length; i++ ) { - - $handles - .eq( i ) - .append( $(""+ options.formatLabel(tipValues[i]) +"") ); - - } - - } - - if ( options.pips ) { - - // if this slider also has pip-labels, we make those into tips, too. - slider.element.find(".ui-slider-label").each(function(k, v) { - - var $this = $(v), - val = [ $this.data("value") ], - label, - $tip; - - - label = options.formatLabel( getPipLabels( val )[0] ); - - // create a tip element - $tip = - $("" + label + "") - .insertAfter( $this ); - - }); - - } - - // check that the event option is actually valid against our - // own list of the slider's events. - if ( options.event !== "slide" && - options.event !== "slidechange" && - options.event !== "slide slidechange" && - options.event !== "slidechange slide" ) { - - options.event = "slidechange slide"; - - } - - // when slider changes, update handle tip label. - slider.element - .off(".sliderFloat") - .on( options.event + ".sliderFloat", function( e, ui ) { - - var uiValue = ( $.type( ui.value ) === "array" ) ? ui.value : [ ui.value ], - val = options.formatLabel( getPipLabels( uiValue )[0] ); - - $(ui.handle) - .find(".ui-slider-tip") - .html( val ); - - }); - - } - - }; - - $.extend(true, $.ui.slider.prototype, extensionMethods); - -})(jQuery); diff --git a/static/vendor/leaflet-providers/leaflet-providers.js b/static/vendor/leaflet-providers/leaflet-providers.js deleted file mode 100644 index 6d834983..00000000 --- a/static/vendor/leaflet-providers/leaflet-providers.js +++ /dev/null @@ -1,1011 +0,0 @@ -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['leaflet'], factory); - } else if (typeof modules === 'object' && module.exports) { - // define a Common JS module that relies on 'leaflet' - module.exports = factory(require('leaflet')); - } else { - // Assume Leaflet is loaded into global object L already - factory(L); - } -}(this, function (L) { - 'use strict'; - - L.TileLayer.Provider = L.TileLayer.extend({ - initialize: function (arg, options) { - var providers = L.TileLayer.Provider.providers; - - var parts = arg.split('.'); - - var providerName = parts[0]; - var variantName = parts[1]; - - if (!providers[providerName]) { - throw 'No such provider (' + providerName + ')'; - } - - var provider = { - url: providers[providerName].url, - options: providers[providerName].options - }; - - // overwrite values in provider from variant. - if (variantName && 'variants' in providers[providerName]) { - if (!(variantName in providers[providerName].variants)) { - throw 'No such variant of ' + providerName + ' (' + variantName + ')'; - } - var variant = providers[providerName].variants[variantName]; - var variantOptions; - if (typeof variant === 'string') { - variantOptions = { - variant: variant - }; - } else { - variantOptions = variant.options; - } - provider = { - url: variant.url || provider.url, - options: L.Util.extend({}, provider.options, variantOptions) - }; - } - - // replace attribution placeholders with their values from toplevel provider attribution, - // recursively - var attributionReplacer = function (attr) { - if (attr.indexOf('{attribution.') === -1) { - return attr; - } - return attr.replace(/\{attribution.(\w*)\}/g, - function (match, attributionName) { - return attributionReplacer(providers[attributionName].options.attribution); - } - ); - }; - provider.options.attribution = attributionReplacer(provider.options.attribution); - - // Compute final options combining provider options with any user overrides - var layerOpts = L.Util.extend({}, provider.options, options); - L.TileLayer.prototype.initialize.call(this, provider.url, layerOpts); - } - }); - - /** - * Definition of providers. - * see http://leafletjs.com/reference.html#tilelayer for options in the options map. - */ - - L.TileLayer.Provider.providers = { - OpenStreetMap: { - url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - options: { - maxZoom: 19, - attribution: - '© OpenStreetMap contributors' - }, - variants: { - Mapnik: {}, - DE: { - url: 'https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png', - options: { - maxZoom: 18 - } - }, - CH: { - url: 'https://tile.osm.ch/switzerland/{z}/{x}/{y}.png', - options: { - maxZoom: 18, - bounds: [[45, 5], [48, 11]] - } - }, - France: { - url: 'https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png', - options: { - maxZoom: 20, - attribution: '© Openstreetmap France | {attribution.OpenStreetMap}' - } - }, - HOT: { - url: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', - options: { - attribution: - '{attribution.OpenStreetMap}, ' + - 'Tiles style by Humanitarian OpenStreetMap Team ' + - 'hosted by OpenStreetMap France' - } - }, - BZH: { - url: 'https://tile.openstreetmap.bzh/br/{z}/{x}/{y}.png', - options: { - attribution: '{attribution.OpenStreetMap}, Tiles courtesy of Breton OpenStreetMap Team', - bounds: [[46.2, -5.5], [50, 0.7]] - } - } - } - }, - OpenSeaMap: { - url: 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', - options: { - attribution: 'Map data: © OpenSeaMap contributors' - } - }, - OpenPtMap: { - url: 'http://openptmap.org/tiles/{z}/{x}/{y}.png', - options: { - maxZoom: 17, - attribution: 'Map data: © OpenPtMap contributors' - } - }, - OpenTopoMap: { - url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', - options: { - maxZoom: 17, - attribution: 'Map data: {attribution.OpenStreetMap}, SRTM | Map style: © OpenTopoMap (CC-BY-SA)' - } - }, - OpenRailwayMap: { - url: 'https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png', - options: { - maxZoom: 19, - attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © OpenRailwayMap (CC-BY-SA)' - } - }, - OpenFireMap: { - url: 'http://openfiremap.org/hytiles/{z}/{x}/{y}.png', - options: { - maxZoom: 19, - attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © OpenFireMap (CC-BY-SA)' - } - }, - SafeCast: { - url: 'https://s3.amazonaws.com/te512.safecast.org/{z}/{x}/{y}.png', - options: { - maxZoom: 16, - attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © SafeCast (CC-BY-SA)' - } - }, - Stadia: { - url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png', - options: { - maxZoom: 20, - attribution: '© Stadia Maps, © OpenMapTiles © OpenStreetMap contributors' - }, - variants: { - AlidadeSmooth: { - url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png' - }, - AlidadeSmoothDark: { - url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png' - }, - OSMBright: { - url: 'https://tiles.stadiamaps.com/tiles/osm_bright/{z}/{x}/{y}{r}.png' - }, - Outdoors: { - url: 'https://tiles.stadiamaps.com/tiles/outdoors/{z}/{x}/{y}{r}.png' - } - } - }, - Thunderforest: { - url: 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}', - options: { - attribution: - '© Thunderforest, {attribution.OpenStreetMap}', - variant: 'cycle', - apikey: '', - maxZoom: 22 - }, - variants: { - OpenCycleMap: 'cycle', - Transport: { - options: { - variant: 'transport' - } - }, - TransportDark: { - options: { - variant: 'transport-dark' - } - }, - SpinalMap: { - options: { - variant: 'spinal-map' - } - }, - Landscape: 'landscape', - Outdoors: 'outdoors', - Pioneer: 'pioneer', - MobileAtlas: 'mobile-atlas', - Neighbourhood: 'neighbourhood' - } - }, - CyclOSM: { - url: 'https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', - options: { - maxZoom: 20, - attribution: 'CyclOSM | Map data: {attribution.OpenStreetMap}' - } - }, - Hydda: { - url: 'https://{s}.tile.openstreetmap.se/hydda/{variant}/{z}/{x}/{y}.png', - options: { - maxZoom: 20, - variant: 'full', - attribution: 'Tiles courtesy of OpenStreetMap Sweden — Map data {attribution.OpenStreetMap}' - }, - variants: { - Full: 'full', - Base: 'base', - RoadsAndLabels: 'roads_and_labels' - } - }, - Jawg: { - url: 'https://{s}.tile.jawg.io/{variant}/{z}/{x}/{y}{r}.png?access-token={accessToken}', - options: { - attribution: - '© JawgMaps ' + - '{attribution.OpenStreetMap}', - minZoom: 0, - maxZoom: 22, - subdomains: 'abcd', - variant: 'jawg-terrain', - // Get your own Jawg access token here : https://www.jawg.io/lab/ - // NB : this is a demonstration key that comes with no guarantee - accessToken: '', - }, - variants: { - Streets: 'jawg-streets', - Terrain: 'jawg-terrain', - Sunny: 'jawg-sunny', - Dark: 'jawg-dark', - Light: 'jawg-light', - Matrix: 'jawg-matrix' - } - }, - MapBox: { - url: 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}{r}?access_token={accessToken}', - options: { - attribution: - '© Mapbox ' + - '{attribution.OpenStreetMap} ' + - 'Improve this map', - tileSize: 512, - maxZoom: 18, - zoomOffset: -1, - id: 'mapbox/streets-v11', - accessToken: '', - } - }, - MapTiler: { - url: 'https://api.maptiler.com/maps/{variant}/{z}/{x}/{y}{r}.{ext}?key={key}', - options: { - attribution: - '© MapTiler © OpenStreetMap contributors', - variant: 'streets', - ext: 'png', - key: '', - tileSize: 512, - zoomOffset: -1, - minZoom: 0, - maxZoom: 21 - }, - variants: { - Streets: 'streets', - Basic: 'basic', - Bright: 'bright', - Pastel: 'pastel', - Positron: 'positron', - Hybrid: { - options: { - variant: 'hybrid', - ext: 'jpg' - } - }, - Toner: 'toner', - Topo: 'topo', - Voyager: 'voyager' - } - }, - Stamen: { - url: 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}', - options: { - attribution: - 'Map tiles by Stamen Design, ' + - 'CC BY 3.0 — ' + - 'Map data {attribution.OpenStreetMap}', - subdomains: 'abcd', - minZoom: 0, - maxZoom: 20, - variant: 'toner', - ext: 'png' - }, - variants: { - Toner: 'toner', - TonerBackground: 'toner-background', - TonerHybrid: 'toner-hybrid', - TonerLines: 'toner-lines', - TonerLabels: 'toner-labels', - TonerLite: 'toner-lite', - Watercolor: { - url: 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}.{ext}', - options: { - variant: 'watercolor', - ext: 'jpg', - minZoom: 1, - maxZoom: 16 - } - }, - Terrain: { - options: { - variant: 'terrain', - minZoom: 0, - maxZoom: 18 - } - }, - TerrainBackground: { - options: { - variant: 'terrain-background', - minZoom: 0, - maxZoom: 18 - } - }, - TerrainLabels: { - options: { - variant: 'terrain-labels', - minZoom: 0, - maxZoom: 18 - } - }, - TopOSMRelief: { - url: 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}.{ext}', - options: { - variant: 'toposm-color-relief', - ext: 'jpg', - bounds: [[22, -132], [51, -56]] - } - }, - TopOSMFeatures: { - options: { - variant: 'toposm-features', - bounds: [[22, -132], [51, -56]], - opacity: 0.9 - } - } - } - }, - TomTom: { - url: 'https://{s}.api.tomtom.com/map/1/tile/{variant}/{style}/{z}/{x}/{y}.{ext}?key={apikey}', - options: { - variant: 'basic', - maxZoom: 22, - attribution: - '© 1992 - ' + new Date().getFullYear() + ' TomTom. ', - subdomains: 'abcd', - style: 'main', - ext: 'png', - apikey: '', - }, - variants: { - Basic: 'basic', - Hybrid: 'hybrid', - Labels: 'labels' - } - }, - Esri: { - url: 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}', - options: { - variant: 'World_Street_Map', - attribution: 'Tiles © Esri' - }, - variants: { - WorldStreetMap: { - options: { - attribution: - '{attribution.Esri} — ' + - 'Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012' - } - }, - DeLorme: { - options: { - variant: 'Specialty/DeLorme_World_Base_Map', - minZoom: 1, - maxZoom: 11, - attribution: '{attribution.Esri} — Copyright: ©2012 DeLorme' - } - }, - WorldTopoMap: { - options: { - variant: 'World_Topo_Map', - attribution: - '{attribution.Esri} — ' + - 'Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community' - } - }, - WorldImagery: { - options: { - variant: 'World_Imagery', - attribution: - '{attribution.Esri} — ' + - 'Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' - } - }, - WorldTerrain: { - options: { - variant: 'World_Terrain_Base', - maxZoom: 13, - attribution: - '{attribution.Esri} — ' + - 'Source: USGS, Esri, TANA, DeLorme, and NPS' - } - }, - WorldShadedRelief: { - options: { - variant: 'World_Shaded_Relief', - maxZoom: 13, - attribution: '{attribution.Esri} — Source: Esri' - } - }, - WorldPhysical: { - options: { - variant: 'World_Physical_Map', - maxZoom: 8, - attribution: '{attribution.Esri} — Source: US National Park Service' - } - }, - OceanBasemap: { - options: { - variant: 'Ocean_Basemap', - maxZoom: 13, - attribution: '{attribution.Esri} — Sources: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri' - } - }, - NatGeoWorldMap: { - options: { - variant: 'NatGeo_World_Map', - maxZoom: 16, - attribution: '{attribution.Esri} — National Geographic, Esri, DeLorme, NAVTEQ, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, iPC' - } - }, - WorldGrayCanvas: { - options: { - variant: 'Canvas/World_Light_Gray_Base', - maxZoom: 16, - attribution: '{attribution.Esri} — Esri, DeLorme, NAVTEQ' - } - } - } - }, - OpenWeatherMap: { - url: 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}', - options: { - maxZoom: 19, - attribution: 'Map data © OpenWeatherMap', - apiKey:'', - opacity: 0.5 - }, - variants: { - Clouds: 'clouds', - CloudsClassic: 'clouds_cls', - Precipitation: 'precipitation', - PrecipitationClassic: 'precipitation_cls', - Rain: 'rain', - RainClassic: 'rain_cls', - Pressure: 'pressure', - PressureContour: 'pressure_cntr', - Wind: 'wind', - Temperature: 'temp', - Snow: 'snow' - } - }, - HERE: { - /* - * HERE maps, formerly Nokia maps. - * These basemaps are free, but you need an api id and app key. Please sign up at - * https://developer.here.com/plans - */ - url: - 'https://{s}.{base}.maps.api.here.com/maptile/2.1/' + - '{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?' + - 'app_id={app_id}&app_code={app_code}&lg={language}', - options: { - attribution: - 'Map © 1987-' + new Date().getFullYear() + ' HERE', - subdomains: '1234', - mapID: 'newest', - 'app_id': '', - 'app_code': '', - base: 'base', - variant: 'normal.day', - maxZoom: 20, - type: 'maptile', - language: 'eng', - format: 'png8', - size: '256' - }, - variants: { - normalDay: 'normal.day', - normalDayCustom: 'normal.day.custom', - normalDayGrey: 'normal.day.grey', - normalDayMobile: 'normal.day.mobile', - normalDayGreyMobile: 'normal.day.grey.mobile', - normalDayTransit: 'normal.day.transit', - normalDayTransitMobile: 'normal.day.transit.mobile', - normalDayTraffic: { - options: { - variant: 'normal.traffic.day', - base: 'traffic', - type: 'traffictile' - } - }, - normalNight: 'normal.night', - normalNightMobile: 'normal.night.mobile', - normalNightGrey: 'normal.night.grey', - normalNightGreyMobile: 'normal.night.grey.mobile', - normalNightTransit: 'normal.night.transit', - normalNightTransitMobile: 'normal.night.transit.mobile', - reducedDay: 'reduced.day', - reducedNight: 'reduced.night', - basicMap: { - options: { - type: 'basetile' - } - }, - mapLabels: { - options: { - type: 'labeltile', - format: 'png' - } - }, - trafficFlow: { - options: { - base: 'traffic', - type: 'flowtile' - } - }, - carnavDayGrey: 'carnav.day.grey', - hybridDay: { - options: { - base: 'aerial', - variant: 'hybrid.day' - } - }, - hybridDayMobile: { - options: { - base: 'aerial', - variant: 'hybrid.day.mobile' - } - }, - hybridDayTransit: { - options: { - base: 'aerial', - variant: 'hybrid.day.transit' - } - }, - hybridDayGrey: { - options: { - base: 'aerial', - variant: 'hybrid.grey.day' - } - }, - hybridDayTraffic: { - options: { - variant: 'hybrid.traffic.day', - base: 'traffic', - type: 'traffictile' - } - }, - pedestrianDay: 'pedestrian.day', - pedestrianNight: 'pedestrian.night', - satelliteDay: { - options: { - base: 'aerial', - variant: 'satellite.day' - } - }, - terrainDay: { - options: { - base: 'aerial', - variant: 'terrain.day' - } - }, - terrainDayMobile: { - options: { - base: 'aerial', - variant: 'terrain.day.mobile' - } - } - } - }, - HEREv3: { - /* - * HERE maps API Version 3. - * These basemaps are free, but you need an API key. Please sign up at - * https://developer.here.com/plans - * Version 3 deprecates the app_id and app_code access in favor of apiKey - * - * Supported access methods as of 2019/12/21: - * @see https://developer.here.com/faqs#access-control-1--how-do-you-control-access-to-here-location-services - */ - url: - 'https://{s}.{base}.maps.ls.hereapi.com/maptile/2.1/' + - '{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?' + - 'apiKey={apiKey}&lg={language}', - options: { - attribution: - 'Map © 1987-' + new Date().getFullYear() + ' HERE', - subdomains: '1234', - mapID: 'newest', - apiKey: '', - base: 'base', - variant: 'normal.day', - maxZoom: 20, - type: 'maptile', - language: 'eng', - format: 'png8', - size: '256' - }, - variants: { - normalDay: 'normal.day', - normalDayCustom: 'normal.day.custom', - normalDayGrey: 'normal.day.grey', - normalDayMobile: 'normal.day.mobile', - normalDayGreyMobile: 'normal.day.grey.mobile', - normalDayTransit: 'normal.day.transit', - normalDayTransitMobile: 'normal.day.transit.mobile', - normalNight: 'normal.night', - normalNightMobile: 'normal.night.mobile', - normalNightGrey: 'normal.night.grey', - normalNightGreyMobile: 'normal.night.grey.mobile', - normalNightTransit: 'normal.night.transit', - normalNightTransitMobile: 'normal.night.transit.mobile', - reducedDay: 'reduced.day', - reducedNight: 'reduced.night', - basicMap: { - options: { - type: 'basetile' - } - }, - mapLabels: { - options: { - type: 'labeltile', - format: 'png' - } - }, - trafficFlow: { - options: { - base: 'traffic', - type: 'flowtile' - } - }, - carnavDayGrey: 'carnav.day.grey', - hybridDay: { - options: { - base: 'aerial', - variant: 'hybrid.day' - } - }, - hybridDayMobile: { - options: { - base: 'aerial', - variant: 'hybrid.day.mobile' - } - }, - hybridDayTransit: { - options: { - base: 'aerial', - variant: 'hybrid.day.transit' - } - }, - hybridDayGrey: { - options: { - base: 'aerial', - variant: 'hybrid.grey.day' - } - }, - pedestrianDay: 'pedestrian.day', - pedestrianNight: 'pedestrian.night', - satelliteDay: { - options: { - base: 'aerial', - variant: 'satellite.day' - } - }, - terrainDay: { - options: { - base: 'aerial', - variant: 'terrain.day' - } - }, - terrainDayMobile: { - options: { - base: 'aerial', - variant: 'terrain.day.mobile' - } - } - } - }, - FreeMapSK: { - url: 'https://{s}.freemap.sk/T/{z}/{x}/{y}.jpeg', - options: { - minZoom: 8, - maxZoom: 16, - subdomains: 'abcd', - bounds: [[47.204642, 15.996093], [49.830896, 22.576904]], - attribution: - '{attribution.OpenStreetMap}, vizualization CC-By-SA 2.0 Freemap.sk' - } - }, - MtbMap: { - url: 'http://tile.mtbmap.cz/mtbmap_tiles/{z}/{x}/{y}.png', - options: { - attribution: - '{attribution.OpenStreetMap} & USGS' - } - }, - CartoDB: { - url: 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png', - options: { - attribution: '{attribution.OpenStreetMap} © CARTO', - subdomains: 'abcd', - maxZoom: 19, - variant: 'light_all' - }, - variants: { - Positron: 'light_all', - PositronNoLabels: 'light_nolabels', - PositronOnlyLabels: 'light_only_labels', - DarkMatter: 'dark_all', - DarkMatterNoLabels: 'dark_nolabels', - DarkMatterOnlyLabels: 'dark_only_labels', - Voyager: 'rastertiles/voyager', - VoyagerNoLabels: 'rastertiles/voyager_nolabels', - VoyagerOnlyLabels: 'rastertiles/voyager_only_labels', - VoyagerLabelsUnder: 'rastertiles/voyager_labels_under' - } - }, - HikeBike: { - url: 'https://tiles.wmflabs.org/{variant}/{z}/{x}/{y}.png', - options: { - maxZoom: 19, - attribution: '{attribution.OpenStreetMap}', - variant: 'hikebike' - }, - variants: { - HikeBike: {}, - HillShading: { - options: { - maxZoom: 15, - variant: 'hillshading' - } - } - } - }, - BasemapAT: { - url: 'https://maps{s}.wien.gv.at/basemap/{variant}/{type}/google3857/{z}/{y}/{x}.{format}', - options: { - maxZoom: 19, - attribution: 'Datenquelle: basemap.at', - subdomains: ['', '1', '2', '3', '4'], - type: 'normal', - format: 'png', - bounds: [[46.358770, 8.782379], [49.037872, 17.189532]], - variant: 'geolandbasemap' - }, - variants: { - basemap: { - options: { - maxZoom: 20, // currently only in Vienna - variant: 'geolandbasemap' - } - }, - grau: 'bmapgrau', - overlay: 'bmapoverlay', - terrain: { - options: { - variant: 'bmapgelaende', - type: 'grau', - format: 'jpeg' - } - }, - surface: { - options: { - variant: 'bmapoberflaeche', - type: 'grau', - format: 'jpeg' - } - }, - highdpi: { - options: { - variant: 'bmaphidpi', - format: 'jpeg' - } - }, - orthofoto: { - options: { - maxZoom: 20, // currently only in Vienna - variant: 'bmaporthofoto30cm', - format: 'jpeg' - } - } - } - }, - nlmaps: { - url: 'https://geodata.nationaalgeoregister.nl/tiles/service/wmts/{variant}/EPSG:3857/{z}/{x}/{y}.png', - options: { - minZoom: 6, - maxZoom: 19, - bounds: [[50.5, 3.25], [54, 7.6]], - attribution: 'Kaartgegevens © Kadaster' - }, - variants: { - 'standaard': 'brtachtergrondkaart', - 'pastel': 'brtachtergrondkaartpastel', - 'grijs': 'brtachtergrondkaartgrijs', - 'luchtfoto': { - 'url': 'https://geodata.nationaalgeoregister.nl/luchtfoto/rgb/wmts/2018_ortho25/EPSG:3857/{z}/{x}/{y}.png', - } - } - }, - NASAGIBS: { - url: 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{maxZoom}/{z}/{y}/{x}.{format}', - options: { - attribution: - 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System ' + - '(ESDIS) with funding provided by NASA/HQ.', - bounds: [[-85.0511287776, -179.999999975], [85.0511287776, 179.999999975]], - minZoom: 1, - maxZoom: 9, - format: 'jpg', - time: '', - tilematrixset: 'GoogleMapsCompatible_Level' - }, - variants: { - ModisTerraTrueColorCR: 'MODIS_Terra_CorrectedReflectance_TrueColor', - ModisTerraBands367CR: 'MODIS_Terra_CorrectedReflectance_Bands367', - ViirsEarthAtNight2012: { - options: { - variant: 'VIIRS_CityLights_2012', - maxZoom: 8 - } - }, - ModisTerraLSTDay: { - options: { - variant: 'MODIS_Terra_Land_Surface_Temp_Day', - format: 'png', - maxZoom: 7, - opacity: 0.75 - } - }, - ModisTerraSnowCover: { - options: { - variant: 'MODIS_Terra_Snow_Cover', - format: 'png', - maxZoom: 8, - opacity: 0.75 - } - }, - ModisTerraAOD: { - options: { - variant: 'MODIS_Terra_Aerosol', - format: 'png', - maxZoom: 6, - opacity: 0.75 - } - }, - ModisTerraChlorophyll: { - options: { - variant: 'MODIS_Terra_Chlorophyll_A', - format: 'png', - maxZoom: 7, - opacity: 0.75 - } - } - } - }, - NLS: { - // NLS maps are copyright National library of Scotland. - // http://maps.nls.uk/projects/api/index.html - // Please contact NLS for anything other than non-commercial low volume usage - // - // Map sources: Ordnance Survey 1:1m to 1:63K, 1920s-1940s - // z0-9 - 1:1m - // z10-11 - quarter inch (1:253440) - // z12-18 - one inch (1:63360) - url: 'https://nls-{s}.tileserver.com/nls/{z}/{x}/{y}.jpg', - options: { - attribution: 'National Library of Scotland Historic Maps', - bounds: [[49.6, -12], [61.7, 3]], - minZoom: 1, - maxZoom: 18, - subdomains: '0123', - } - }, - JusticeMap: { - // Justice Map (http://www.justicemap.org/) - // Visualize race and income data for your community, county and country. - // Includes tools for data journalists, bloggers and community activists. - url: 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png', - options: { - attribution: 'Justice Map', - // one of 'county', 'tract', 'block' - size: 'county', - // Bounds for USA, including Alaska and Hawaii - bounds: [[14, -180], [72, -56]] - }, - variants: { - income: 'income', - americanIndian: 'indian', - asian: 'asian', - black: 'black', - hispanic: 'hispanic', - multi: 'multi', - nonWhite: 'nonwhite', - white: 'white', - plurality: 'plural' - } - }, - Wikimedia: { - url: 'https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}{r}.png', - options: { - attribution: 'Wikimedia', - minZoom: 1, - maxZoom: 19 - } - }, - GeoportailFrance: { - url: 'https://wxs.ign.fr/{apikey}/geoportail/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE={style}&TILEMATRIXSET=PM&FORMAT={format}&LAYER={variant}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}', - options: { - attribution: 'Geoportail France', - bounds: [[-75, -180], [81, 180]], - minZoom: 2, - maxZoom: 18, - // Get your own geoportail apikey here : http://professionnels.ign.fr/ign/contrats/ - // NB : 'choisirgeoportail' is a demonstration key that comes with no guarantee - apikey: 'choisirgeoportail', - format: 'image/jpeg', - style : 'normal', - variant: 'GEOGRAPHICALGRIDSYSTEMS.MAPS.SCAN-EXPRESS.STANDARD' - }, - variants: { - parcels: { - options : { - variant: 'CADASTRALPARCELS.PARCELS', - maxZoom: 20, - style : 'bdparcellaire', - format: 'image/png' - } - }, - ignMaps: 'GEOGRAPHICALGRIDSYSTEMS.MAPS', - maps: 'GEOGRAPHICALGRIDSYSTEMS.MAPS.SCAN-EXPRESS.STANDARD', - orthos: { - options: { - maxZoom: 19, - variant: 'ORTHOIMAGERY.ORTHOPHOTOS' - } - } - } - }, - OneMapSG: { - url: 'https://maps-{s}.onemap.sg/v3/{variant}/{z}/{x}/{y}.png', - options: { - variant: 'Default', - minZoom: 11, - maxZoom: 18, - bounds: [[1.56073, 104.11475], [1.16, 103.502]], - attribution: ' New OneMap | Map data © contributors, Singapore Land Authority' - }, - variants: { - Default: 'Default', - Night: 'Night', - Original: 'Original', - Grey: 'Grey', - LandLot: 'LandLot' - } - } - }; - - L.tileLayer.provider = function (provider, options) { - return new L.TileLayer.Provider(provider, options); - }; - - return L; -})); diff --git a/templates/attitude-indicator/CNAME b/templates/attitude-indicator/CNAME deleted file mode 100644 index 49238d67..00000000 --- a/templates/attitude-indicator/CNAME +++ /dev/null @@ -1 +0,0 @@ -attitude-indicator.igneosaur.co.uk diff --git a/templates/attitude-indicator/README.md b/templates/attitude-indicator/README.md deleted file mode 100644 index 7283af71..00000000 --- a/templates/attitude-indicator/README.md +++ /dev/null @@ -1,2 +0,0 @@ -https://dannyedwards.gitlab.io/attitude-indicator/ -https://github.com/saasmath/attitude-indicator diff --git a/templates/attitude-indicator/index.html b/templates/attitude-indicator/index.html deleted file mode 100644 index e967bb7d..00000000 --- a/templates/attitude-indicator/index.html +++ /dev/null @@ -1,92 +0,0 @@ - - - - - Attitude Indicator - - - - - - - - - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
- -
-
-
-
- -
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
    -
  • STBY
  • -
  • PWR
  • -
  • TEST
  • -
-
    -
  • -
  • -
  • -
-
-
- -
PULL TO CAGE
-
- - - - \ No newline at end of file diff --git a/templates/glass.html b/templates/glass.html deleted file mode 100644 index 33762a76..00000000 --- a/templates/glass.html +++ /dev/null @@ -1,615 +0,0 @@ - - - - - MSFS2020 Cockpit Companion - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - -
-
-
-
- -
- -
- - -
- -
-
-
-
-
-
- Altitude -

?

-
-
-
-
-
-
- V/Speed -

?

-
-
-
-
-
-
- Heading -

?

-
-
-
-
-
-
- Airspeed -

?

-
-
-
-
- -
-
-
- - -
-
-
-
-
-
-
- Fuel % -
-
-
-
-
-
-
-
-
-
- - -
-
-
-
-
-
-
- -
-
-
-
-
- -
- - - - - -
-
-
-
-
-
-
- - -
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
- - -
-
-
-
-
-
-
- Landing gear - -
-
-
-
-
-
- Flaps (%)

- - - - - - - - -
?
- -
-
-
-
-
-
- Elevator Trim (%)

- - - - - - - -
?
-
-
-
- - -
-
-
-
- - -
-
-
-
-
-
-
- Autopilot Master - -
-
-
- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Wing Leveler
Heading -
- -
- -
-
-
Altitude -
- -
- -
-
-
Vertical Speed -
- -
- -
-
-
Airspeed -
- -
- -
-
-
Attitude
Backcourse
Approach
- -
-
-
-
-
-
-
- - -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - -
COM1 Frequency - - - - - -
COM2 Frequency - - - - - -
NAV1 Frequency - - - - - -
NAV2 Frequency - - - - - -
- -
-
-
-
-
-
-
-
-
-
- -
- - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/test_request.md b/test_request.md new file mode 100644 index 00000000..59d8e366 --- /dev/null +++ b/test_request.md @@ -0,0 +1,8 @@ +测试流程要求: + +测试需按照流程逐步逐项执行,当一个项目完全通过后,才可以执行后续的项目。 + +1.import测试 +2.函数的逻辑测试 + +增加功能时,以函数作为最小单位,先进行烟测,通过后才能叠加到现有代码中 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/glass_dummy_server.py b/tests/glass_dummy_server.py deleted file mode 100644 index 10a51570..00000000 --- a/tests/glass_dummy_server.py +++ /dev/null @@ -1,79 +0,0 @@ -# -# This is a dummy server which does not connect to MSFS2020 -# It just serves up random data which allows testing of the front end without MSFS2020 running -# If you want to connect to MSFS2020 then you are looking for glass_server.py - - -from flask import Flask, jsonify, render_template, request -from time import sleep -import random - -app = Flask(__name__) - -latitude = 47.606209 -longitude = -122.332069 - -def thousandify(x): - return f"{x:,}" - - - -@app.route ('/') -def glass(): - return render_template("glass.html") - -@app.route('/ui') -def output_ui_variables(): - global latitude, longitude - - ui_friendly_dictionary = {} - ui_friendly_dictionary["STATUS"] = "success" - ui_friendly_dictionary["ALTITUDE"] = thousandify(random.randint(7995,8005)) - ui_friendly_dictionary["LATITUDE"] = latitude - ui_friendly_dictionary["LONGITUDE"] = longitude - ui_friendly_dictionary["AIRSPEED_INDICATE"] = random.randint(395,405) - ui_friendly_dictionary["MAGNETIC_COMPASS"] = random.randint(89,91) - ui_friendly_dictionary["VERTICAL_SPEED"] = random.randint(-5,5) - ui_friendly_dictionary["FUEL_PERCENTAGE"] = random.randint(79,81) - - ui_friendly_dictionary["AUTOPILOT_HEADING_LOCK"] = random.randint(0,1) - ui_friendly_dictionary["AUTOPILOT_HEADING_LOCK_DIR"] = random.randint(1,360) - - ui_friendly_dictionary["AUTOPILOT_ALTITUDE_LOCK"] = random.randint(0,1) - ui_friendly_dictionary["AUTOPILOT_ALTITUDE_LOCK_VAR"] = thousandify(random.randint(5000,25000)) - - ui_friendly_dictionary["AUTOPILOT_AIRSPEED_HOLD"] = random.randint(0,1) - ui_friendly_dictionary["AUTOPILOT_AIRSPEED_HOLD_VAR"] = thousandify(random.randint(100,350)) - - ui_friendly_dictionary["AUTOPILOT_PITCH_HOLD"] = random.randint(0,1) - ui_friendly_dictionary["AUTOPILOT_PITCH_HOLD_REF"] = thousandify(random.randint(-25,25)) - - if random.randint(0,1) == 1: - ui_friendly_dictionary["GEAR_HANDLE_POSITION"] = "UP" - else: - ui_friendly_dictionary["GEAR_HANDLE_POSITION"] = "DOWN" - - ui_friendly_dictionary["ELEVATOR_TRIM_PCT"] = random.randint(-10,10) - ui_friendly_dictionary["RUDDER_TRIM_PCT"] = random.randint(-10,10) - ui_friendly_dictionary["FLAPS_HANDLE_PERCENT"] = random.randint(0,100) - - longitude = longitude + 0.01 - - return jsonify(ui_friendly_dictionary) - - -@app.route('/datapoint//set', methods=["POST"]) -def set_datapoint(datapoint_name): - - value_to_use = request.form.get('value_to_use') - - if value_to_use == None: - print(datapoint_name + ": " + "No value passed") - else: - print(datapoint_name + ": " + value_to_use) - - status = "success" - return jsonify(status) - - -app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file diff --git a/tests/test_entity_plane.py b/tests/test_entity_plane.py deleted file mode 100644 index afeca062..00000000 --- a/tests/test_entity_plane.py +++ /dev/null @@ -1,62 +0,0 @@ -import SimConnect -from unittest import TestCase -from unittest.mock import Mock, patch, create_autospec - -import logging - -LOGGER = logging.getLogger(__name__) - - -class sData(dict): - __getattr__ = dict.__getitem__ - __delattr__ = dict.__delitem__ - - def __setattr__(self, key, value): - super(sData, self).__setitem__(key, value) - setattr( - self, - key, - value, - ) - - def __init__(self, data=None): - if data is not None: - try: - for key, value in data.items(): - self[key] = value - except: - pass - - -class TestPlane(TestCase): - def test_init(self): - # SimConnect.Plane() - self.assertTrue(True) - - def test_values(self): - - sm = create_autospec(SimConnect.SimConnect) - - def side_effect(*args, **kwargs): - def val(): - v = 100 - x = 100 - while True: - yield x - x += v - - data = sData() - val = val() - data["Altitude"] = next(val) - data["Latitude"] = next(val) - data["Longitude"] = next(val) - data["Kohlsman"] = next(val) - return data - - sm.get_data.side_effect = side_effect - - pl = SimConnect.Plane(sm=sm) - self.assertEqual(100, pl.altitude) - self.assertEqual(200, pl.latitude) - self.assertEqual(300, pl.longitude) - self.assertEqual(400, pl.kohlsman) diff --git a/tests/test_request.py b/tests/test_request.py deleted file mode 100644 index f81c92da..00000000 --- a/tests/test_request.py +++ /dev/null @@ -1,11 +0,0 @@ -from SimConnect import Request -from unittest import TestCase -from unittest.mock import Mock - -import logging - -LOGGER = logging.getLogger(__name__) - -class TestSimple(TestCase): - def test_init_request(self): - self.assertTrue(True) diff --git a/tests/test_simconnect.py b/tests/test_simconnect.py deleted file mode 100644 index 117ed795..00000000 --- a/tests/test_simconnect.py +++ /dev/null @@ -1,11 +0,0 @@ -from SimConnect import * - -from unittest import TestCase - -import logging - -LOGGER = logging.getLogger(__name__) - -class TestSimple(TestCase): - def test_init_simconnect(self): - self.assertTrue(True) diff --git a/track_data/flight_20260831_040917_021811.csv b/track_data/flight_20260831_040917_021811.csv new file mode 100644 index 00000000..182bb0e8 --- /dev/null +++ b/track_data/flight_20260831_040917_021811.csv @@ -0,0 +1,265 @@ +# aircraft_title=Extra 300S Paint2 +# recorded_at=2026-08-31T04:09:17 +# laps_s=29.889,35.056,37.889,35.279 +timestamp_s,latitude,longitude,altitude_ft,agl_ft,heading_deg,airspeed_kts,groundspeed_kts,pitch_deg,bank_deg,torque,health_pct,lap +0.344,51.18970081,-2.27593398,979.47,613.66,166.12,184.03,182.83,7.51,0.13,1122.72,100.00,1 +0.860,51.18926711,-2.27576361,953.37,583.46,165.75,188.11,185.25,12.51,0.19,1170.26,100.00,1 +1.375,51.18884510,-2.27559748,916.62,543.35,165.87,192.53,187.01,13.57,0.20,1174.28,100.00,1 +1.875,51.18842272,-2.27543035,875.63,498.90,166.00,197.02,191.37,12.91,0.19,1174.77,100.00,1 +2.454,51.18797471,-2.27525429,828.83,447.88,166.02,202.05,196.13,12.16,0.19,1178.12,100.00,1 +2.985,51.18744472,-2.27504261,787.36,405.99,166.03,206.49,201.38,11.35,0.20,1177.62,100.00,1 +3.469,51.18699117,-2.27486297,750.36,368.95,166.03,210.20,205.56,10.51,0.20,1180.87,100.00,1 +3.954,51.18653594,-2.27468239,716.01,334.73,166.03,213.78,209.46,9.60,0.20,1180.14,100.00,1 +4.422,51.18608502,-2.27450375,684.69,303.54,166.08,216.86,212.91,9.61,0.20,1182.99,100.00,1 +4.922,51.18560049,-2.27431095,651.62,270.32,166.44,220.18,216.01,8.76,0.16,1182.29,100.00,1 +5.454,51.18509826,-2.27411194,619.64,237.78,166.43,223.09,219.52,7.66,0.17,1184.66,100.00,1 +5.985,51.18458497,-2.27391049,591.00,201.97,166.38,225.96,222.52,7.73,0.18,1184.00,100.00,1 +6.485,51.18406749,-2.27370773,561.73,172.54,166.37,228.63,224.62,8.26,0.19,1186.16,100.00,1 +6.969,51.18356832,-2.27351306,532.86,143.80,166.58,231.18,227.28,6.94,0.16,1185.60,100.00,1 +7.469,51.18305811,-2.27331403,507.07,118.15,166.50,233.33,229.99,6.43,0.18,1187.85,100.00,1 +8.000,51.18254037,-2.27311292,481.72,91.90,166.46,235.63,231.77,6.68,0.19,1187.39,100.00,1 +8.516,51.18198955,-2.27289962,456.05,63.26,166.46,237.61,234.21,5.11,0.19,1189.38,100.00,1 +9.125,51.18135484,-2.27265564,432.54,39.89,166.44,239.62,236.73,3.28,1.58,1188.89,100.00,1 +9.688,51.18074108,-2.27241572,417.42,24.99,166.25,240.94,238.49,1.53,8.90,1190.65,100.00,1 +10.313,51.18007501,-2.27214907,408.59,16.46,165.11,241.98,239.75,-1.43,19.76,1191.01,100.00,1 +10.922,51.17941712,-2.27184895,415.00,23.82,162.70,241.81,239.24,-5.34,31.56,1191.07,100.00,1 +11.485,51.17882555,-2.27153832,431.14,36.52,160.38,241.49,238.76,-6.25,59.66,1190.65,100.00,1 +11.969,51.17830349,-2.27122692,444.94,43.39,156.91,241.22,238.79,-5.72,86.20,1190.49,100.00,1 +12.422,51.17782719,-2.27090133,453.66,51.87,149.93,240.81,239.01,-3.39,97.48,1189.03,100.00,1 +12.954,51.17736254,-2.27045567,453.69,51.28,122.50,234.03,235.72,-0.74,89.37,1188.86,100.00,1 +13.469,51.17703605,-2.26972690,446.08,43.92,104.76,228.80,228.41,-2.74,73.72,1189.02,100.00,1 +14.079,51.17687862,-2.26873016,447.10,45.03,92.32,227.46,226.24,-4.40,71.04,1189.62,100.00,1 +14.657,51.17685754,-2.26775560,453.48,44.08,87.22,228.07,226.56,-3.88,66.25,1190.06,100.00,1 +15.172,51.17688657,-2.26691981,458.63,62.37,82.31,228.61,227.19,-4.18,66.59,1190.10,100.00,1 +15.672,51.17696468,-2.26605087,465.03,70.29,76.83,228.86,227.36,-4.45,67.48,1189.58,100.00,1 +16.172,51.17708294,-2.26526769,471.52,76.21,72.32,229.28,227.83,-4.38,66.14,1189.55,100.00,1 +16.672,51.17724826,-2.26445708,478.54,53.13,66.88,229.45,227.96,-5.31,63.02,1188.86,100.00,1 +17.172,51.17745057,-2.26371825,488.63,49.10,59.89,229.19,227.63,-7.03,64.38,1188.85,100.00,1 +17.688,51.17771920,-2.26298607,504.62,56.50,49.86,228.18,226.34,-9.14,73.05,1188.06,100.00,1 +18.375,51.17820953,-2.26216928,534.28,58.62,23.37,221.88,220.93,-11.01,90.67,1187.54,100.00,1 +18.891,51.17870351,-2.26179786,556.64,68.56,16.55,220.85,219.18,-7.09,99.26,1187.03,100.00,1 +19.344,51.17916270,-2.26156603,568.55,72.46,18.92,221.50,220.34,-4.84,96.15,1186.06,100.00,1 +19.829,51.17962148,-2.26131190,575.02,78.61,23.56,222.17,221.25,-1.37,73.14,1185.27,100.00,1 +20.391,51.18010737,-2.26099661,574.45,74.58,21.09,223.61,222.59,0.08,23.33,1185.08,100.00,1 +20.985,51.18070085,-2.26062982,569.68,64.94,20.44,225.37,224.23,0.39,-32.01,1184.65,100.00,1 +21.500,51.18120647,-2.26030871,564.63,53.56,23.65,226.66,225.59,-1.11,-30.94,1184.86,100.00,1 +22.079,51.18176646,-2.25991273,564.65,40.15,24.43,227.78,226.70,-3.34,47.81,1184.86,100.00,1 +22.547,51.18223483,-2.25960732,567.74,32.70,15.06,226.48,226.02,-4.69,86.01,577.22,100.00,1 +23.047,51.18272370,-2.25938822,569.79,28.49,9.32,224.63,223.86,-2.79,70.21,571.96,100.00,1 +23.516,51.18321301,-2.25927277,571.20,28.57,358.36,221.41,221.34,-5.71,72.08,574.60,100.00,1 +24.110,51.18378359,-2.25933334,580.99,40.35,335.80,215.43,216.23,-8.11,85.12,1180.27,100.00,1 +24.688,51.18429598,-2.25970428,592.36,58.88,314.05,210.16,211.11,-4.16,97.69,1188.48,100.00,1 +25.297,51.18472926,-2.26036397,590.54,68.92,307.46,210.90,210.38,0.33,93.41,754.82,99.77,1 +25.938,51.18509671,-2.26112849,576.11,65.40,302.47,211.55,210.35,3.32,88.95,809.41,99.93,1 +26.625,51.18544828,-2.26205328,547.22,48.36,289.03,212.44,210.68,4.67,81.24,1184.92,99.61,1 +27.204,51.18565092,-2.26292367,518.78,37.08,279.24,214.26,212.14,3.86,68.93,1178.74,99.29,1 +27.829,51.18574183,-2.26389349,494.06,29.57,269.31,215.30,213.99,1.26,54.94,622.71,99.12,1 +28.407,51.18573416,-2.26478773,481.12,27.06,266.11,215.26,214.09,0.55,29.03,568.78,99.29,1 +29.016,51.18568705,-2.26574972,474.65,32.61,264.63,213.92,212.90,-0.56,-4.68,571.53,99.48,1 +29.641,51.18563164,-2.26671843,471.72,43.62,264.18,214.45,213.35,0.42,-40.21,1181.63,99.26,1 +30.172,51.18558756,-2.26760370,464.55,45.70,267.43,215.87,214.67,0.17,-59.09,1178.78,98.95,1 +30.750,51.18557207,-2.26850276,456.46,47.41,272.59,216.05,214.94,-0.22,-49.14,570.27,99.04,1 +31.282,51.18559702,-2.26932029,451.68,52.49,274.61,214.91,213.84,-0.38,-22.35,571.50,99.20,1 +31.782,51.18563945,-2.27013101,448.93,62.62,275.07,213.93,212.80,0.30,-11.67,571.78,99.35,1 +32.329,51.18568762,-2.27094166,445.78,68.34,275.70,212.72,211.67,-1.00,24.83,572.47,99.50,1 +32.860,51.18573293,-2.27180939,444.19,65.83,272.42,212.65,211.77,-2.78,61.79,1176.79,99.37,1 +33.313,51.18574294,-2.27253718,444.64,64.82,257.54,210.95,211.45,-7.47,75.13,1182.93,99.10,1 +33.813,51.18564560,-2.27327240,452.00,72.25,229.49,203.62,206.51,-6.54,93.84,1177.21,98.83,1 +34.391,51.18531127,-2.27389832,452.00,71.01,195.87,190.07,193.71,-1.60,96.73,1172.61,98.51,1 +34.954,51.18483516,-2.27415957,435.94,50.68,182.88,191.89,191.43,1.25,75.58,1168.32,98.20,1 +35.500,51.18439516,-2.27420256,421.44,32.90,170.59,192.68,192.73,-2.23,64.23,1164.40,97.91,1 +36.110,51.18385291,-2.27405850,415.25,27.15,164.59,194.88,194.38,-1.81,47.81,1160.97,97.57,1 +36.750,51.18329654,-2.27379736,413.09,25.06,162.51,197.75,196.99,-1.27,22.98,1156.15,97.21,1 +37.282,51.18280780,-2.27354151,412.96,24.99,161.32,199.97,199.13,-1.71,15.33,1151.53,96.91,1 +37.907,51.18227806,-2.27324383,415.39,25.44,160.48,201.09,200.22,-2.69,4.53,562.47,96.88,1 +38.500,51.18176200,-2.27295130,421.58,29.88,160.54,199.49,198.57,-4.03,-6.27,553.46,97.07,1 +39.125,51.18123663,-2.27265427,432.37,40.96,161.06,197.99,196.97,-4.70,-19.58,555.54,97.24,1 +39.719,51.18071251,-2.27237994,444.36,52.77,162.16,196.60,195.66,-4.71,-32.49,556.71,97.43,2 +40.282,51.18021912,-2.27213752,454.75,63.12,164.42,195.35,194.49,-4.60,-39.49,556.85,97.60,2 +40.938,51.17966644,-2.27190637,465.39,73.83,165.74,194.01,193.29,-3.21,-28.53,557.83,97.79,2 +41.469,51.17917969,-2.27171954,470.39,78.53,166.48,193.27,192.75,-1.00,3.31,559.40,97.97,2 +42.016,51.17873104,-2.27154371,470.85,72.23,166.47,192.71,192.33,-2.07,55.82,560.56,98.12,2 +42.532,51.17824248,-2.27134421,468.12,65.93,161.97,192.18,191.96,-1.57,84.25,561.91,98.29,2 +43.016,51.17783522,-2.27112478,460.94,58.78,149.36,190.65,190.92,-0.38,87.09,563.17,98.45,2 +43.516,51.17746164,-2.27078009,449.98,47.37,128.34,187.36,188.85,-0.74,82.48,1158.22,98.29,2 +44.094,51.17715665,-2.27020085,437.70,35.43,109.14,185.05,186.03,-2.37,73.89,1168.20,97.99,2 +44.688,51.17696830,-2.26939368,431.32,29.17,100.14,186.96,186.94,-3.17,65.05,1162.49,97.65,2 +45.282,51.17688418,-2.26860199,430.07,24.37,92.39,188.83,188.73,-4.01,64.32,1158.15,97.33,2 +45.750,51.17686553,-2.26792282,430.96,22.72,88.67,190.83,190.48,-3.33,64.68,1154.06,97.06,2 +46.282,51.17688225,-2.26721546,431.80,30.79,81.49,191.81,191.67,-4.46,64.60,618.08,96.94,2 +46.813,51.17695152,-2.26647776,434.62,39.71,77.51,191.03,190.71,-3.91,54.69,557.11,97.09,2 +47.391,51.17706782,-2.26569733,439.13,43.94,72.80,190.17,189.79,-5.86,40.72,555.43,97.26,2 +48.032,51.17724693,-2.26483889,452.67,46.28,68.62,188.37,187.39,-8.15,35.06,555.41,97.47,2 +48.563,51.17742580,-2.26413881,470.19,42.55,66.54,186.72,185.49,-8.21,47.79,556.92,97.64,2 +49.047,51.17760029,-2.26353410,484.87,40.59,64.40,186.34,185.41,-7.78,65.78,1076.12,97.66,2 +49.547,51.17779169,-2.26292572,496.84,45.03,59.31,187.15,186.71,-7.15,77.97,1158.66,97.39,2 +50.032,51.17800665,-2.26234817,505.48,38.44,49.83,187.63,187.77,-6.79,80.87,1153.22,97.13,2 +50.610,51.17832021,-2.26177342,513.72,23.66,26.49,184.90,186.98,-8.07,84.65,1148.07,96.84,2 +51.172,51.17874229,-2.26140448,521.81,19.31,14.34,182.99,183.34,-5.75,86.68,1142.65,96.51,2 +51.641,51.17913830,-2.26123259,525.13,19.41,11.39,185.19,185.24,-3.18,71.41,1138.92,96.24,2 +52.219,51.17958084,-2.26109048,524.55,20.81,9.59,187.94,187.63,-1.10,19.79,1134.26,95.96,2 +52.813,51.18011372,-2.26095602,523.80,23.72,9.31,190.64,190.34,-1.89,-28.71,1129.36,95.63,2 +53.375,51.18061498,-2.26080515,528.91,29.63,17.52,190.53,190.49,-10.42,-38.14,589.78,95.48,2 +53.891,51.18104815,-2.26059785,550.20,49.05,20.09,188.34,186.37,-9.87,-64.02,541.56,95.63,2 +54.469,51.18150640,-2.26031484,573.24,62.99,23.04,186.72,185.53,-8.43,-65.18,541.65,95.81,2 +55.000,51.18192051,-2.26003220,589.30,69.32,25.07,185.36,184.60,-7.03,-26.79,539.56,95.98,2 +55.594,51.18239163,-2.25967419,607.41,77.84,26.02,183.87,183.15,-7.35,52.27,541.21,96.17,2 +56.172,51.18282967,-2.25936010,621.33,80.93,17.54,182.42,182.70,-7.61,93.23,895.68,96.30,2 +56.657,51.18323573,-2.25916176,626.10,81.12,346.58,175.31,180.89,0.57,106.69,1131.93,96.06,2 +57.141,51.18363504,-2.25923759,609.51,66.68,334.74,172.18,171.82,4.23,87.32,1129.67,95.80,2 +57.672,51.18399731,-2.25949392,582.95,44.32,326.13,176.42,174.49,4.34,62.23,1125.46,95.53,2 +58.344,51.18442580,-2.25997627,559.52,30.70,313.79,177.92,178.28,-1.46,50.18,1122.25,95.18,2 +58.907,51.18476202,-2.26055239,552.80,34.12,310.44,181.22,181.24,-1.20,44.54,1117.89,94.88,2 +59.485,51.18507271,-2.26115533,547.70,37.94,308.17,184.20,184.10,-0.43,62.68,1114.44,94.57,2 +60.079,51.18537812,-2.26180604,538.70,36.68,301.74,185.72,185.68,-0.41,81.05,541.46,94.56,2 +60.735,51.18564996,-2.26254321,523.73,34.22,281.36,182.52,183.38,1.84,90.29,529.94,94.75,2 +61.297,51.18576665,-2.26328859,502.43,27.47,270.69,181.76,180.78,3.41,70.34,532.69,94.93,2 +62.094,51.18576713,-2.26430159,476.46,17.17,264.08,182.42,181.70,1.38,23.00,534.99,95.15,2 +62.875,51.18569140,-2.26534763,465.29,18.16,262.67,184.60,184.12,0.22,-18.69,1106.40,95.13,2 +63.657,51.18561450,-2.26641758,457.59,25.44,265.60,188.23,187.61,0.56,-46.71,1120.77,94.70,2 +64.422,51.18557357,-2.26750713,448.23,28.63,270.35,191.66,191.09,-0.63,-55.91,1113.50,94.25,2 +65.079,51.18559024,-2.26842755,441.69,31.95,275.60,194.06,193.49,-0.72,-56.20,923.49,93.88,2 +65.844,51.18566892,-2.26948793,433.69,37.06,278.63,193.99,193.33,0.07,19.86,530.61,94.07,2 +66.454,51.18574806,-2.27039170,427.07,45.23,276.55,193.49,192.96,-0.38,73.38,531.69,94.28,2 +67.032,51.18579998,-2.27123436,415.61,40.83,271.11,193.48,192.68,0.47,64.55,531.96,94.47,2 +67.610,51.18580257,-2.27204309,404.58,24.62,259.10,193.05,193.04,-3.32,65.02,1081.31,94.45,2 +68.188,51.18569611,-2.27285535,405.35,25.74,239.28,189.60,190.67,-10.16,68.04,1114.52,94.12,2 +68.844,51.18539858,-2.27360699,427.49,48.41,211.49,183.48,184.29,-14.72,85.70,1109.02,93.77,2 +69.516,51.18493711,-2.27405354,453.99,71.64,183.64,177.14,178.65,-9.44,99.55,1102.72,93.42,2 +70.032,51.18447645,-2.27413876,462.81,74.85,168.80,175.42,176.85,-4.27,96.77,667.24,93.16,2 +70.532,51.18408240,-2.27404934,460.17,71.97,167.41,175.81,175.86,-0.82,75.37,526.59,93.30,2 +71.032,51.18368454,-2.27390882,452.35,64.01,165.36,176.86,176.46,1.12,44.73,522.04,93.46,2 +71.579,51.18324829,-2.27371596,442.44,54.13,163.49,177.28,176.83,1.41,7.97,522.81,93.63,2 +72.063,51.18286514,-2.27353240,435.40,47.16,163.14,177.61,177.22,0.38,-4.30,525.61,93.78,2 +72.579,51.18246980,-2.27334289,430.96,42.14,163.45,177.98,177.70,-0.57,-5.76,526.87,93.94,2 +73.141,51.18202124,-2.27313332,428.76,37.18,163.68,178.05,177.77,-1.38,16.17,527.75,94.12,2 +73.657,51.18161592,-2.27293648,429.02,37.11,162.23,177.71,177.54,-3.39,20.92,529.50,94.27,2 +74.204,51.18119027,-2.27270800,433.43,41.63,161.13,177.46,177.11,-3.68,-7.59,531.18,94.44,2 +74.782,51.18073164,-2.27246300,440.45,48.67,161.69,177.01,176.66,-4.10,-28.46,532.31,94.62,3 +75.282,51.18034762,-2.27227162,445.78,53.95,162.31,176.70,176.42,-2.87,-28.36,533.21,94.77,3 +75.782,51.17995497,-2.27208547,448.30,56.34,163.12,176.67,176.46,-1.53,-19.47,534.59,94.93,3 +76.297,51.17955747,-2.27190496,448.34,56.33,164.12,176.79,176.58,-1.33,-2.13,535.64,95.08,3 +76.844,51.17912055,-2.27170950,448.60,56.40,164.37,176.82,176.62,-1.90,16.79,537.00,95.25,3 +77.344,51.17872661,-2.27152758,449.48,51.30,163.51,176.77,176.63,-2.32,38.01,538.36,95.41,3 +77.829,51.17833992,-2.27133112,449.44,47.40,161.65,176.77,176.69,-2.23,55.60,539.67,95.56,3 +78.329,51.17796057,-2.27111552,447.26,45.16,155.74,176.53,176.75,-3.41,66.34,540.96,95.73,3 +78.797,51.17760563,-2.27085748,445.64,43.64,144.37,175.03,175.98,-5.96,70.99,719.91,95.87,3 +79.360,51.17726593,-2.27044983,448.96,47.23,127.98,173.42,174.99,-9.87,76.52,1131.05,95.63,3 +79.985,51.17696564,-2.26978557,459.27,57.55,95.83,165.67,170.05,-11.54,91.38,1130.47,95.29,3 +80.485,51.17688570,-2.26916518,464.79,62.88,87.18,165.77,167.56,-7.88,92.95,1125.28,94.99,3 +80.938,51.17688928,-2.26859587,464.73,58.76,86.52,169.18,169.65,-3.27,82.20,1121.25,94.71,3 +81.454,51.17691357,-2.26797648,459.62,55.92,84.50,172.01,171.91,-0.13,54.51,592.43,94.60,3 +82.016,51.17696175,-2.26729791,451.12,52.71,81.99,172.26,172.02,0.55,28.68,533.53,94.76,3 +82.579,51.17703446,-2.26655123,443.52,48.30,80.39,173.06,172.87,-0.54,10.25,536.86,94.93,3 +83.110,51.17710796,-2.26589178,442.40,46.51,79.39,172.91,173.01,-5.83,11.18,536.30,95.10,3 +83.672,51.17719579,-2.26519493,452.23,54.99,78.17,172.40,171.91,-6.05,35.74,537.03,95.27,3 +84.204,51.17729184,-2.26453605,462.85,40.34,74.09,171.40,171.16,-8.70,44.94,538.83,95.42,3 +84.797,51.17743615,-2.26380980,481.06,42.64,66.83,169.55,168.59,-12.59,50.05,540.39,95.61,3 +85.360,51.17761378,-2.26318316,504.72,56.50,59.72,167.73,166.15,-12.85,74.71,540.81,95.79,3 +85.875,51.17780781,-2.26265531,525.15,72.06,51.22,167.48,166.68,-10.31,93.23,1093.86,95.74,3 +86.469,51.17808620,-2.26209612,540.43,67.28,37.55,167.47,168.32,-6.15,97.77,1129.37,95.44,3 +86.985,51.17841017,-2.26166268,543.17,48.12,29.22,168.99,169.79,-2.36,93.34,1123.81,95.11,3 +87.579,51.17881200,-2.26130279,535.90,29.25,12.62,169.59,171.16,-2.27,74.96,1118.46,94.79,3 +88.141,51.17920738,-2.26113418,530.25,22.23,7.78,171.65,172.26,-4.36,31.31,1114.58,94.52,3 +88.719,51.17968667,-2.26103794,534.00,30.25,7.10,172.98,173.01,-3.93,-9.42,543.82,94.53,3 +89.329,51.18017519,-2.26094475,541.55,42.03,7.80,172.72,172.70,-3.88,-51.29,529.90,94.72,3 +89.891,51.18061315,-2.26083841,545.42,46.70,10.16,172.17,172.51,-4.32,-67.85,531.55,94.90,3 +90.469,51.18108505,-2.26068444,544.78,43.97,15.08,171.76,172.51,-4.74,-72.20,533.62,95.09,3 +90.938,51.18145088,-2.26051897,542.00,36.69,18.18,169.90,172.49,-9.47,-71.82,535.49,95.24,3 +91.438,51.18182054,-2.26030163,538.90,25.26,20.06,167.76,171.73,-11.32,-71.01,535.89,95.41,3 +91.938,51.18218422,-2.26004785,536.22,13.02,17.94,160.42,170.33,-19.66,-51.65,537.96,95.57,3 +92.438,51.18253377,-2.25974694,543.40,10.44,16.62,156.87,165.96,-16.36,-31.55,1082.96,95.55,3 +93.000,51.18287570,-2.25942147,559.32,18.84,24.14,165.57,165.82,-7.93,28.26,1127.14,95.27,3 +93.454,51.18321447,-2.25912909,576.76,33.33,22.50,167.74,166.92,-8.62,90.72,1121.84,94.98,3 +93.907,51.18354138,-2.25891991,588.04,40.08,353.75,161.55,166.30,-5.89,94.59,1116.98,94.71,3 +94.516,51.18394386,-2.25895828,596.84,52.98,326.70,149.51,152.99,-16.94,23.74,1112.23,94.41,3 +95.079,51.18431793,-2.25922034,632.11,90.74,331.88,151.85,146.50,-17.41,13.97,1107.60,94.09,3 +95.579,51.18463459,-2.25947169,668.33,129.38,332.25,152.34,147.54,-17.15,84.06,1101.96,93.82,3 +96.172,51.18498363,-2.25977154,701.91,166.94,316.73,151.40,151.21,-7.05,132.64,1097.13,93.51,3 +96.813,51.18532228,-2.26022499,703.48,179.11,294.75,147.88,150.04,4.57,93.66,1089.92,93.19,3 +97.422,51.18552215,-2.26079360,679.91,162.32,287.79,152.26,150.77,7.14,90.43,1085.77,92.88,3 +98.000,51.18565099,-2.26142279,646.13,136.10,277.99,156.91,153.54,9.81,77.49,1082.07,92.56,3 +98.657,51.18572291,-2.26216464,600.80,101.50,272.46,163.63,158.72,10.21,58.12,1079.14,92.21,3 +99.266,51.18573606,-2.26286571,560.71,77.26,266.02,168.47,165.05,8.37,39.43,1075.22,91.90,3 +99.969,51.18568506,-2.26375644,521.38,54.61,262.95,174.60,172.36,6.90,0.44,1072.30,91.50,3 +100.563,51.18562659,-2.26452349,497.34,39.74,265.09,178.56,177.41,2.03,-31.90,1067.51,91.17,3 +101.141,51.18559168,-2.26529225,485.53,37.46,270.32,180.90,180.84,-1.60,-36.33,1064.34,90.83,3 +101.719,51.18559592,-2.26605977,482.90,45.44,271.45,183.55,183.22,-0.75,-21.35,1060.12,90.51,3 +102.329,51.18561574,-2.26687352,479.61,53.05,271.48,186.23,185.72,2.30,-17.63,1055.04,90.17,3 +102.907,51.18563766,-2.26769840,467.20,48.84,271.71,189.27,188.10,3.88,-24.56,1049.89,89.83,3 +103.485,51.18566225,-2.26851846,449.92,40.12,273.19,192.36,190.98,3.81,-24.27,1044.93,89.51,3 +104.110,51.18570160,-2.26938878,431.37,32.57,274.66,195.44,194.03,3.69,-11.29,1040.91,89.18,3 +104.688,51.18574702,-2.27022192,416.14,31.33,274.23,197.81,196.82,-0.30,28.24,1037.20,88.88,3 +105.204,51.18577300,-2.27099358,411.38,35.33,269.39,198.86,198.39,-3.73,50.79,1033.22,88.59,3 +105.735,51.18575778,-2.27176445,414.57,38.16,262.52,199.33,198.79,-5.82,68.18,1029.24,88.30,3 +106.266,51.18568498,-2.27254102,421.51,41.76,247.78,198.26,198.60,-7.40,80.83,1024.79,87.99,3 +106.766,51.18552221,-2.27320204,429.61,49.99,221.05,191.48,194.49,-8.46,87.49,1020.24,87.70,3 +107.266,51.18520254,-2.27368993,437.52,56.82,199.19,184.06,186.10,-7.23,83.91,1016.53,87.42,3 +107.860,51.18472976,-2.27396895,444.98,58.52,182.57,182.96,183.96,-7.56,80.64,1011.60,87.08,3 +108.360,51.18429995,-2.27401224,452.46,64.59,168.91,181.37,182.10,-7.15,82.42,1007.69,86.79,3 +108.844,51.18388943,-2.27390661,458.29,70.41,167.07,182.92,182.81,-4.22,83.09,1003.26,86.51,3 +109.329,51.18349770,-2.27376550,459.08,71.01,165.09,184.55,184.40,-2.01,74.06,999.17,86.22,3 +109.875,51.18307770,-2.27357591,454.92,66.73,163.06,186.63,186.16,0.35,35.59,994.78,85.94,3 +110.391,51.18263731,-2.27335393,448.70,60.44,161.69,188.71,188.14,0.71,25.05,990.33,85.64,3 +110.969,51.18215800,-2.27308946,441.87,50.89,160.53,190.81,190.21,0.33,13.97,986.13,85.33,3 +111.547,51.18167229,-2.27280358,436.87,44.74,159.79,192.76,192.13,-0.27,3.88,981.29,85.00,3 +112.047,51.18125000,-2.27255700,434.54,42.49,159.64,194.18,193.55,-0.86,-9.13,977.47,84.71,3 +112.579,51.18081211,-2.27230049,433.85,41.85,160.18,195.49,194.84,-1.41,-21.74,973.18,84.42,4 +113.157,51.18032010,-2.27202875,434.21,42.23,161.59,196.80,196.13,-1.64,-28.09,968.63,84.11,4 +113.657,51.17988279,-2.27180498,434.88,42.91,163.07,197.89,197.19,-1.70,-28.81,964.58,83.83,4 +114.188,51.17941890,-2.27158962,435.91,43.96,164.40,198.92,198.19,-1.93,-22.52,960.38,83.54,4 +114.750,51.17890375,-2.27136997,438.35,42.44,165.56,199.91,199.14,-2.42,-7.31,955.50,83.22,4 +115.250,51.17846417,-2.27119177,442.46,40.66,165.92,200.60,199.75,-3.15,10.16,951.35,82.94,4 +115.735,51.17802543,-2.27101153,448.24,46.46,165.39,201.07,200.25,-3.98,48.44,947.32,82.67,4 +116.204,51.17760134,-2.27082231,453.24,51.45,152.27,200.08,200.50,-8.27,75.67,943.18,82.39,4 +116.829,51.17716123,-2.27036285,469.04,67.60,102.43,174.33,183.57,-19.31,98.70,938.44,82.07,4 +117.375,51.17700915,-2.26966267,482.71,81.10,88.00,168.71,171.79,-12.65,84.18,934.72,81.72,4 +117.813,51.17700934,-2.26909970,489.74,87.83,90.58,172.21,172.60,-6.08,73.97,930.85,81.45,4 +118.297,51.17701299,-2.26846883,491.07,89.08,92.86,174.18,174.20,0.08,51.45,926.51,81.15,4 +118.782,51.17700616,-2.26786304,485.07,85.81,90.67,176.20,175.92,0.84,46.11,922.66,80.89,4 +119.297,51.17700868,-2.26722398,476.21,79.04,88.36,178.25,177.82,0.97,45.93,918.58,80.62,4 +119.922,51.17703955,-2.26638524,466.06,70.90,81.45,179.95,180.05,-3.00,46.49,914.23,80.29,4 +120.454,51.17711467,-2.26566643,465.95,69.93,76.17,180.81,180.86,-5.08,47.40,909.88,79.97,4 +121.000,51.17722738,-2.26498416,471.81,69.11,71.75,181.50,181.33,-6.34,48.38,905.73,79.68,4 +121.579,51.17739123,-2.26423426,481.81,50.68,68.72,182.32,181.91,-5.84,49.96,900.81,79.35,4 +122.172,51.17757829,-2.26350603,491.79,49.47,64.04,182.82,182.51,-6.82,58.94,895.60,79.02,4 +122.672,51.17777198,-2.26290965,500.72,47.72,55.14,182.61,182.82,-7.60,78.78,891.44,78.74,4 +123.454,51.17814629,-2.26214543,513.60,37.61,27.26,177.45,179.28,-8.28,84.85,885.07,78.34,4 +124.219,51.17869663,-2.26168012,524.18,29.27,21.27,177.93,177.95,-4.52,60.03,878.15,77.91,4 +125.047,51.17931894,-2.26132026,529.33,28.98,18.16,179.75,179.72,-2.51,37.48,871.18,77.45,4 +125.829,51.17997213,-2.26100168,529.49,28.62,16.45,181.87,181.75,-0.86,12.54,864.46,76.99,4 +126.594,51.18058755,-2.26072155,528.14,24.99,16.07,183.64,183.51,-1.43,-15.03,857.95,76.55,4 +127.297,51.18116669,-2.26044827,528.68,22.43,17.27,185.08,184.89,-1.79,-23.53,851.98,76.13,4 +127.985,51.18173383,-2.26014947,532.68,15.69,20.36,185.77,185.57,-5.68,-9.09,846.06,75.72,4 +128.657,51.18227360,-2.25983458,547.89,18.77,19.74,186.02,185.34,-7.13,33.37,840.32,75.34,4 +129.375,51.18286629,-2.25953154,569.09,31.36,14.97,185.54,184.76,-8.79,68.57,833.47,74.92,4 +130.047,51.18342333,-2.25932268,586.05,45.48,0.29,184.15,184.82,-6.66,95.95,827.46,74.54,4 +130.688,51.18398369,-2.25932836,589.39,49.05,341.06,181.25,182.53,-1.88,91.78,821.60,74.17,4 +131.329,51.18448364,-2.25960884,579.37,43.46,314.69,176.15,178.64,-0.91,85.66,816.57,73.84,4 +131.907,51.18483136,-2.26011913,565.92,39.99,305.07,176.13,176.32,0.62,71.59,811.67,73.52,4 +132.594,51.18515136,-2.26086516,547.80,33.23,298.94,178.65,178.39,0.82,58.22,806.97,73.14,4 +133.266,51.18540434,-2.26162387,532.21,27.75,292.10,180.52,180.30,0.19,65.52,802.53,72.77,4 +133.954,51.18561219,-2.26250735,517.36,27.23,278.00,181.14,181.53,-1.50,73.40,796.98,72.38,4 +134.547,51.18568894,-2.26328960,507.17,32.85,272.16,181.97,181.80,0.42,70.72,792.89,72.04,4 +135.157,51.18570328,-2.26409983,493.49,31.53,269.41,184.01,183.25,2.46,41.82,788.41,71.71,4 +135.875,51.18568608,-2.26506736,475.78,24.58,267.23,186.30,185.43,2.61,6.94,782.60,71.30,4 +136.469,51.18565964,-2.26587757,463.27,22.89,266.94,187.87,187.13,1.91,-12.59,778.52,70.96,4 +137.172,51.18563347,-2.26683459,451.32,24.26,268.23,189.41,188.72,1.19,-27.30,773.19,70.57,4 +137.829,51.18562806,-2.26779098,445.05,28.33,273.09,189.81,189.56,-3.54,-29.88,767.65,70.17,4 +138.422,51.18565755,-2.26861388,450.17,41.87,274.86,190.16,189.58,-3.01,-27.29,763.04,69.84,4 +139.016,51.18570319,-2.26943122,454.69,56.83,275.80,190.52,189.99,-0.39,7.68,758.18,69.51,4 +139.594,51.18575370,-2.27028915,452.82,69.28,275.55,191.05,190.66,-1.20,55.58,752.72,69.16,4 +140.172,51.18578971,-2.27110854,446.60,71.23,269.57,191.50,191.23,-1.52,69.41,748.17,68.85,4 +140.735,51.18577799,-2.27190018,439.83,60.86,259.46,191.14,191.18,-2.29,75.46,743.53,68.53,4 +141.219,51.18569943,-2.27258422,434.72,54.53,246.44,189.86,190.37,-2.95,79.59,739.84,68.25,4 +141.750,51.18552556,-2.27322726,430.71,50.62,220.77,183.55,186.53,-5.28,83.49,735.70,67.96,4 +142.282,51.18520174,-2.27370717,429.29,47.33,191.98,173.46,177.55,-6.28,86.72,731.71,67.67,4 +142.907,51.18472872,-2.27391351,427.30,39.66,183.69,173.29,173.62,-3.32,74.44,726.62,67.34,4 +143.500,51.18425192,-2.27394225,424.46,36.43,170.86,172.51,173.24,-4.44,68.24,721.98,67.00,4 +144.110,51.18377536,-2.27382157,424.12,36.13,167.22,173.69,173.71,-3.19,43.70,716.97,66.67,4 +144.625,51.18335416,-2.27365973,424.81,36.84,165.52,174.70,174.53,-2.40,32.30,712.72,66.37,4 +145.172,51.18292687,-2.27346841,425.46,37.48,163.75,175.61,175.43,-2.19,30.13,708.29,66.07,4 +145.813,51.18245899,-2.27322995,426.16,37.18,161.93,176.59,176.35,-2.14,20.46,703.52,65.74,4 +146.313,51.18205220,-2.27301295,427.50,36.33,160.82,177.31,177.03,-2.34,11.66,699.27,65.45,4 +146.797,51.18167372,-2.27279774,429.88,38.00,160.19,177.87,177.56,-2.68,2.83,695.44,65.19,4 +147.391,51.18120725,-2.27252770,434.39,42.57,160.03,178.40,178.04,-3.18,-3.27,690.49,64.87,4 diff --git a/track_data/flight_20260831_041652_021214.csv b/track_data/flight_20260831_041652_021214.csv new file mode 100644 index 00000000..963d367c --- /dev/null +++ b/track_data/flight_20260831_041652_021214.csv @@ -0,0 +1,240 @@ +# aircraft_title=Extra 300S Paint2 +# recorded_at=2026-08-31T04:16:52 +# laps_s=29.881,35.039,33.734,33.489 +timestamp_s,latitude,longitude,altitude_ft,agl_ft,heading_deg,airspeed_kts,groundspeed_kts,pitch_deg,bank_deg,torque,health_pct,lap +0.360,51.18973107,-2.27594496,981.31,617.58,166.24,183.70,182.69,5.71,0.11,1101.63,100.00,1 +0.953,51.18925214,-2.27575568,958.49,589.29,165.99,187.70,186.68,8.45,0.16,1170.31,100.00,1 +1.500,51.18877597,-2.27557070,928.74,555.31,166.05,192.10,189.18,10.33,0.17,1174.09,100.00,1 +2.094,51.18826153,-2.27536857,888.07,510.39,166.01,197.20,192.30,14.26,0.18,1174.58,100.00,1 +2.828,51.18763672,-2.27512001,821.58,439.94,166.38,203.97,195.28,16.58,0.19,1176.84,100.00,1 +3.391,51.18712994,-2.27492349,764.22,381.26,166.53,209.32,200.16,15.57,0.18,1180.07,100.00,1 +3.985,51.18657524,-2.27470646,703.06,321.09,166.50,214.78,206.21,14.41,0.19,1179.42,100.00,1 +4.578,51.18601331,-2.27448851,646.43,264.79,166.60,219.42,211.81,13.14,0.19,1183.39,100.00,1 +5.172,51.18543738,-2.27426534,593.11,211.47,166.89,223.91,217.02,11.77,-0.07,1184.04,100.00,1 +5.766,51.18485564,-2.27404232,544.73,157.19,166.88,227.65,221.78,10.26,-2.16,1186.15,100.00,1 +6.297,51.18432507,-2.27384128,506.37,117.12,166.89,230.84,225.65,8.79,-2.23,1188.42,100.00,1 +6.875,51.18373391,-2.27362114,470.02,81.07,167.07,233.58,229.44,6.69,-2.23,1188.70,100.00,1 +7.406,51.18318136,-2.27341969,443.26,54.43,167.25,235.79,232.58,4.01,2.71,1190.86,100.00,1 +8.031,51.18254477,-2.27318312,425.94,37.08,166.87,237.23,235.14,-0.38,4.74,1190.50,100.00,1 +8.563,51.18196319,-2.27296195,424.13,32.24,166.45,238.02,235.98,-1.90,5.58,1191.85,100.00,1 +9.125,51.18138540,-2.27273309,429.18,37.50,166.04,238.41,236.26,-3.53,13.12,1191.44,100.00,1 +9.688,51.18077307,-2.27248349,440.63,49.17,164.96,238.54,236.12,-5.12,24.99,1190.85,100.00,1 +10.313,51.18012294,-2.27218682,457.96,66.50,164.44,238.49,236.02,-3.13,27.45,1190.02,100.00,1 +10.922,51.17949892,-2.27189897,465.37,73.55,164.08,238.97,237.00,-2.08,41.98,1189.32,100.00,1 +11.469,51.17888993,-2.27160893,467.57,71.80,161.41,239.34,237.58,-2.46,74.54,1188.26,100.00,1 +12.078,51.17825335,-2.27124991,465.38,63.19,157.14,240.11,238.26,-0.56,86.10,1188.02,100.00,1 +12.641,51.17768891,-2.27084677,456.79,54.44,140.90,238.87,237.94,-0.05,86.95,1188.08,100.00,1 +13.203,51.17725054,-2.27025958,444.75,42.29,112.91,231.55,232.33,-0.51,85.30,1188.18,100.00,1 +13.844,51.17698906,-2.26924916,433.26,31.15,92.51,226.75,226.19,-3.66,63.91,1189.67,100.00,1 +14.360,51.17695621,-2.26833592,435.36,32.04,88.29,227.37,225.85,-3.51,58.69,1190.60,100.00,1 +14.906,51.17697801,-2.26749950,439.49,41.77,85.65,228.25,226.68,-3.09,55.28,1191.07,100.00,1 +15.531,51.17704184,-2.26643656,444.27,49.50,81.78,229.07,227.53,-4.06,48.09,1190.56,100.00,1 +16.094,51.17713941,-2.26546830,452.23,55.82,76.97,229.55,227.92,-6.32,47.79,1190.39,100.00,1 +16.672,51.17729428,-2.26452287,471.22,46.22,70.13,228.75,225.94,-9.58,56.75,1189.84,100.00,1 +17.235,51.17750561,-2.26364220,497.83,61.42,63.45,227.98,225.26,-9.51,89.60,1189.85,100.00,1 +17.766,51.17773463,-2.26290543,517.28,66.03,50.90,226.72,225.32,-6.67,93.78,1187.93,100.00,1 +18.469,51.17820820,-2.26205036,531.52,54.61,28.88,222.47,222.46,-5.25,85.32,1187.05,100.00,1 +19.078,51.17878216,-2.26154567,539.00,40.56,18.73,222.09,221.32,-5.19,73.36,1186.72,100.00,1 +19.578,51.17928135,-2.26128090,544.96,43.01,15.79,222.88,221.92,-4.14,68.71,1186.32,100.00,1 +20.110,51.17975444,-2.26107635,548.80,47.21,14.94,223.89,222.87,-2.12,29.58,1186.33,100.00,1 +20.703,51.18036272,-2.26083548,549.63,49.10,14.32,225.50,224.32,-1.17,-41.05,1185.86,100.00,1 +21.328,51.18101751,-2.26054843,550.55,47.03,20.37,226.41,225.38,-3.09,-51.15,1185.76,100.00,1 +21.891,51.18155075,-2.26023041,556.01,42.96,22.27,227.32,226.01,-3.19,-28.53,1185.73,100.00,1 +22.469,51.18212384,-2.25984966,564.65,38.21,23.50,228.13,226.72,-3.19,12.78,1185.45,100.00,1 +23.000,51.18264128,-2.25950084,573.07,35.39,22.53,228.71,227.43,-4.36,60.60,1185.13,100.00,1 +23.563,51.18320213,-2.25917085,581.17,36.67,9.98,227.86,227.37,-6.47,80.61,1184.44,100.00,1 +24.078,51.18371319,-2.25905024,589.99,45.66,335.31,216.93,221.88,-6.18,95.63,1184.35,100.00,1 +24.672,51.18427837,-2.25940178,589.02,49.86,322.86,213.53,213.42,-1.01,92.49,1184.57,100.00,1 +25.203,51.18469350,-2.25989075,578.40,48.13,312.10,214.65,214.33,0.58,83.83,1182.37,99.80,1 +25.844,51.18509493,-2.26058953,561.93,43.63,302.97,216.22,215.17,1.18,77.48,1178.77,99.49,1 +26.375,51.18539253,-2.26133519,545.67,37.15,293.16,217.49,216.34,1.35,77.46,1174.71,99.21,1 +27.031,51.18563792,-2.26233229,526.70,32.75,279.18,218.27,217.48,0.22,74.99,1171.48,98.88,1 +27.563,51.18572701,-2.26318691,514.33,38.16,269.99,219.17,218.22,0.09,75.22,1168.14,98.59,1 +28.172,51.18572433,-2.26416839,500.76,39.76,265.44,219.84,218.53,1.10,61.55,565.66,98.63,1 +28.735,51.18567335,-2.26504757,488.09,36.97,263.22,219.21,217.89,0.91,11.05,566.61,98.79,1 +29.328,51.18559939,-2.26597937,481.06,42.62,263.79,217.80,216.71,-0.27,-38.06,567.00,98.97,1 +29.875,51.18554279,-2.26684656,477.24,50.76,266.72,216.34,215.27,-0.26,-67.83,568.91,99.14,1 +30.375,51.18551610,-2.26766771,471.18,53.36,269.72,215.15,214.00,0.60,-54.00,570.38,99.30,1 +31.031,51.18552168,-2.26870141,461.07,54.37,273.56,214.01,212.71,2.33,-61.52,572.30,99.49,1 +31.578,51.18555306,-2.26951301,449.89,53.05,276.02,213.33,211.86,2.44,-21.59,573.68,99.65,1 +32.110,51.18560586,-2.27036847,439.06,56.08,276.39,212.48,211.26,0.93,25.46,575.12,99.80,1 +32.703,51.18565818,-2.27130760,429.59,52.50,273.53,211.55,210.42,-0.04,75.21,630.29,99.98,1 +33.297,51.18567047,-2.27222851,417.30,36.91,255.56,209.98,210.07,-1.05,80.37,1191.23,99.72,1 +33.875,51.18553395,-2.27308967,407.70,27.56,231.77,206.03,207.80,-5.43,76.53,1187.87,99.41,1 +34.453,51.18519707,-2.27375128,413.00,32.22,200.65,194.91,197.23,-11.26,78.23,1182.45,99.10,1 +34.985,51.18476830,-2.27403606,430.29,47.32,185.87,193.10,192.53,-10.70,81.44,1178.86,98.82,1 +35.469,51.18432739,-2.27411925,446.93,59.83,173.79,192.34,191.76,-9.93,84.16,1175.18,98.55,1 +36.000,51.18389833,-2.27405870,463.30,76.09,164.50,192.06,191.44,-7.88,87.90,628.51,98.43,1 +36.610,51.18335878,-2.27383385,475.37,87.61,160.11,190.73,190.45,-4.79,89.72,568.07,98.61,1 +37.172,51.18286912,-2.27355517,477.96,89.90,157.27,190.35,190.17,-1.90,89.56,567.81,98.79,1 +37.688,51.18246185,-2.27328833,472.88,83.53,158.42,190.42,189.91,1.39,70.00,566.62,98.94,1 +38.360,51.18193874,-2.27294586,455.57,62.88,157.54,191.17,189.82,4.06,-8.20,568.41,99.12,1 +38.953,51.18144921,-2.27262543,437.12,44.58,158.79,191.82,190.46,3.68,-50.45,570.89,99.32,1 +39.610,51.18091076,-2.27231757,418.10,25.81,163.97,192.07,191.30,0.35,-13.99,572.75,99.50,2 +40.235,51.18037241,-2.27206901,412.46,20.40,164.43,191.94,191.35,-1.06,-6.13,575.31,99.70,2 +40.797,51.17989380,-2.27185642,412.39,20.44,165.07,191.61,191.00,-1.90,-8.48,576.50,99.85,2 +41.422,51.17934312,-2.27162186,415.68,23.85,165.63,191.00,190.36,-2.91,9.32,577.81,100.00,2 +41.985,51.17888924,-2.27142779,420.88,24.45,164.74,190.29,189.71,-3.80,34.36,577.73,100.00,2 +42.594,51.17835431,-2.27117595,426.58,24.71,161.80,189.99,189.58,-4.16,63.86,1025.08,99.97,2 +43.125,51.17789735,-2.27092079,428.65,26.64,157.05,191.52,191.19,-3.23,80.04,1193.47,99.69,2 +43.625,51.17750859,-2.27064410,427.30,25.23,139.69,190.73,191.92,-4.30,83.59,1188.81,99.42,2 +44.172,51.17715962,-2.27016091,426.35,24.37,109.51,181.68,185.43,-6.59,84.10,1183.79,99.13,2 +44.703,51.17697135,-2.26947497,427.51,25.56,100.39,183.24,183.64,-4.99,73.40,1179.38,98.82,2 +45.297,51.17687618,-2.26868796,428.90,25.07,93.77,185.82,185.79,-4.35,68.18,1174.77,98.50,2 +45.891,51.17684714,-2.26787070,430.90,20.59,86.56,187.92,187.79,-4.73,64.78,1170.05,98.18,2 +46.516,51.17688954,-2.26700073,435.14,38.21,81.04,188.96,188.64,-4.43,56.04,569.19,98.19,2 +47.078,51.17697445,-2.26622356,439.43,44.58,77.94,187.97,187.60,-4.14,45.15,562.98,98.35,2 +47.703,51.17709533,-2.26541038,446.71,51.18,72.21,186.43,186.06,-7.45,43.20,565.02,98.52,2 +48.328,51.17727430,-2.26459203,463.05,50.73,68.76,185.06,184.07,-7.69,47.00,567.00,98.71,2 +48.860,51.17744633,-2.26392656,477.21,40.41,66.35,184.04,183.35,-7.17,60.52,567.43,98.86,2 +49.344,51.17762253,-2.26331587,487.59,40.71,63.21,183.72,183.32,-6.37,73.36,1037.15,98.93,2 +49.891,51.17782921,-2.26269139,495.38,39.49,47.98,183.31,184.16,-7.63,81.15,1175.85,98.68,2 +50.563,51.17820938,-2.26204247,505.09,26.52,31.29,182.40,182.87,-6.34,84.17,1170.22,98.31,2 +51.125,51.17862356,-2.26164060,510.40,13.77,16.19,182.28,183.25,-5.43,83.17,1164.44,97.99,2 +51.703,51.17906204,-2.26141678,512.68,11.94,13.31,184.70,184.70,-3.24,43.08,1160.00,97.70,2 +52.360,51.17963168,-2.26122009,516.03,17.31,12.14,186.64,186.40,-3.78,-11.46,565.64,97.67,2 +52.953,51.18012418,-2.26104899,525.48,28.65,14.95,185.18,184.70,-6.64,-45.63,556.49,97.84,2 +53.500,51.18059228,-2.26083931,539.37,41.37,21.34,183.39,182.79,-8.52,-55.67,558.70,98.03,2 +54.125,51.18107002,-2.26053630,558.55,55.42,24.44,181.98,181.07,-7.53,-19.81,560.67,98.21,2 +54.703,51.18149734,-2.26022571,575.33,64.22,25.93,181.15,180.45,-4.60,36.89,561.12,98.38,2 +55.281,51.18196874,-2.25988172,585.52,59.49,23.36,180.27,180.42,-5.30,64.49,561.20,98.56,2 +55.844,51.18241421,-2.25959730,590.29,54.14,17.03,179.47,179.96,-4.55,83.44,562.75,98.74,2 +56.391,51.18282998,-2.25939898,590.09,48.74,7.49,180.09,180.84,-2.63,87.09,1148.74,98.66,2 +56.953,51.18329940,-2.25929401,583.99,41.71,356.24,181.78,182.38,-1.14,83.10,1167.92,98.36,2 +57.485,51.18374813,-2.25933818,574.80,33.93,340.37,181.49,182.67,-1.49,79.76,1162.97,98.09,2 +58.125,51.18424455,-2.25961650,563.74,28.05,325.44,182.96,183.83,-2.01,75.07,1158.64,97.77,2 +58.672,51.18464334,-2.26004124,556.31,28.60,314.86,184.40,184.94,-2.14,75.02,1154.40,97.48,2 +59.235,51.18497429,-2.26056646,549.86,31.42,304.85,185.85,186.31,-2.09,76.05,611.31,97.35,2 +59.781,51.18525693,-2.26120744,543.23,33.58,294.93,184.49,184.90,-1.80,77.03,555.66,97.51,2 +60.469,51.18549602,-2.26206517,533.33,34.85,284.10,183.80,184.03,-0.73,78.14,557.49,97.71,2 +61.156,51.18562977,-2.26292998,520.13,39.21,274.49,183.67,183.61,0.60,77.84,558.71,97.90,2 +61.875,51.18567413,-2.26385988,501.31,36.42,268.15,184.30,183.52,2.49,60.46,560.02,98.13,2 +62.703,51.18564055,-2.26497982,477.19,25.13,265.84,186.98,186.01,3.05,-5.89,1125.68,98.18,2 +63.547,51.18558943,-2.26614601,459.48,23.57,269.96,189.05,188.67,-1.53,-35.02,570.11,98.10,2 +64.344,51.18559502,-2.26726649,457.86,35.99,273.48,188.23,187.89,-1.66,-53.42,565.16,98.35,2 +65.110,51.18564845,-2.26832890,454.38,43.19,276.51,188.02,187.63,-0.37,-50.14,566.30,98.60,2 +65.860,51.18573332,-2.26935174,445.94,47.05,279.13,188.25,187.72,0.58,-7.12,567.60,98.82,2 +66.563,51.18583596,-2.27035366,438.46,56.51,278.70,188.35,187.89,-0.57,49.67,569.55,99.05,2 +67.235,51.18590537,-2.27128758,430.98,58.38,264.98,187.30,187.72,-3.78,77.22,571.33,99.26,2 +67.813,51.18585100,-2.27207512,428.22,48.61,244.57,182.99,184.00,-4.65,79.94,1181.49,99.03,2 +68.375,51.18565635,-2.27277930,426.83,46.71,237.87,185.81,185.75,-3.39,73.78,1179.81,98.74,2 +69.063,51.18532882,-2.27354163,427.66,47.60,209.93,181.69,183.99,-8.38,80.44,1173.73,98.38,2 +69.703,51.18484921,-2.27397061,436.25,51.40,188.92,179.79,180.94,-6.94,86.34,1168.50,98.02,2 +70.313,51.18434950,-2.27409717,441.10,53.15,171.77,179.36,180.39,-4.57,88.84,1163.32,97.70,2 +70.844,51.18389314,-2.27400599,439.41,51.23,161.76,180.50,180.85,-1.74,84.32,682.03,97.47,2 +71.375,51.18350798,-2.27381957,433.03,44.67,160.36,181.46,181.12,0.03,26.93,563.64,97.60,2 +71.985,51.18301391,-2.27352361,426.49,38.34,159.78,182.02,181.61,-0.07,-8.20,559.57,97.80,2 +72.578,51.18253565,-2.27324552,423.02,34.39,160.64,182.11,181.72,-0.52,-20.61,559.83,97.98,2 +73.172,51.18205846,-2.27298191,420.65,29.24,162.08,182.11,181.71,-0.76,-23.29,561.73,98.15,2 +73.703,51.18163768,-2.27276719,418.98,26.92,163.85,182.17,181.78,-0.70,-24.08,563.92,98.31,2 +74.297,51.18115940,-2.27254693,417.78,25.78,165.59,182.26,181.88,-1.40,-22.65,565.45,98.49,2 +74.797,51.18075362,-2.27237919,418.10,26.14,166.30,182.23,181.84,-1.80,-15.36,565.97,98.64,3 +75.391,51.18025628,-2.27219089,419.90,27.99,167.18,182.07,181.67,-2.34,-10.73,567.58,98.82,3 +76.000,51.17977706,-2.27202052,423.54,31.71,167.65,181.82,181.41,-3.01,9.12,569.09,98.99,3 +76.625,51.17925846,-2.27183077,428.99,37.45,166.72,181.46,181.09,-3.67,38.17,570.69,99.19,3 +77.156,51.17882421,-2.27165312,432.59,36.37,164.62,181.13,180.97,-3.53,67.81,571.88,99.35,3 +77.641,51.17841602,-2.27146040,432.10,30.05,161.34,182.26,182.12,-2.20,80.70,1151.99,99.29,3 +78.125,51.17803167,-2.27124885,427.03,24.62,155.00,184.33,184.14,-0.96,81.43,1184.11,99.01,3 +78.625,51.17765576,-2.27096074,420.19,18.11,139.80,183.12,184.63,-6.01,69.87,1179.15,98.73,3 +79.156,51.17731914,-2.27051979,420.18,18.20,125.09,183.91,184.98,-5.99,79.80,1174.33,98.44,3 +79.766,51.17702638,-2.26980452,425.61,24.02,101.16,179.13,180.45,-9.02,74.92,1169.34,98.08,3 +80.235,51.17693003,-2.26918869,434.50,33.00,97.47,181.45,181.17,-6.87,76.45,1165.23,97.80,3 +80.750,51.17687746,-2.26850333,442.21,35.21,88.98,182.78,182.81,-6.39,78.22,1161.08,97.52,3 +81.266,51.17688033,-2.26780755,448.01,40.52,84.46,184.64,184.49,-4.68,74.91,1156.21,97.22,3 +81.781,51.17692199,-2.26713309,450.19,50.56,82.17,186.45,186.24,-2.94,63.16,612.90,97.10,3 +82.422,51.17700780,-2.26626577,449.28,54.28,78.61,185.93,185.73,-2.73,37.07,554.62,97.28,3 +83.047,51.17712194,-2.26544786,451.41,55.28,75.78,185.47,185.21,-4.20,31.26,557.50,97.46,3 +83.610,51.17724941,-2.26470767,458.43,45.96,72.20,184.66,184.33,-7.02,38.68,557.74,97.63,3 +84.203,51.17742052,-2.26392440,473.87,36.66,66.58,182.83,182.00,-9.83,56.40,558.11,97.82,3 +84.766,51.17761130,-2.26325327,492.51,46.16,58.45,182.07,181.15,-10.86,74.64,1138.89,97.74,3 +85.297,51.17784872,-2.26264375,510.62,54.67,48.09,182.14,181.81,-9.49,86.94,1159.62,97.46,3 +85.969,51.17821271,-2.26201104,526.06,50.16,33.30,182.21,182.65,-6.52,88.21,1152.50,97.12,3 +86.578,51.17865366,-2.26153961,532.93,34.27,22.40,183.36,183.83,-5.14,81.64,1146.17,96.78,3 +87.094,51.17907238,-2.26126506,535.53,29.77,11.14,183.90,184.61,-4.49,78.41,1142.12,96.51,3 +87.735,51.17956944,-2.26109435,536.12,32.12,9.51,186.88,186.80,-2.15,36.10,1137.25,96.20,3 +88.344,51.18010604,-2.26096790,536.72,37.06,8.24,189.72,189.41,-1.92,-16.54,1132.37,95.87,3 +88.985,51.18068613,-2.26082574,540.44,41.69,15.79,191.48,191.60,-6.96,-54.35,1127.04,95.51,3 +89.547,51.18117443,-2.26060068,554.44,53.92,21.29,192.01,190.99,-6.87,-55.99,1122.44,95.21,3 +90.110,51.18162672,-2.26032045,567.20,57.28,19.12,193.56,193.12,-2.69,-39.83,1118.51,94.94,3 +90.688,51.18210342,-2.26003145,570.90,48.21,21.23,195.59,195.28,-2.06,1.82,1113.01,94.64,3 +91.313,51.18262388,-2.25970672,574.31,39.92,21.13,197.67,197.35,-2.64,51.35,1108.49,94.33,3 +91.844,51.18311242,-2.25943122,574.90,34.35,15.26,199.14,199.16,-3.59,72.58,1103.95,94.04,3 +92.344,51.18357111,-2.25924974,574.83,32.52,356.77,197.38,199.09,-5.73,81.48,1099.93,93.76,3 +92.891,51.18402741,-2.25929414,577.69,37.23,330.36,190.60,193.56,-6.08,87.69,1095.89,93.49,3 +93.500,51.18451550,-2.25969208,578.89,44.90,312.76,189.16,190.51,-3.01,89.38,1091.64,93.16,3 +94.141,51.18489296,-2.26033539,572.37,50.21,303.19,190.45,190.51,-0.27,82.92,1087.37,92.83,3 +94.719,51.18518580,-2.26103352,560.12,47.90,297.94,193.21,192.72,1.26,74.33,1083.23,92.52,3 +95.344,51.18543073,-2.26179259,542.97,40.69,290.19,195.85,195.05,1.75,74.22,1078.90,92.23,3 +95.938,51.18561389,-2.26260181,525.49,37.32,278.96,197.32,196.76,1.03,72.21,1075.43,91.91,3 +96.610,51.18570618,-2.26357898,507.44,38.24,270.75,199.67,198.93,0.86,66.25,1071.22,91.55,3 +97.235,51.18571124,-2.26445422,492.60,34.81,268.00,202.25,201.18,1.95,31.36,1066.77,91.24,3 +97.828,51.18568203,-2.26538282,479.40,32.49,266.99,204.75,203.74,1.55,-18.40,1062.75,90.90,3 +98.531,51.18565158,-2.26645225,467.97,35.74,268.94,207.21,206.09,1.52,-46.41,1057.45,90.52,3 +99.172,51.18564787,-2.26742178,455.57,34.59,272.29,209.32,208.04,1.61,-49.59,1052.88,90.19,3 +99.860,51.18568180,-2.26846977,442.18,32.10,275.35,211.48,210.13,1.68,-20.14,1048.17,89.82,3 +100.438,51.18573668,-2.26938743,432.68,34.31,276.04,213.16,211.86,0.66,21.50,1043.23,89.49,3 +101.047,51.18579093,-2.27035921,425.34,43.13,274.07,214.54,213.35,-0.28,49.56,1039.15,89.16,3 +101.610,51.18581787,-2.27125804,418.83,44.57,268.86,215.55,214.45,-1.57,64.77,1035.08,88.86,3 +102.188,51.18578681,-2.27218397,415.64,35.81,255.04,214.57,214.22,-4.40,76.87,1030.65,88.55,3 +102.766,51.18563887,-2.27300225,417.81,37.94,229.24,209.34,211.29,-6.09,85.38,1026.52,88.24,3 +103.375,51.18525397,-2.27370266,422.28,40.96,196.07,197.80,200.57,-5.49,89.24,1022.17,87.90,3 +104.016,51.18472653,-2.27398033,422.43,34.48,183.44,197.05,197.00,-2.95,83.98,1017.74,87.57,3 +104.547,51.18420021,-2.27402103,418.83,30.71,171.16,196.51,196.61,-3.19,71.62,1013.66,87.26,3 +105.125,51.18370758,-2.27390253,416.39,28.35,166.15,197.81,197.42,-3.47,57.52,1009.75,86.97,3 +105.672,51.18321452,-2.27369309,417.39,29.47,162.60,198.93,198.33,-3.56,46.96,1005.53,86.67,3 +106.344,51.18263862,-2.27338284,421.75,33.96,160.39,200.26,199.42,-3.43,7.08,1000.40,86.31,3 +106.938,51.18212553,-2.27308227,429.66,39.19,160.48,201.15,200.13,-3.97,-41.37,995.69,86.00,3 +107.485,51.18162694,-2.27281197,437.13,45.31,162.63,201.91,201.05,-3.26,-57.46,991.35,85.71,3 +108.078,51.18109156,-2.27255867,441.09,49.17,165.06,202.95,202.13,-2.25,-38.02,986.16,85.39,3 +108.578,51.18063956,-2.27237434,443.31,51.42,166.54,203.80,202.95,-2.28,-6.48,982.27,85.13,4 +109.219,51.18005470,-2.27214930,448.73,56.94,166.48,204.65,203.74,-3.38,29.64,977.05,84.79,4 +109.766,51.17955952,-2.27194342,454.63,62.84,164.62,205.17,204.33,-3.85,49.51,972.74,84.51,4 +110.328,51.17902297,-2.27169381,459.60,66.61,161.79,205.80,205.05,-3.33,60.31,968.02,84.19,4 +110.813,51.17859327,-2.27145252,461.38,60.65,159.36,206.39,205.69,-2.52,64.02,963.68,83.91,4 +111.313,51.17815029,-2.27117479,460.31,58.21,155.92,207.12,206.42,-1.77,77.81,959.53,83.63,4 +111.797,51.17773066,-2.27085977,456.03,53.84,143.89,206.51,206.42,-1.66,84.83,955.36,83.33,4 +112.438,51.17728615,-2.27029445,447.25,44.93,116.26,200.53,202.77,-2.39,85.97,950.68,83.00,4 +113.047,51.17702289,-2.26952382,438.06,35.73,98.90,197.52,198.14,-3.60,72.68,947.07,82.68,4 +113.594,51.17693780,-2.26869540,435.02,31.68,89.98,197.19,197.22,-5.10,59.47,942.68,82.35,4 +114.156,51.17694251,-2.26785575,438.49,35.53,86.95,198.13,197.61,-4.36,50.60,938.81,82.04,4 +114.656,51.17697734,-2.26711627,442.90,45.28,83.88,198.68,198.08,-4.43,49.80,934.67,81.75,4 +115.188,51.17703673,-2.26637910,448.29,53.52,80.58,199.04,198.48,-5.67,46.26,930.43,81.47,4 +115.781,51.17714443,-2.26550732,459.56,63.67,75.84,198.97,198.01,-7.21,46.19,925.36,81.14,4 +116.344,51.17728131,-2.26472821,474.49,65.52,72.65,198.96,197.78,-7.24,53.71,920.66,80.82,4 +116.813,51.17741695,-2.26406300,487.40,51.92,67.44,198.57,197.64,-8.41,65.36,916.00,80.54,4 +117.328,51.17760116,-2.26338667,502.51,57.94,58.27,197.41,196.54,-9.56,74.20,911.50,80.26,4 +117.891,51.17785913,-2.26273622,520.79,67.00,45.24,195.58,195.01,-9.89,80.42,906.36,79.95,4 +118.563,51.17830341,-2.26207653,543.89,66.16,27.12,191.63,191.18,-9.21,84.43,901.33,79.59,4 +119.203,51.17880252,-2.26166171,560.99,66.03,21.13,191.52,191.18,-6.41,86.08,895.27,79.25,4 +119.766,51.17927267,-2.26136647,568.78,69.47,19.05,191.97,191.99,-4.40,78.55,891.08,78.96,4 +120.344,51.17972959,-2.26112010,569.75,68.66,19.81,192.98,192.94,-0.83,53.55,886.35,78.67,4 +121.078,51.18035987,-2.26078516,559.36,56.23,19.54,195.01,194.50,1.72,-8.88,880.92,78.30,4 +121.735,51.18091683,-2.26048041,549.25,42.46,21.41,196.58,196.09,1.71,-20.49,875.66,77.93,4 +122.516,51.18157111,-2.26009108,545.69,27.69,21.66,197.15,196.91,-4.74,34.87,870.53,77.52,4 +123.125,51.18210668,-2.25978686,553.94,24.85,11.62,196.44,196.77,-8.33,69.39,865.59,77.17,4 +123.750,51.18267150,-2.25961279,567.11,31.44,5.83,195.60,195.21,-6.60,71.78,860.95,76.83,4 +124.344,51.18320490,-2.25953640,576.61,39.22,3.39,195.72,195.59,-4.85,72.70,855.79,76.52,4 +125.094,51.18386608,-2.25947061,577.49,38.62,357.89,196.34,196.44,-3.21,74.03,849.87,76.13,4 +125.813,51.18450398,-2.25957841,578.72,42.86,327.62,188.83,191.40,-8.06,83.25,844.25,75.74,4 +126.563,51.18500601,-2.26012127,586.54,62.06,293.75,179.14,182.12,-4.45,93.14,839.20,75.36,4 +127.360,51.18532753,-2.26110730,579.14,67.96,291.32,181.52,181.43,1.19,96.21,831.96,74.89,4 +128.235,51.18557639,-2.26217294,549.87,52.40,281.26,184.38,182.64,5.61,75.53,825.69,74.42,4 +129.110,51.18570368,-2.26332827,506.12,32.51,271.42,187.72,185.51,4.06,47.23,819.69,73.95,4 +130.000,51.18570393,-2.26455330,476.61,20.12,267.60,190.30,189.40,2.23,-4.83,813.62,73.48,4 +130.844,51.18566857,-2.26578445,466.23,25.00,269.17,191.82,191.32,-0.67,-35.76,808.32,73.01,4 +131.500,51.18566477,-2.26671585,462.80,34.56,271.60,192.76,192.21,0.00,-46.47,803.02,72.61,4 +132.203,51.18568875,-2.26771891,455.57,37.57,273.61,193.97,193.24,1.62,-24.97,797.70,72.24,4 +132.953,51.18573564,-2.26880555,441.31,34.70,274.65,195.64,194.45,3.08,2.42,791.45,71.82,4 +133.594,51.18577678,-2.26971520,426.94,33.55,274.24,196.86,195.82,1.89,22.92,786.57,71.45,4 +134.281,51.18580989,-2.27074694,414.39,36.21,268.19,197.53,196.95,-2.12,58.97,781.47,71.06,4 +134.922,51.18577055,-2.27166615,412.79,36.92,256.52,195.96,195.73,-5.31,67.21,776.43,70.71,4 +135.453,51.18564555,-2.27244192,418.35,38.59,244.51,194.41,194.44,-6.76,75.99,772.41,70.41,4 +135.985,51.18544728,-2.27311423,425.90,46.19,228.66,191.95,192.51,-7.65,81.07,768.16,70.13,4 +136.641,51.18507292,-2.27376091,437.33,54.61,199.69,182.62,184.48,-8.20,87.64,763.07,69.79,4 +137.250,51.18459799,-2.27405052,445.45,57.63,180.94,179.37,180.82,-5.29,90.66,757.75,69.48,4 +137.938,51.18404488,-2.27406267,444.29,55.92,164.08,177.04,177.73,-1.83,83.41,752.17,69.11,4 +138.578,51.18355178,-2.27385594,435.56,47.10,162.13,178.84,178.47,0.31,28.23,747.09,68.77,4 +139.266,51.18297179,-2.27354502,426.87,38.67,161.11,180.66,180.25,0.11,-1.01,741.55,68.37,4 +139.953,51.18244559,-2.27325087,422.76,33.53,161.00,181.87,181.51,-1.62,-2.86,736.17,67.99,4 +140.531,51.18196783,-2.27298776,423.77,32.05,161.12,182.56,182.16,-2.72,-2.23,731.51,67.65,4 +141.172,51.18147217,-2.27271742,429.89,38.16,161.37,182.78,182.25,-5.10,6.14,726.51,67.31,4 diff --git a/track_data/flight_20260831_050922_021083.csv b/track_data/flight_20260831_050922_021083.csv new file mode 100644 index 00000000..7a1238ce --- /dev/null +++ b/track_data/flight_20260831_050922_021083.csv @@ -0,0 +1,238 @@ +# aircraft_title=Extra 300S Paint2 +# recorded_at=2026-08-31T05:09:22 +# laps_s=29.482,34.556,34.278,32.510 +timestamp_s,latitude,longitude,altitude_ft,agl_ft,heading_deg,airspeed_kts,groundspeed_kts,pitch_deg,bank_deg,torque,health_pct,lap +0.328,51.18968859,-2.27592902,978.85,613.07,166.21,184.10,182.98,6.56,0.12,1127.02,100.00,1 +0.953,51.18918503,-2.27573193,952.35,583.35,166.27,188.46,186.94,8.07,0.12,1170.97,100.00,1 +1.516,51.18869015,-2.27553909,922.58,549.23,166.35,192.90,190.87,8.42,0.11,1174.26,100.00,1 +2.109,51.18817677,-2.27533781,889.82,511.92,166.24,197.45,194.71,9.99,0.13,1175.90,100.00,1 +2.766,51.18760988,-2.27511710,847.94,466.54,166.28,202.46,198.73,10.57,0.13,1176.56,100.00,1 +3.297,51.18712648,-2.27492690,811.31,430.00,166.31,206.62,202.60,10.51,0.13,1178.82,100.00,1 +3.828,51.18663320,-2.27473503,773.97,392.51,166.23,210.54,206.33,10.65,0.14,1178.32,100.00,1 +4.406,51.18609600,-2.27452414,732.39,350.98,166.13,214.83,209.93,11.67,0.16,1180.74,100.00,1 +4.906,51.18561197,-2.27433585,692.35,310.77,166.16,218.70,212.75,11.78,0.16,1180.04,100.00,1 +5.453,51.18509834,-2.27413482,649.30,267.09,166.17,222.33,216.47,11.43,0.17,1183.06,100.00,1 +6.047,51.18451619,-2.27390654,602.04,212.47,166.17,226.40,220.44,10.88,0.17,1182.30,100.00,1 +6.609,51.18394975,-2.27368415,558.63,169.09,166.17,229.78,224.15,10.26,0.18,1185.38,100.00,1 +7.141,51.18341342,-2.27347387,519.53,130.06,166.17,232.95,227.40,9.60,0.18,1185.54,100.00,1 +7.734,51.18280668,-2.27323488,478.33,88.94,166.18,235.94,230.82,8.46,0.18,1187.82,100.00,1 +8.344,51.18216406,-2.27298283,443.14,51.40,166.39,238.30,235.10,3.53,0.15,1190.12,100.00,1 +8.969,51.18151041,-2.27272289,424.00,31.44,166.26,240.11,237.47,2.15,0.18,1190.02,100.00,1 +9.531,51.18091818,-2.27249638,413.28,20.97,166.25,241.20,238.84,0.75,0.19,1191.37,100.00,1 +10.156,51.18024026,-2.27223080,409.19,17.23,166.21,241.98,239.76,-1.95,11.80,1190.89,100.00,1 +10.672,51.17970428,-2.27200583,414.06,22.54,165.13,242.10,239.72,-3.93,31.01,1191.33,100.00,1 +11.219,51.17908585,-2.27173252,424.67,32.33,163.03,242.02,239.65,-4.63,57.06,1190.88,100.00,1 +11.813,51.17847257,-2.27141338,434.51,32.77,158.59,241.97,239.83,-4.36,78.54,1190.29,100.00,1 +12.359,51.17787755,-2.27100871,441.52,39.72,144.56,240.23,238.92,-4.74,82.14,1189.31,100.00,1 +12.891,51.17742153,-2.27048781,447.78,46.03,129.38,237.82,236.80,-4.86,83.96,1189.19,100.00,1 +13.453,51.17704472,-2.26974769,454.35,52.53,110.33,233.96,233.43,-4.73,86.02,1188.96,100.00,1 +14.109,51.17681679,-2.26872304,459.71,57.10,89.76,230.14,229.77,-3.47,88.05,1189.28,100.00,1 +14.719,51.17681868,-2.26765242,458.98,53.14,80.67,229.96,228.55,-0.92,79.76,1189.02,100.00,1 +15.359,51.17692341,-2.26663345,451.42,56.10,78.01,231.66,229.83,0.54,48.37,1189.32,100.00,1 +16.000,51.17706942,-2.26560537,444.56,48.82,73.14,232.67,231.19,-4.72,32.09,1189.42,100.00,1 +16.656,51.17730793,-2.26447062,465.93,41.77,67.73,231.49,227.85,-9.99,41.83,1189.85,100.00,1 +17.188,51.17752827,-2.26364100,494.58,57.38,65.31,230.84,227.35,-10.09,74.22,1189.90,100.00,1 +17.719,51.17776680,-2.26286338,519.88,73.14,50.61,228.76,226.78,-10.31,87.33,1187.80,100.00,1 +18.438,51.17825643,-2.26203158,549.08,68.16,23.03,221.09,220.64,-7.66,92.84,1186.60,100.00,1 +19.031,51.17883401,-2.26162258,563.85,67.65,17.84,221.41,220.31,-4.11,91.86,1186.26,100.00,1 +19.563,51.17936184,-2.26135048,567.78,69.04,16.50,222.59,221.67,-1.41,70.34,1185.10,100.00,1 +20.156,51.17990564,-2.26109114,563.77,64.80,16.08,224.40,223.15,1.68,5.73,1185.07,100.00,1 +20.656,51.18041794,-2.26085552,555.93,56.10,17.24,225.92,224.70,-0.45,-47.25,1185.06,100.00,1 +21.219,51.18098663,-2.26053920,554.66,50.28,27.95,225.57,224.77,-5.30,-48.47,1185.01,100.00,1 +21.859,51.18154194,-2.26006885,569.52,54.66,29.41,226.27,224.51,-5.08,5.29,1185.72,100.00,1 +22.406,51.18207406,-2.25960987,585.09,52.79,29.13,226.55,225.08,-5.85,67.74,1185.07,100.00,1 +22.906,51.18254487,-2.25921977,595.60,50.85,16.08,225.65,225.41,-6.28,86.42,1184.37,100.00,1 +23.375,51.18301965,-2.25899905,603.14,54.28,356.13,220.99,222.00,-5.21,89.88,1184.04,100.00,1 +23.906,51.18351262,-2.25903620,606.52,61.58,335.85,216.77,218.06,-2.94,91.91,1183.84,100.00,1 +24.438,51.18403308,-2.25939346,601.67,62.41,319.04,214.36,214.61,0.23,92.90,1184.18,100.00,1 +25.078,51.18450015,-2.26002119,584.96,56.61,313.88,216.89,215.47,2.73,77.76,1182.79,99.87,1 +25.594,51.18487173,-2.26063365,565.38,47.53,308.53,219.17,217.33,3.18,71.73,1180.35,99.63,1 +26.203,51.18523922,-2.26140294,542.34,35.12,298.99,220.94,219.36,2.35,72.02,1176.83,99.38,1 +26.766,51.18550745,-2.26218974,523.47,27.05,286.81,221.84,220.71,0.70,73.64,1175.12,99.14,1 +27.375,51.18568049,-2.26315445,508.36,32.15,273.43,221.95,221.09,-0.48,75.23,1171.09,98.86,1 +27.922,51.18572198,-2.26403961,498.18,35.99,269.38,223.37,221.97,1.07,68.18,1168.98,98.61,1 +28.438,51.18571215,-2.26490333,485.92,32.85,267.52,225.24,223.44,2.12,48.28,1165.81,98.38,1 +29.094,51.18567217,-2.26594397,469.31,30.09,265.95,227.43,225.57,2.30,6.62,1162.37,98.09,1 +29.641,51.18562657,-2.26685878,457.96,31.29,266.17,229.11,227.24,2.01,-32.23,1159.56,97.85,1 +30.266,51.18558462,-2.26795213,444.99,29.88,269.81,230.51,228.56,1.79,-51.92,653.13,97.66,1 +30.797,51.18558352,-2.26883905,436.08,31.04,275.41,229.50,227.92,-0.08,-53.48,555.97,97.78,1 +31.344,51.18563322,-2.26973642,431.79,39.15,277.74,227.77,226.18,-0.19,-35.38,560.49,97.92,1 +31.969,51.18572357,-2.27075466,428.65,50.06,279.13,225.55,224.08,-0.97,39.03,561.45,98.07,1 +32.516,51.18580248,-2.27167758,425.55,49.26,271.58,223.55,222.58,-3.23,73.09,1061.66,98.13,1 +33.016,51.18580742,-2.27252649,425.91,46.08,251.00,218.87,219.50,-6.84,79.65,1165.82,97.91,1 +33.500,51.18566049,-2.27324472,432.68,52.87,225.75,211.10,213.07,-5.44,94.72,1161.37,97.67,1 +34.000,51.18535214,-2.27380589,433.78,53.50,209.67,208.16,208.71,-2.83,89.25,1157.41,97.45,1 +34.547,51.18491775,-2.27420358,428.25,45.11,180.37,201.59,204.79,-2.72,87.26,1153.91,97.21,1 +35.094,51.18438637,-2.27425621,420.52,32.28,170.87,200.72,200.28,-1.56,72.53,1150.53,96.95,1 +35.672,51.18387709,-2.27411155,414.42,26.28,162.93,201.83,201.33,-2.13,59.22,632.31,96.80,1 +36.297,51.18335395,-2.27384145,411.05,22.89,160.92,201.56,200.82,-0.99,32.71,555.49,96.94,1 +36.969,51.18274213,-2.27349283,408.35,20.30,159.31,200.72,199.91,-1.04,4.73,555.17,97.12,1 +37.578,51.18222501,-2.27317610,408.51,18.07,159.41,199.85,199.05,-1.60,-23.39,554.84,97.28,1 +38.172,51.18170529,-2.27287199,410.86,19.00,161.93,198.70,197.96,-3.76,-31.85,556.21,97.44,1 +38.766,51.18119621,-2.27261919,418.83,27.22,164.35,197.25,196.30,-4.77,-32.82,557.52,97.59,1 +39.375,51.18065469,-2.27238908,429.75,38.11,166.06,195.92,194.98,-4.19,-32.08,558.98,97.75,2 +40.000,51.18012298,-2.27218983,439.26,47.60,167.53,194.72,193.87,-3.92,-0.71,559.87,97.91,2 +40.609,51.17959100,-2.27199773,448.90,57.36,167.53,193.50,192.79,-4.30,38.70,561.08,98.06,2 +41.172,51.17907994,-2.27180145,456.06,63.48,165.59,192.47,192.03,-3.91,61.53,561.79,98.22,2 +41.703,51.17862682,-2.27159886,458.26,58.57,163.26,191.84,191.53,-2.49,69.80,562.69,98.35,2 +42.297,51.17811710,-2.27134247,454.73,52.52,157.60,191.37,191.12,-1.32,80.17,563.85,98.50,2 +42.797,51.17769393,-2.27105683,447.95,45.60,138.49,187.91,189.40,-2.90,81.79,565.20,98.64,2 +43.375,51.17734285,-2.27055386,442.02,39.87,112.80,179.24,181.95,-4.33,83.71,566.47,98.77,2 +43.922,51.17714139,-2.26987905,437.12,34.92,100.59,177.37,178.14,-2.50,78.07,567.90,98.93,2 +44.563,51.17703647,-2.26907280,429.05,26.72,98.43,179.92,179.50,-0.41,50.16,1160.04,98.88,2 +45.109,51.17697637,-2.26833530,421.93,19.24,94.65,182.65,182.31,-0.79,48.48,1178.87,98.63,2 +45.625,51.17694913,-2.26764851,416.54,17.18,91.48,185.37,184.94,-1.34,45.72,1174.33,98.40,2 +46.281,51.17695098,-2.26677221,413.65,18.72,84.47,187.95,187.77,-5.54,45.47,1169.74,98.09,2 +46.797,51.17700178,-2.26602178,421.25,26.68,79.44,189.25,188.52,-7.40,49.64,1166.04,97.85,2 +47.313,51.17708991,-2.26532498,433.97,39.00,75.68,190.72,189.68,-7.83,55.34,1162.48,97.62,2 +47.891,51.17722578,-2.26454897,450.22,33.81,67.35,191.37,190.41,-10.04,63.91,1157.49,97.35,2 +48.391,51.17739839,-2.26387222,468.12,31.63,63.88,192.39,190.96,-8.87,69.48,1153.69,97.10,2 +48.906,51.17759451,-2.26326521,483.51,37.79,55.85,192.85,192.10,-9.00,79.24,1149.05,96.88,2 +49.563,51.17792541,-2.26251713,501.70,42.32,41.69,192.57,192.41,-8.22,83.21,1144.55,96.59,2 +50.125,51.17830166,-2.26196516,514.41,34.49,29.57,191.54,191.63,-6.80,85.42,560.32,96.56,2 +50.641,51.17870086,-2.26160156,521.88,24.20,19.97,188.79,189.12,-4.97,86.97,548.18,96.70,2 +51.219,51.17917697,-2.26131066,523.75,20.88,17.43,188.37,188.33,-1.91,71.45,550.32,96.85,2 +51.875,51.17968572,-2.26106652,519.13,15.55,15.29,188.43,188.20,-0.23,23.32,550.85,97.01,2 +52.500,51.18021229,-2.26084314,516.18,13.94,15.64,188.03,187.94,-3.64,-26.02,551.18,97.17,2 +53.031,51.18068255,-2.26063022,522.93,18.74,19.58,186.83,186.46,-5.72,-47.27,552.44,97.32,2 +53.672,51.18118776,-2.26033846,534.64,25.49,21.24,185.80,185.37,-4.58,-18.16,553.89,97.47,2 +54.234,51.18165008,-2.26004280,544.72,25.87,22.06,185.01,184.58,-4.42,4.22,554.86,97.62,2 +54.859,51.18213207,-2.25973575,555.16,26.00,21.62,184.11,183.85,-5.14,43.98,556.01,97.77,2 +55.391,51.18257126,-2.25947863,563.74,24.82,12.25,182.44,182.96,-8.91,69.13,556.75,97.91,2 +55.906,51.18299885,-2.25933339,574.13,32.36,359.38,179.46,180.27,-7.55,93.55,557.48,98.04,2 +56.406,51.18340123,-2.25932404,579.04,38.07,348.15,177.61,178.72,-3.34,97.57,558.48,98.17,2 +56.938,51.18383021,-2.25944534,574.14,34.97,336.72,178.59,179.26,0.58,92.29,1156.62,98.01,2 +57.563,51.18431173,-2.25977355,557.60,24.75,321.21,179.70,179.98,0.70,78.08,1159.16,97.73,2 +58.141,51.18468085,-2.26024187,541.80,17.66,306.80,180.77,181.23,-0.18,75.10,1155.91,97.49,2 +58.781,51.18500553,-2.26094054,526.70,13.88,300.21,184.31,184.01,0.24,50.99,1151.43,97.20,2 +59.359,51.18525155,-2.26163077,517.94,14.55,293.54,186.92,187.05,-3.13,50.96,1148.77,96.94,2 +59.953,51.18544494,-2.26237429,516.64,24.78,290.15,189.43,189.25,-2.23,75.14,1144.95,96.68,2 +60.531,51.18561623,-2.26314533,511.51,35.03,282.43,191.78,191.71,-0.28,91.98,1141.34,96.42,2 +61.188,51.18574081,-2.26404543,495.36,32.95,270.78,193.75,193.15,2.09,78.39,615.84,96.25,2 +62.031,51.18575123,-2.26516534,470.88,21.31,265.37,193.90,192.92,1.45,21.53,546.07,96.43,2 +62.891,51.18567810,-2.26640475,457.58,25.04,263.92,193.86,193.20,0.66,-21.72,547.93,96.66,2 +63.688,51.18561097,-2.26757112,447.34,28.09,268.73,193.41,192.83,-0.01,-70.47,550.43,96.88,2 +64.328,51.18561109,-2.26849022,436.76,27.18,276.64,192.65,192.01,0.93,-52.76,552.19,97.06,2 +65.125,51.18569588,-2.26959950,422.97,27.87,278.10,192.91,192.03,1.69,12.96,554.29,97.27,2 +65.875,51.18578138,-2.27065523,410.90,31.15,275.82,192.84,192.11,-0.49,41.49,556.20,97.47,2 +66.609,51.18582278,-2.27169572,405.33,29.13,263.14,191.80,191.95,-5.47,69.10,1083.80,97.55,2 +67.281,51.18573244,-2.27266449,410.87,31.18,243.32,190.20,190.93,-8.13,76.97,1159.24,97.23,2 +67.875,51.18550706,-2.27337579,422.55,43.29,219.32,185.75,187.22,-10.27,82.37,1154.31,96.97,2 +68.469,51.18511720,-2.27390122,438.68,57.78,194.48,181.02,182.46,-9.24,90.36,1149.29,96.69,2 +69.094,51.18462676,-2.27411252,449.39,61.57,172.79,177.71,179.30,-4.92,92.83,1145.17,96.41,2 +69.625,51.18415684,-2.27406341,449.14,61.03,170.94,180.51,180.40,-1.67,70.65,611.67,96.26,2 +70.203,51.18367155,-2.27392657,444.11,55.95,165.50,180.16,180.09,-1.78,54.92,547.81,96.40,2 +70.703,51.18327622,-2.27375791,440.55,52.43,162.74,180.53,180.39,-1.40,51.11,550.05,96.53,2 +71.219,51.18285800,-2.27353804,436.17,47.97,160.88,180.81,180.49,-0.15,45.30,548.44,96.66,2 +71.859,51.18237903,-2.27325048,428.42,38.65,158.68,181.19,180.74,0.85,13.39,550.13,96.82,2 +72.406,51.18194615,-2.27297646,421.77,29.42,158.06,181.61,181.11,0.89,-25.53,552.17,96.95,2 +72.922,51.18153853,-2.27272671,415.73,23.58,161.49,181.80,181.49,-1.49,-40.79,553.42,97.09,2 +73.641,51.18095975,-2.27243984,413.68,21.70,165.80,181.54,181.27,-2.51,-37.48,555.07,97.27,3 +74.219,51.18048717,-2.27225916,414.85,22.90,167.29,181.46,181.09,-2.08,-11.02,555.92,97.41,3 +74.813,51.18000891,-2.27209366,418.04,26.23,168.10,181.07,180.68,-4.17,0.33,557.16,97.56,3 +75.344,51.17957796,-2.27194715,425.49,33.79,168.02,180.60,180.05,-4.13,26.88,558.31,97.69,3 +75.875,51.17912998,-2.27178286,432.24,40.18,166.93,180.14,179.81,-4.09,57.75,559.44,97.83,3 +76.375,51.17871784,-2.27161384,435.18,37.47,161.35,179.55,179.64,-4.96,66.36,560.07,97.96,3 +76.875,51.17833875,-2.27140386,437.69,35.79,155.06,178.78,178.93,-4.99,71.92,561.18,98.10,3 +77.344,51.17797387,-2.27113204,439.30,37.31,149.41,179.55,179.67,-3.92,79.39,1133.58,98.04,3 +77.922,51.17757862,-2.27073896,437.68,35.57,137.53,180.84,181.31,-3.22,81.68,1165.94,97.76,3 +78.469,51.17725271,-2.27026507,433.85,31.71,120.53,180.42,181.56,-3.28,83.19,1161.45,97.51,3 +79.016,51.17700049,-2.26962550,428.65,26.44,104.79,180.21,181.14,-2.60,80.47,1157.08,97.24,3 +79.609,51.17686450,-2.26885284,422.60,20.35,92.72,181.86,182.36,-4.15,65.74,1153.27,96.96,3 +80.141,51.17684142,-2.26815409,424.39,18.58,83.15,182.34,182.45,-7.21,58.78,1149.67,96.71,3 +80.719,51.17690171,-2.26736876,434.03,34.03,81.01,184.53,184.01,-5.02,58.67,694.35,96.51,3 +81.359,51.17699505,-2.26653750,440.46,45.61,78.43,184.01,183.71,-3.53,41.57,552.55,96.64,3 +81.953,51.17710086,-2.26577158,444.61,48.74,76.17,183.76,183.46,-4.38,23.46,552.15,96.79,3 +82.531,51.17722647,-2.26501039,453.62,50.79,74.36,182.90,182.29,-5.59,28.03,550.69,96.93,3 +83.156,51.17738657,-2.26417847,466.04,35.74,70.86,181.64,181.28,-7.12,56.86,552.18,97.10,3 +83.656,51.17753595,-2.26353674,476.31,32.72,63.66,181.20,181.01,-8.43,67.10,1097.42,97.09,3 +84.219,51.17773713,-2.26290682,489.10,36.62,53.91,181.39,181.19,-9.58,71.20,1151.56,96.86,3 +84.859,51.17806632,-2.26222970,508.47,38.26,36.28,180.33,180.37,-10.80,83.32,1145.64,96.56,3 +85.422,51.17844042,-2.26177917,525.05,37.39,22.91,179.75,179.99,-7.82,97.13,1140.51,96.31,3 +86.031,51.17891823,-2.26143786,532.76,31.71,19.15,182.29,182.41,-3.02,94.25,1136.14,96.02,3 +86.672,51.17939311,-2.26116952,529.55,24.39,17.67,185.49,185.27,0.29,36.54,1131.17,95.75,3 +87.281,51.17989439,-2.26092147,524.58,20.00,15.54,187.59,187.51,-3.17,5.43,565.68,95.67,3 +87.875,51.18039382,-2.26070220,531.31,27.33,16.89,186.03,185.57,-6.40,-30.03,539.96,95.82,3 +88.422,51.18086389,-2.26046580,545.99,39.52,21.18,184.43,183.76,-7.82,-55.34,542.23,95.97,3 +89.031,51.18132739,-2.26017092,562.54,50.86,23.20,183.31,182.59,-6.44,-18.37,543.68,96.12,3 +89.656,51.18181614,-2.25983609,578.94,52.82,24.11,182.37,181.84,-6.14,48.54,756.92,96.27,3 +90.172,51.18224069,-2.25954907,588.95,53.45,22.51,183.44,183.52,-5.50,85.22,1133.71,96.06,3 +90.672,51.18263506,-2.25929807,592.25,48.69,11.45,184.29,185.21,-3.28,93.57,1131.23,95.83,3 +91.141,51.18304198,-2.25915653,588.41,42.46,359.15,184.37,185.25,-0.52,89.21,1127.15,95.60,3 +91.594,51.18340692,-2.25914394,579.97,35.40,350.44,185.62,185.96,-0.24,76.65,1123.76,95.38,3 +92.094,51.18383273,-2.25925838,569.94,28.71,333.73,184.81,186.33,-3.16,74.91,1120.31,95.15,3 +92.625,51.18423224,-2.25955702,565.21,28.62,323.25,185.26,185.94,-3.00,75.97,1117.90,94.92,3 +93.234,51.18465817,-2.26006093,560.23,33.03,315.27,187.76,187.95,-1.74,77.10,1114.02,94.64,3 +93.844,51.18502479,-2.26065561,552.40,35.30,307.01,190.01,190.01,-0.62,78.03,1110.50,94.37,3 +94.516,51.18537653,-2.26141399,539.89,32.40,295.67,192.26,192.16,0.02,78.84,1106.39,94.07,3 +95.188,51.18563024,-2.26228840,524.33,29.39,282.14,193.95,193.79,0.50,79.54,1102.19,93.78,3 +95.766,51.18574362,-2.26312743,508.76,31.26,272.29,195.87,195.25,1.54,77.71,1099.40,93.52,3 +96.516,51.18577003,-2.26415744,487.08,25.81,267.45,199.56,198.26,1.80,34.99,1095.05,93.20,3 +97.234,51.18572938,-2.26524252,473.86,25.44,264.96,202.35,201.57,0.73,5.02,1090.58,92.85,3 +97.922,51.18567307,-2.26624338,466.76,31.91,264.84,204.84,203.93,0.63,-36.26,1086.80,92.54,3 +98.563,51.18562758,-2.26724597,456.95,34.21,267.83,207.17,206.03,1.04,-64.89,1082.40,92.24,3 +99.125,51.18561506,-2.26810959,445.06,31.32,272.78,208.97,207.76,1.63,-67.05,1078.71,91.98,3 +99.750,51.18564748,-2.26905938,428.50,25.38,274.86,211.45,209.62,3.37,-57.37,1075.60,91.72,3 +100.344,51.18570436,-2.26997225,409.61,20.66,277.95,213.82,211.88,2.56,-0.28,1071.80,91.46,3 +100.875,51.18577036,-2.27079867,399.92,22.24,274.97,214.67,213.74,-4.39,36.83,1069.40,91.22,3 +101.438,51.18580277,-2.27169316,408.07,32.38,264.84,213.60,212.43,-9.01,70.96,1065.44,90.96,3 +102.031,51.18572379,-2.27264564,428.80,49.51,244.19,209.90,209.19,-9.87,89.43,1061.71,90.68,3 +102.609,51.18549536,-2.27339298,444.28,64.64,217.17,202.70,204.75,-5.46,94.33,1057.06,90.41,3 +103.234,51.18504010,-2.27395579,446.74,65.17,189.84,196.33,198.47,-3.60,89.64,1053.11,90.13,3 +103.828,51.18450983,-2.27411396,442.21,54.03,171.25,192.87,193.74,-3.20,79.01,1049.53,89.87,3 +104.422,51.18397979,-2.27400712,436.55,48.31,169.76,195.50,194.90,-0.49,54.48,1046.11,89.60,3 +105.047,51.18344515,-2.27383429,428.30,40.03,167.55,197.81,197.03,0.43,41.86,1042.73,89.33,3 +105.625,51.18291948,-2.27362426,420.23,31.99,164.26,199.75,199.05,-1.58,33.74,1039.11,89.06,3 +106.266,51.18235385,-2.27334909,418.08,28.61,161.64,201.45,200.73,-2.62,31.80,1035.10,88.78,3 +106.828,51.18185106,-2.27305986,421.90,30.16,159.29,202.45,201.54,-4.15,23.27,1031.41,88.51,3 +107.438,51.18132666,-2.27272699,431.16,39.53,158.85,203.40,202.30,-3.90,-15.43,1027.42,88.24,3 +108.031,51.18079729,-2.27239618,441.38,49.65,159.59,204.29,203.20,-3.51,-57.19,1023.13,87.97,4 +108.594,51.18029728,-2.27210981,446.64,54.72,162.62,205.17,204.36,-2.44,-72.82,1018.89,87.71,4 +109.125,51.17982088,-2.27187698,446.88,54.83,165.29,206.22,205.36,-0.97,-42.08,1014.88,87.46,4 +109.719,51.17928087,-2.27165808,445.04,53.09,166.57,207.55,206.63,-1.22,20.68,1010.94,87.19,4 +110.313,51.17871263,-2.27142277,444.55,45.24,164.36,208.55,207.73,-2.36,61.03,1006.94,86.92,4 +110.797,51.17826089,-2.27120219,443.46,41.43,156.44,208.79,208.34,-3.65,73.62,1003.52,86.68,4 +111.266,51.17785333,-2.27091700,443.36,41.36,146.04,208.09,207.90,-3.65,81.58,1000.20,86.45,4 +111.766,51.17746616,-2.27050666,442.76,40.74,130.72,206.52,206.96,-3.66,83.95,996.74,86.20,4 +112.344,51.17711865,-2.26984223,441.59,39.55,106.55,200.38,201.88,-4.33,82.87,993.19,85.95,4 +112.906,51.17694775,-2.26903294,441.13,39.14,96.24,200.08,200.11,-4.48,74.59,990.04,85.69,4 +113.391,51.17689471,-2.26830987,442.19,36.72,87.73,199.94,199.91,-5.09,73.70,986.74,85.44,4 +113.922,51.17691197,-2.26757462,445.00,45.11,82.21,200.24,199.86,-4.75,59.71,983.45,85.21,4 +114.563,51.17700220,-2.26662819,450.06,55.25,80.50,201.42,200.69,-3.30,28.63,979.42,84.92,4 +115.125,51.17710167,-2.26580258,455.04,59.32,79.51,202.27,201.51,-3.24,25.86,975.34,84.66,4 +115.750,51.17722170,-2.26491499,463.28,55.52,75.26,202.43,201.59,-8.41,36.65,971.16,84.39,4 +116.266,51.17736596,-2.26413119,482.04,52.95,68.99,201.57,199.68,-11.01,69.41,967.24,84.13,4 +116.750,51.17753450,-2.26346736,502.59,58.72,58.45,200.06,198.78,-10.55,84.77,962.99,83.89,4 +117.281,51.17776668,-2.26284304,521.23,71.60,48.43,199.06,198.28,-8.26,93.25,959.16,83.65,4 +118.031,51.17822754,-2.26209186,535.93,54.80,31.09,196.06,196.31,-5.36,85.54,953.41,83.33,4 +118.703,51.17875055,-2.26158712,538.39,39.81,21.33,196.58,196.75,-2.80,84.03,949.24,83.02,4 +119.359,51.17931151,-2.26123575,533.50,29.45,18.84,198.16,197.81,-0.60,51.40,944.44,82.71,4 +120.016,51.17984497,-2.26095678,526.73,22.46,16.25,199.60,199.22,-1.33,25.42,940.80,82.45,4 +120.734,51.18052320,-2.26066861,525.37,20.45,16.38,200.96,200.48,-2.05,-24.59,935.79,82.09,4 +121.406,51.18113200,-2.26038554,531.05,23.23,20.43,201.48,200.88,-3.67,-48.04,931.50,81.78,4 +122.078,51.18169380,-2.26005421,539.64,19.74,22.07,202.01,201.34,-3.12,12.67,927.02,81.48,4 +122.844,51.18236334,-2.25964724,551.44,16.71,13.48,201.32,201.09,-8.34,68.80,921.89,81.12,4 +123.516,51.18298505,-2.25942882,566.63,26.62,4.69,200.86,200.59,-5.87,91.48,916.66,80.79,4 +124.219,51.18362592,-2.25935948,571.68,31.14,351.73,200.16,200.55,-2.47,92.45,911.27,80.47,4 +125.016,51.18430469,-2.25957043,562.20,25.42,326.07,196.46,197.61,-0.63,85.23,906.58,80.14,4 +125.906,51.18492170,-2.26029847,545.98,24.01,300.20,192.95,193.92,-2.65,75.11,901.47,79.75,4 +126.781,51.18532909,-2.26145063,538.34,32.61,289.93,194.02,193.98,-2.10,65.96,896.48,79.32,4 +127.641,51.18558222,-2.26262655,529.58,43.05,281.08,195.51,195.39,-1.17,76.65,891.16,78.92,4 +128.469,51.18570836,-2.26382888,515.29,50.00,271.64,196.71,196.09,1.35,80.07,885.75,78.52,4 +129.313,51.18572223,-2.26499842,491.31,39.37,268.55,199.26,197.85,3.59,37.88,880.81,78.13,4 +130.203,51.18568546,-2.26629934,463.29,28.99,267.19,202.00,200.56,3.09,-14.67,875.94,77.73,4 +130.984,51.18565127,-2.26751679,444.12,24.26,270.07,203.78,202.61,1.58,-46.83,870.33,77.34,4 +131.672,51.18566603,-2.26858664,431.70,23.19,274.75,204.72,203.73,0.89,-48.31,866.07,76.99,4 +132.406,51.18573201,-2.26966748,419.67,25.81,276.79,206.01,204.80,1.65,17.69,861.68,76.65,4 +133.078,51.18579709,-2.27068133,408.66,29.54,272.38,206.60,205.72,-1.64,53.81,857.31,76.33,4 +133.641,51.18580933,-2.27157540,405.21,30.01,264.30,206.11,205.55,-4.15,63.06,853.32,76.04,4 +134.219,51.18574265,-2.27243875,409.21,29.46,248.90,204.02,204.06,-6.53,82.55,849.88,75.78,4 +134.875,51.18552044,-2.27328009,416.10,36.25,215.93,194.57,197.32,-5.60,90.72,845.98,75.50,4 +135.484,51.18508027,-2.27380144,415.79,32.85,188.78,186.40,188.34,-3.40,82.26,841.95,75.20,4 +136.078,51.18458178,-2.27395013,412.94,24.94,172.68,183.64,184.68,-5.17,77.30,838.17,74.93,4 +136.672,51.18408641,-2.27386439,413.06,25.01,165.88,184.12,184.08,-3.01,72.30,834.26,74.66,4 +137.344,51.18355070,-2.27363774,411.11,23.10,162.90,185.41,185.05,-2.06,25.69,830.01,74.35,4 +138.016,51.18299051,-2.27335170,411.75,23.79,161.76,186.66,186.15,-1.47,4.52,825.07,74.03,4 +138.688,51.18242822,-2.27304901,413.04,23.81,161.32,187.71,187.19,-2.44,3.38,820.33,73.71,4 +139.375,51.18185857,-2.27273802,418.93,27.23,161.00,188.29,187.63,-3.93,3.44,815.39,73.38,4 +140.000,51.18136294,-2.27245830,427.97,36.38,160.75,188.64,187.91,-3.88,3.53,811.21,73.09,4 diff --git a/track_data/flight_20260831_051229_021189.csv b/track_data/flight_20260831_051229_021189.csv new file mode 100644 index 00000000..5e25afc3 --- /dev/null +++ b/track_data/flight_20260831_051229_021189.csv @@ -0,0 +1,238 @@ +# aircraft_title=Extra 300S Paint2 +# recorded_at=2026-08-31T05:12:29 +# laps_s=29.500,35.223,34.110,33.058 +timestamp_s,latitude,longitude,altitude_ft,agl_ft,heading_deg,airspeed_kts,groundspeed_kts,pitch_deg,bank_deg,torque,health_pct,lap +0.375,51.18975228,-2.27595115,981.78,618.29,166.31,183.58,182.50,6.29,0.10,1085.75,100.00,1 +0.891,51.18931096,-2.27578098,959.93,590.82,166.30,187.40,186.09,7.55,0.11,1169.64,100.00,1 +1.516,51.18879263,-2.27557857,929.13,556.59,166.37,192.01,189.55,9.62,0.11,1174.06,100.00,1 +2.079,51.18830526,-2.27538888,895.10,518.60,166.78,196.49,193.57,9.54,0.06,1174.54,100.00,1 +2.672,51.18778048,-2.27518467,858.29,477.53,167.03,201.04,198.01,9.44,0.04,1176.92,100.00,1 +3.204,51.18731579,-2.27500762,826.30,445.03,167.08,204.89,201.73,9.23,0.03,1177.04,100.00,1 +3.782,51.18678153,-2.27480467,790.59,409.38,167.04,208.98,205.75,9.44,0.04,1178.56,100.00,1 +4.375,51.18621687,-2.27459153,751.74,370.39,166.92,213.28,209.29,10.25,0.07,1180.30,100.00,1 +4.875,51.18574391,-2.27441405,717.04,335.49,166.89,216.67,212.15,10.83,0.07,1179.97,100.00,1 +5.375,51.18526408,-2.27423377,679.54,297.85,166.89,220.28,214.94,11.43,0.08,1182.11,100.00,1 +6.000,51.18466655,-2.27401165,630.49,240.93,167.05,224.36,218.55,11.19,0.10,1181.76,100.00,1 +6.610,51.18405280,-2.27378149,582.69,193.17,167.14,228.37,222.74,10.33,7.89,1184.76,100.00,1 +7.204,51.18345530,-2.27355449,538.97,149.51,166.60,231.94,226.52,9.42,10.96,1184.02,100.00,1 +7.829,51.18282092,-2.27330397,496.52,107.13,165.98,235.06,230.19,8.44,11.13,1187.17,100.00,1 +8.407,51.18222719,-2.27306031,460.51,68.24,165.53,237.88,233.29,7.45,6.11,1189.02,100.00,1 +9.016,51.18158337,-2.27278460,425.99,32.91,165.41,240.28,236.24,6.35,2.62,1189.22,100.00,1 +9.594,51.18096459,-2.27251997,399.05,6.33,165.38,242.13,239.05,1.78,0.73,1191.41,100.00,1 +10.297,51.18022924,-2.27220180,399.49,8.28,165.24,242.00,239.22,-5.49,0.69,1191.00,100.00,1 +10.938,51.17954021,-2.27191269,421.75,30.33,165.34,241.68,238.48,-5.30,24.21,1191.98,100.00,1 +11.500,51.17892079,-2.27164318,438.54,43.70,164.68,241.34,238.82,-5.22,56.01,1190.40,100.00,1 +12.125,51.17827980,-2.27133784,451.50,49.94,160.52,241.14,239.05,-4.89,84.59,1189.99,100.00,1 +12.625,51.17772564,-2.27099854,458.34,56.48,141.67,238.12,237.81,-3.78,90.99,1188.60,100.00,1 +13.188,51.17727703,-2.27042173,458.26,56.05,113.70,229.68,231.22,-2.55,87.03,1188.74,100.00,1 +13.813,51.17699768,-2.26945369,453.53,51.38,100.94,227.67,226.55,-1.66,79.69,1189.17,100.00,1 +14.391,51.17688205,-2.26848784,447.48,39.67,94.92,228.99,227.54,-0.91,72.59,1189.66,100.00,1 +14.969,51.17684069,-2.26750612,441.68,38.90,84.52,229.14,228.09,-3.48,64.91,1189.88,100.00,1 +15.594,51.17691007,-2.26647941,444.98,50.17,78.86,229.62,228.06,-3.66,56.48,1190.32,100.00,1 +16.219,51.17704965,-2.26546155,452.58,57.96,73.63,230.08,228.43,-5.41,51.90,1190.33,100.00,1 +16.766,51.17722557,-2.26457139,465.79,57.73,67.75,229.96,227.93,-7.51,59.57,1189.77,100.00,1 +17.250,51.17743153,-2.26379218,482.55,43.20,61.23,229.43,227.30,-8.32,72.85,1189.75,100.00,1 +17.797,51.17769761,-2.26304253,502.08,50.99,52.02,228.57,226.46,-8.68,77.46,1188.31,100.00,1 +18.485,51.17814173,-2.26219666,526.88,56.94,32.23,225.26,224.27,-7.42,93.04,1187.84,100.00,1 +19.110,51.17870829,-2.26164403,540.35,43.92,17.75,223.40,222.79,-3.32,96.03,1186.93,100.00,1 +19.657,51.17925678,-2.26134284,540.30,39.65,15.94,224.84,223.74,-0.41,72.32,1186.11,100.00,1 +20.235,51.17981424,-2.26109591,534.54,34.26,13.93,226.51,225.22,0.32,-8.53,1186.08,100.00,1 +20.829,51.18042195,-2.26083925,530.40,30.15,21.42,227.39,226.56,-3.56,-63.07,1186.37,100.00,1 +21.375,51.18094398,-2.26050619,536.26,30.84,27.23,227.26,225.80,-4.73,-23.31,1186.36,100.00,1 +22.000,51.18153261,-2.26003358,552.39,34.53,27.82,227.78,225.84,-5.57,9.84,1186.38,100.00,1 +22.579,51.18208448,-2.25958181,570.67,41.26,26.98,227.94,226.12,-6.43,56.13,1185.40,100.00,1 +23.157,51.18263365,-2.25916216,586.02,41.19,16.43,227.57,226.68,-6.97,85.12,1185.04,100.00,1 +23.641,51.18312362,-2.25894662,596.68,47.09,351.02,220.26,222.05,-6.81,89.83,1183.87,100.00,1 +24.157,51.18361746,-2.25904997,604.07,59.13,330.46,215.11,216.24,-4.20,92.38,1184.16,100.00,1 +24.688,51.18409240,-2.25944221,603.87,65.32,322.49,215.10,214.75,-0.86,93.47,1184.09,100.00,1 +25.282,51.18456832,-2.26002037,593.72,65.23,316.84,217.25,216.32,1.70,88.58,1181.80,99.81,1 +25.782,51.18492733,-2.26055396,578.56,59.42,310.02,218.93,217.41,3.31,85.64,1179.46,99.61,1 +26.391,51.18530864,-2.26129280,552.99,43.78,299.54,220.95,218.82,3.72,79.01,1175.98,99.36,1 +27.047,51.18562730,-2.26223311,524.47,28.00,283.89,221.35,219.74,2.71,76.33,1173.83,99.09,1 +27.641,51.18576744,-2.26312896,502.82,25.96,270.32,221.58,220.31,0.99,68.37,1171.28,98.85,1 +28.235,51.18577421,-2.26411636,488.15,26.98,265.59,223.31,221.83,1.18,39.76,1168.74,98.59,1 +28.797,51.18572274,-2.26506986,478.25,27.51,264.05,225.25,223.70,1.07,-8.09,1165.94,98.33,1 +29.422,51.18566085,-2.26609488,469.99,32.84,265.01,226.96,225.38,0.64,-46.94,1162.19,98.07,1 +29.954,51.18562211,-2.26694587,461.51,35.96,266.97,228.38,226.65,1.33,-50.23,1159.77,97.86,1 +30.485,51.18559875,-2.26787461,450.53,34.45,269.05,228.74,226.86,1.78,-50.84,561.68,97.90,1 +31.000,51.18559705,-2.26872669,438.81,32.22,272.14,227.92,226.07,1.84,-45.39,559.04,98.02,1 +31.532,51.18562025,-2.26959678,427.92,32.69,273.99,226.48,224.66,1.64,-1.12,561.48,98.15,1 +32.094,51.18565659,-2.27053882,418.78,37.59,273.50,224.79,223.22,0.38,46.93,563.56,98.28,1 +32.672,51.18567948,-2.27150320,409.15,31.63,268.55,223.03,221.57,-0.35,67.79,564.80,98.42,1 +33.204,51.18565660,-2.27235648,401.06,20.86,257.20,221.01,220.20,-2.66,70.33,1114.19,98.42,1 +33.735,51.18552170,-2.27320221,402.38,22.79,230.20,213.29,215.30,-10.84,74.08,1171.29,98.18,1 +34.266,51.18521229,-2.27381126,422.12,43.05,203.90,203.81,203.88,-13.40,85.52,1166.79,97.95,1 +34.860,51.18473128,-2.27415470,449.28,65.52,181.17,198.48,198.33,-8.19,103.78,1161.44,97.69,1 +35.407,51.18422495,-2.27420825,459.43,71.50,172.38,198.56,198.29,-3.13,95.84,1157.82,97.44,1 +36.000,51.18370378,-2.27411011,456.82,68.57,164.22,200.11,199.80,-0.65,87.92,671.77,97.28,1 +36.610,51.18314144,-2.27385266,445.65,57.21,157.34,199.45,198.55,1.40,54.88,558.45,97.42,1 +37.313,51.18257017,-2.27346865,431.75,43.10,155.77,199.39,198.44,1.06,-1.79,557.89,97.58,1 +37.969,51.18200047,-2.27306301,423.28,31.24,156.72,198.94,198.13,-0.16,-26.24,558.90,97.75,1 +38.641,51.18144536,-2.27269965,420.64,28.73,160.45,197.90,197.27,-2.92,-33.89,559.72,97.91,1 +39.219,51.18093768,-2.27241160,424.82,33.00,162.50,196.86,196.14,-2.64,-43.70,561.14,98.06,2 +39.797,51.18044146,-2.27217696,427.64,35.74,164.66,195.97,195.37,-2.58,-45.06,562.33,98.20,2 +40.422,51.17989675,-2.27195563,429.25,37.27,166.79,195.22,194.62,-1.80,-32.60,563.55,98.34,2 +41.047,51.17933871,-2.27175983,429.99,38.18,168.35,194.62,193.99,-1.59,2.46,564.79,98.49,2 +41.594,51.17885315,-2.27159576,431.62,35.54,167.67,193.93,193.40,-3.27,36.34,565.75,98.62,2 +42.219,51.17831611,-2.27138563,436.38,34.59,162.69,192.65,192.26,-5.11,64.46,566.91,98.78,2 +42.688,51.17789182,-2.27116406,440.95,39.14,154.02,192.46,192.43,-6.10,77.69,1155.48,98.71,2 +43.204,51.17750176,-2.27085736,445.25,43.37,135.07,190.51,191.80,-4.79,92.25,1175.65,98.49,2 +43.922,51.17709658,-2.27013104,441.31,39.03,96.48,179.92,183.68,-3.04,85.27,1169.97,98.19,2 +44.500,51.17700061,-2.26936752,433.67,31.38,92.36,182.56,182.28,-0.09,51.12,1165.96,97.92,2 +45.172,51.17698646,-2.26845596,424.26,21.38,90.00,186.41,185.91,-0.33,17.57,749.01,97.68,2 +45.782,51.17699093,-2.26765232,419.91,22.32,88.78,186.77,186.33,-1.26,35.15,563.87,97.80,2 +46.297,51.17701264,-2.26687376,419.29,24.45,83.92,186.08,185.93,-5.17,45.52,561.03,97.94,2 +46.875,51.17707484,-2.26609232,425.98,31.21,78.58,184.98,184.69,-6.42,56.24,560.88,98.07,2 +47.375,51.17716621,-2.26541485,434.31,37.58,74.55,183.83,183.37,-6.34,57.61,562.52,98.20,2 +47.922,51.17728918,-2.26473497,443.03,30.01,71.86,183.23,182.80,-5.54,43.09,564.10,98.33,2 +48.438,51.17743035,-2.26409282,451.65,15.86,69.26,182.87,182.31,-5.89,39.49,823.53,98.44,2 +49.000,51.17760708,-2.26339827,464.56,18.39,62.14,182.95,182.29,-12.72,42.32,1169.96,98.23,2 +49.641,51.17787585,-2.26264826,500.18,43.42,50.98,181.64,177.42,-18.95,60.85,1167.07,97.95,2 +50.188,51.17816921,-2.26209174,542.59,70.54,38.31,180.23,175.19,-18.48,92.60,1160.74,97.71,2 +50.813,51.17855082,-2.26162078,581.09,88.98,19.99,177.31,176.83,-9.03,118.20,1156.73,97.44,2 +51.407,51.17902078,-2.26128751,595.63,90.76,19.16,180.02,180.10,-4.23,94.81,1149.52,97.17,2 +52.016,51.17947988,-2.26103218,597.21,88.82,15.69,182.64,182.86,-1.56,75.75,955.51,96.94,2 +52.657,51.17998239,-2.26081431,591.18,83.50,13.07,183.57,183.64,-0.02,36.03,554.43,97.04,2 +53.235,51.18048688,-2.26063775,584.43,78.18,11.88,184.08,184.01,0.55,-42.61,549.95,97.19,2 +53.766,51.18093361,-2.26047539,576.59,69.96,17.11,184.01,183.95,0.05,-71.71,550.36,97.33,2 +54.297,51.18136537,-2.26025720,567.24,54.32,24.92,183.35,183.42,0.06,-52.03,552.49,97.46,2 +54.922,51.18184673,-2.25990588,560.00,34.84,25.82,183.70,183.67,-1.74,27.42,554.36,97.60,2 +55.485,51.18229059,-2.25959244,562.05,27.44,18.72,182.75,183.08,-8.01,54.02,1048.10,97.66,2 +56.063,51.18276416,-2.25935326,573.79,32.15,13.81,184.15,184.10,-6.78,86.02,1155.74,97.42,2 +56.579,51.18319623,-2.25919713,579.74,35.42,1.85,184.66,185.51,-4.12,92.84,1151.57,97.19,2 +57.079,51.18362499,-2.25915613,578.38,34.76,352.29,186.06,186.64,-1.72,87.19,1147.16,96.97,2 +57.704,51.18414149,-2.25929731,570.03,29.31,327.88,183.25,185.16,-1.86,82.35,1142.98,96.71,2 +58.344,51.18459653,-2.25977250,560.90,28.38,308.80,181.92,182.91,-2.29,79.56,1139.67,96.44,2 +58.922,51.18489303,-2.26034153,553.36,31.15,304.15,184.20,184.15,0.15,72.21,565.56,96.40,2 +59.485,51.18517420,-2.26100641,541.95,29.57,297.13,183.76,183.77,-0.88,60.88,545.25,96.55,2 +60.172,51.18541266,-2.26180161,533.40,31.85,287.76,182.81,183.11,-2.21,71.42,547.40,96.71,2 +60.813,51.18558063,-2.26265635,525.78,39.06,278.72,182.41,182.64,-0.70,82.72,549.41,96.87,2 +61.438,51.18566403,-2.26349904,511.97,41.36,272.42,182.79,182.33,1.91,77.48,551.12,97.03,2 +62.063,51.18568488,-2.26427250,493.67,33.82,267.96,183.76,182.72,3.02,49.81,551.49,97.17,2 +62.907,51.18564620,-2.26540903,470.29,23.91,265.29,184.83,184.11,1.77,-5.14,554.13,97.38,2 +63.641,51.18559403,-2.26641946,458.35,25.97,267.21,185.25,184.76,0.67,-51.46,556.36,97.57,2 +64.391,51.18557710,-2.26746286,446.33,26.18,272.03,185.54,185.03,0.57,-34.35,558.33,97.77,2 +65.204,51.18561162,-2.26853853,437.79,29.29,274.68,185.70,185.29,-0.62,-8.69,560.04,97.97,2 +65.891,51.18566313,-2.26950841,435.69,39.38,275.10,185.62,185.22,-1.12,11.07,561.76,98.16,2 +66.594,51.18571348,-2.27049532,434.55,53.40,274.23,185.43,185.10,-1.43,58.80,563.48,98.35,2 +67.235,51.18574188,-2.27137750,427.67,51.83,268.65,186.30,185.92,-0.60,78.18,1083.66,98.41,2 +67.844,51.18572226,-2.27218900,416.64,36.32,255.52,187.43,187.44,-0.75,77.16,1172.03,98.17,2 +68.422,51.18559186,-2.27297303,407.01,26.86,232.43,185.19,187.26,-6.02,73.48,1167.84,97.91,2 +69.032,51.18528404,-2.27359822,412.27,32.03,204.00,177.80,180.20,-11.49,77.67,1163.82,97.66,2 +69.641,51.18483756,-2.27394167,429.62,44.81,184.33,175.33,175.88,-11.45,83.97,1160.38,97.39,2 +70.172,51.18437084,-2.27401172,446.34,58.78,170.25,174.92,175.21,-8.81,90.13,1156.32,97.14,2 +70.750,51.18391556,-2.27390087,456.43,68.62,160.85,176.11,176.44,-5.49,90.44,1151.93,96.88,2 +71.266,51.18351250,-2.27369746,458.71,70.69,160.16,178.92,178.81,-2.06,61.19,1147.86,96.64,2 +71.891,51.18302823,-2.27340633,454.99,66.78,158.93,182.00,181.59,1.10,12.76,588.98,96.52,2 +72.485,51.18256354,-2.27311563,447.54,58.70,158.40,182.06,181.58,1.22,-33.26,548.88,96.67,2 +72.985,51.18217466,-2.27287725,439.23,48.09,159.69,182.58,182.01,1.66,-48.49,552.26,96.79,2 +73.500,51.18176667,-2.27265556,428.62,36.30,163.62,183.17,182.58,0.22,-25.93,550.85,96.92,2 +74.079,51.18128114,-2.27243177,422.94,30.88,165.01,183.09,182.70,-0.89,-16.25,552.15,97.06,2 +74.672,51.18080470,-2.27223494,421.44,29.42,166.32,182.98,182.61,-1.74,-19.34,554.06,97.20,3 +75.204,51.18037269,-2.27207279,422.45,30.55,167.71,182.81,182.42,-2.23,-23.13,555.69,97.33,3 +75.813,51.17987505,-2.27190714,424.74,32.81,168.76,182.64,182.25,-1.95,-23.53,556.59,97.48,3 +76.407,51.17935890,-2.27175417,426.37,34.50,169.93,182.48,182.09,-1.76,6.49,557.47,97.63,3 +76.891,51.17896194,-2.27164040,427.56,33.46,169.74,182.30,181.97,-2.27,40.32,558.49,97.75,3 +77.454,51.17849133,-2.27148711,427.26,25.24,163.42,181.74,181.86,-4.62,64.20,559.69,97.89,3 +77.938,51.17810698,-2.27129867,428.62,26.75,154.22,181.55,181.88,-5.98,72.02,1144.77,97.80,3 +78.422,51.17774454,-2.27101995,432.44,30.59,140.60,181.44,182.27,-7.12,79.62,1163.57,97.56,3 +78.985,51.17740366,-2.27056068,438.71,36.90,118.24,177.68,179.51,-7.39,86.12,1159.19,97.32,3 +79.579,51.17714067,-2.26982461,443.73,41.79,105.36,178.16,178.62,-4.29,85.35,1154.20,97.03,3 +80.172,51.17700661,-2.26907219,442.64,40.55,96.95,180.68,180.87,-2.82,75.89,1150.16,96.76,3 +80.750,51.17694850,-2.26833842,439.02,35.27,92.62,182.96,182.76,-1.24,58.43,631.65,96.61,3 +81.344,51.17693200,-2.26756718,433.32,33.45,89.83,183.07,182.73,-1.08,31.17,553.09,96.74,3 +81.907,51.17693963,-2.26679261,431.38,36.51,85.13,182.60,182.71,-6.59,38.87,553.41,96.88,3 +82.485,51.17699560,-2.26599882,444.27,49.91,78.28,180.32,179.37,-9.84,59.44,552.38,97.03,3 +82.969,51.17708330,-2.26536653,459.70,65.24,74.46,179.12,178.33,-8.99,73.18,553.13,97.15,3 +83.485,51.17720440,-2.26470256,472.70,62.58,69.08,177.87,177.65,-7.42,81.30,554.32,97.29,3 +84.000,51.17734682,-2.26410342,480.41,46.28,65.74,177.52,177.49,-5.22,72.73,554.80,97.41,3 +84.610,51.17755708,-2.26338511,485.79,40.24,56.97,177.36,177.66,-6.43,64.20,1083.40,97.45,3 +85.266,51.17784950,-2.26269167,495.58,38.87,43.83,177.50,177.94,-9.70,69.26,1155.91,97.19,3 +85.891,51.17822513,-2.26212250,511.77,38.04,32.16,178.24,178.20,-9.28,80.32,1150.28,96.90,3 +86.485,51.17866052,-2.26170199,525.92,31.02,21.19,179.01,179.20,-7.42,84.48,1094.50,96.63,3 +87.063,51.17910039,-2.26141925,533.72,33.30,16.45,179.20,179.48,-4.43,92.11,555.63,96.72,3 +87.610,51.17952887,-2.26121203,533.65,32.46,14.56,179.29,179.38,-1.30,54.27,547.42,96.85,3 +88.297,51.18006687,-2.26100162,529.40,29.95,13.14,179.61,179.54,-1.46,-19.36,549.06,97.01,3 +88.922,51.18058723,-2.26079229,530.34,29.66,19.10,181.31,181.47,-5.50,-52.88,1146.48,96.81,3 +89.469,51.18103638,-2.26053283,538.76,35.02,24.49,182.59,182.25,-5.98,-55.96,1144.28,96.56,3 +90.016,51.18141904,-2.26024036,547.37,34.44,25.58,184.55,184.23,-4.05,-26.54,1140.38,96.35,3 +90.563,51.18187018,-2.25989000,554.91,30.67,26.85,186.81,186.50,-3.49,27.45,1135.81,96.10,3 +91.172,51.18234139,-2.25952750,561.77,25.54,23.39,188.75,188.68,-5.29,67.73,1131.75,95.85,3 +91.750,51.18282936,-2.25921810,566.75,21.59,11.63,189.84,190.43,-5.22,83.83,1127.31,95.60,3 +92.250,51.18325109,-2.25908878,568.72,22.79,352.50,186.95,188.83,-4.81,86.99,1124.08,95.38,3 +92.735,51.18366851,-2.25915316,568.16,25.00,337.36,186.58,188.07,-3.49,86.13,1120.76,95.16,3 +93.266,51.18409461,-2.25941841,564.38,25.40,326.54,186.72,187.20,-1.74,80.73,1117.58,94.92,3 +93.938,51.18456856,-2.25992847,555.81,25.95,315.50,188.88,189.16,-1.12,76.75,1114.07,94.66,3 +94.500,51.18492483,-2.26047755,546.59,26.52,308.01,191.28,191.15,-0.43,75.03,1110.44,94.40,3 +95.172,51.18527714,-2.26123772,534.79,25.54,295.36,192.89,192.93,-1.04,74.39,1106.89,94.12,3 +95.875,51.18553323,-2.26214273,523.58,26.31,284.57,195.01,194.79,-0.58,75.87,1103.42,93.83,3 +96.469,51.18566486,-2.26298929,513.19,33.52,273.27,196.39,196.30,-0.79,76.63,1099.57,93.57,3 +97.141,51.18569987,-2.26395430,500.99,37.44,268.70,199.07,198.29,1.54,56.32,1096.03,93.29,3 +97.797,51.18567974,-2.26486808,486.81,33.46,266.72,202.06,200.95,2.39,-30.13,1092.80,93.02,3 +98.500,51.18565206,-2.26594852,469.32,29.72,268.65,205.31,203.86,3.19,-22.22,1087.96,92.70,3 +99.188,51.18564387,-2.26695905,451.50,25.82,269.86,208.15,206.69,2.90,-13.00,1084.49,92.41,3 +99.875,51.18564451,-2.26803891,436.70,22.34,272.58,210.52,209.35,-0.10,-35.11,1080.82,92.09,3 +100.610,51.18568451,-2.26918340,429.29,28.37,274.56,212.64,211.41,0.33,-19.57,1075.87,91.76,3 +101.250,51.18574115,-2.27020611,423.68,38.95,275.51,214.34,213.11,-0.06,37.43,1072.18,91.48,3 +101.875,51.18578722,-2.27121176,416.94,42.00,271.31,215.71,214.57,-1.15,64.62,1068.50,91.20,3 +102.469,51.18578004,-2.27215558,412.46,32.68,255.23,214.52,214.53,-5.02,77.06,1064.51,90.94,3 +102.969,51.18565291,-2.27292263,415.65,35.84,232.76,209.47,210.73,-6.64,83.65,1061.80,90.71,3 +103.594,51.18529395,-2.27364184,423.37,43.12,203.81,201.30,203.05,-6.45,87.92,1058.24,90.45,3 +104.235,51.18475424,-2.27401428,428.52,41.24,183.96,198.21,198.68,-4.00,87.30,1054.49,90.17,3 +104.782,51.18427530,-2.27408645,427.42,39.27,175.29,198.71,198.49,-1.99,83.68,1051.31,89.94,3 +105.391,51.18370560,-2.27400006,421.20,33.00,167.28,199.92,199.40,-1.45,66.32,1047.66,89.67,3 +106.016,51.18315381,-2.27378972,414.70,26.47,162.31,201.72,201.03,-1.46,51.16,1044.21,89.41,3 +106.625,51.18261805,-2.27350431,410.43,22.30,160.15,203.51,202.66,-1.63,28.51,1040.27,89.15,3 +107.219,51.18207960,-2.27317627,410.53,19.33,159.12,204.97,204.01,-1.85,-1.91,1036.66,88.88,3 +107.782,51.18158586,-2.27286864,413.50,21.67,159.53,206.13,205.09,-1.93,-23.77,1033.29,88.65,3 +108.454,51.18097009,-2.27250694,417.47,25.58,162.07,207.42,206.38,-2.09,-37.30,1028.81,88.35,4 +109.000,51.18048446,-2.27225690,421.64,29.83,164.26,208.19,207.09,-3.06,-36.53,1025.13,88.12,4 +109.610,51.17992043,-2.27201252,428.11,36.31,165.72,208.98,207.89,-3.01,-28.93,1021.19,87.85,4 +110.094,51.17945599,-2.27183128,433.35,41.57,166.86,209.66,208.53,-2.90,-7.97,1017.65,87.64,4 +110.657,51.17892254,-2.27163451,440.22,45.89,167.28,210.29,209.16,-3.38,35.34,1014.12,87.39,4 +111.250,51.17836195,-2.27141436,445.82,43.92,163.41,210.78,209.97,-4.13,77.41,1009.94,87.14,4 +111.735,51.17790918,-2.27118327,447.98,46.06,146.93,208.88,209.26,-5.02,83.52,1006.32,86.90,4 +112.266,51.17750083,-2.27074491,450.92,49.01,126.61,203.45,204.41,-4.95,86.03,1002.79,86.66,4 +112.891,51.17715519,-2.26999934,452.10,50.03,106.92,201.04,201.94,-3.34,87.52,999.15,86.38,4 +113.500,51.17697308,-2.26912554,447.77,45.52,95.91,201.14,200.92,-1.20,84.71,995.45,86.10,4 +114.063,51.17691627,-2.26828261,438.90,34.31,88.07,201.91,201.33,-0.68,70.98,991.90,85.83,4 +114.657,51.17693837,-2.26740507,429.03,29.64,83.90,203.44,202.63,-0.86,47.96,988.15,85.56,4 +115.219,51.17700213,-2.26661351,424.05,29.05,79.38,204.23,203.69,-4.20,38.36,985.32,85.33,4 +115.844,51.17711623,-2.26573422,431.32,35.51,75.24,204.48,203.41,-6.01,37.97,981.56,85.06,4 +116.438,51.17728061,-2.26483495,446.87,38.42,71.37,204.49,203.01,-7.95,45.77,977.67,84.79,4 +116.954,51.17745124,-2.26407560,464.93,34.37,67.24,204.09,202.37,-9.12,53.47,973.41,84.55,4 +117.469,51.17764799,-2.26336511,485.17,41.98,62.09,203.49,201.56,-10.01,68.70,969.85,84.31,4 +117.985,51.17787253,-2.26272050,505.68,49.76,48.76,201.46,200.52,-10.53,87.54,965.26,84.09,4 +118.688,51.17828956,-2.26198813,528.23,43.82,32.10,198.58,198.28,-7.39,89.25,961.07,83.79,4 +119.360,51.17882334,-2.26147311,540.03,39.33,19.24,197.46,197.57,-4.19,89.64,955.64,83.49,4 +119.907,51.17930166,-2.26119102,540.85,35.51,15.64,198.46,198.28,-1.58,87.72,951.96,83.25,4 +120.547,51.17985307,-2.26094877,532.89,28.28,12.71,200.04,199.47,0.79,50.40,947.96,82.99,4 +121.282,51.18051242,-2.26072829,522.07,19.06,13.40,201.56,201.33,-4.84,-22.52,944.18,82.68,4 +121.938,51.18110660,-2.26047771,541.19,37.72,20.86,199.80,197.30,-11.09,-54.86,939.80,82.38,4 +122.500,51.18160219,-2.26018100,570.46,61.02,23.04,199.18,196.69,-9.72,-52.20,936.35,82.13,4 +123.110,51.18204967,-2.25987195,596.35,72.36,24.91,198.65,196.52,-8.87,31.38,931.84,81.91,4 +123.782,51.18264294,-2.25945487,624.41,86.59,20.58,197.85,196.98,-9.07,87.78,926.88,81.59,4 +124.516,51.18326036,-2.25912674,635.20,88.88,348.61,190.41,194.88,8.48,113.43,920.67,81.26,4 +125.313,51.18392156,-2.25933214,580.70,39.69,329.92,188.19,183.35,8.87,70.08,916.46,80.92,4 +126.266,51.18454474,-2.26007482,542.88,17.97,306.33,183.24,183.86,-8.72,34.97,914.11,80.56,4 +127.063,51.18495798,-2.26099145,562.46,50.40,306.65,184.71,184.59,-5.12,86.11,909.87,80.19,4 +127.860,51.18535680,-2.26188443,563.92,63.48,299.45,186.07,186.34,-0.01,102.18,904.12,79.80,4 +128.719,51.18568946,-2.26291630,538.64,57.23,271.55,182.24,182.52,3.96,80.40,898.26,79.43,4 +129.625,51.18572891,-2.26410033,499.03,37.05,267.16,187.14,185.65,3.14,31.51,893.97,79.04,4 +130.485,51.18567881,-2.26530594,474.92,27.05,265.94,190.40,189.47,2.56,-21.39,889.94,78.66,4 +131.329,51.18563431,-2.26651810,458.94,28.13,270.02,192.77,192.09,0.70,-43.36,885.14,78.28,4 +132.094,51.18564116,-2.26762648,448.76,30.12,274.63,194.51,193.81,0.62,-43.72,879.87,77.91,4 +132.829,51.18569676,-2.26869150,439.43,32.17,276.16,196.20,195.40,1.05,11.39,875.04,77.56,4 +133.469,51.18575343,-2.26962202,431.18,36.39,275.30,197.54,196.70,0.76,32.06,870.89,77.26,4 +134.125,51.18579792,-2.27058191,421.03,40.77,273.23,198.91,197.95,0.95,52.45,866.67,76.96,4 +134.797,51.18581388,-2.27157953,408.68,33.15,263.11,199.28,198.98,-2.19,72.31,862.22,76.64,4 +135.407,51.18572800,-2.27248294,402.35,22.25,241.71,196.14,197.28,-4.58,79.07,858.92,76.37,4 +136.063,51.18544763,-2.27324839,403.53,23.67,210.47,187.47,189.91,-7.29,83.17,855.18,76.10,4 +136.735,51.18496005,-2.27370563,409.91,25.38,193.49,183.29,183.54,-5.19,78.47,851.82,75.80,4 +137.438,51.18437947,-2.27390500,414.61,26.77,177.13,182.09,182.82,-6.82,76.84,846.68,75.49,4 +138.063,51.18387767,-2.27386745,421.11,33.36,166.99,181.45,181.61,-5.45,79.49,842.44,75.21,4 +138.735,51.18329851,-2.27366196,423.87,35.80,164.46,182.90,182.63,-1.99,63.42,837.36,74.89,4 +139.391,51.18280368,-2.27342695,420.00,31.78,162.18,184.47,184.00,-0.13,27.58,833.20,74.62,4 +140.110,51.18219766,-2.27309978,414.95,24.13,160.45,186.16,185.64,-1.02,8.63,828.08,74.27,4 +140.657,51.18174548,-2.27283772,415.34,23.60,159.83,186.86,186.41,-4.00,6.79,824.62,74.01,4 +141.329,51.18119409,-2.27251167,426.42,35.16,159.13,187.02,186.07,-6.83,6.85,820.12,73.70,4 diff --git a/track_data/flight_20260831_211521_021092.csv b/track_data/flight_20260831_211521_021092.csv new file mode 100644 index 00000000..5a565d09 --- /dev/null +++ b/track_data/flight_20260831_211521_021092.csv @@ -0,0 +1,235 @@ +# aircraft_title=Extra 300S Paint2 +# recorded_at=2026-08-31T21:15:21 +# laps_s=29.629,34.612,33.730,32.955 +timestamp_s,latitude,longitude,altitude_ft,agl_ft,heading_deg,airspeed_kts,groundspeed_kts,pitch_deg,bank_deg,torque,health_pct,stick_pitch,stick_roll,stick_yaw,stick_throttle,lap +0.344,51.18974567,-2.27594856,981.41,617.81,166.17,183.66,182.54,6.43,0.12,1099.03,100.00,-0.0840,0.0000,-0.0469,1.0000,1 +0.860,51.18930562,-2.27577837,959.38,590.27,166.04,187.48,186.12,7.68,0.15,1169.79,100.00,-0.2754,0.0000,-0.0098,1.0000,1 +1.391,51.18886394,-2.27560524,932.80,560.32,165.82,191.43,188.78,11.44,0.19,1173.93,100.00,-0.2910,0.0000,-0.0332,1.0000,1 +1.969,51.18837439,-2.27541210,891.70,514.76,165.75,196.46,190.81,15.03,0.20,1174.54,100.00,-0.2773,0.0000,-0.0410,1.0000,1 +2.641,51.18779412,-2.27518196,827.06,445.42,165.81,202.94,193.43,17.33,0.22,1177.23,100.00,-0.0254,0.0000,0.0000,1.0000,1 +3.188,51.18731175,-2.27499147,767.40,385.17,165.86,208.52,198.16,17.05,0.22,1178.41,100.00,0.0000,0.0000,-0.1309,1.0000,1 +3.782,51.18678755,-2.27478077,704.18,321.92,165.88,213.83,203.41,16.74,0.22,1180.57,100.00,0.1255,0.0432,-0.0566,1.0000,1 +4.360,51.18623921,-2.27456293,639.38,257.28,165.97,219.21,209.17,14.93,0.23,1184.21,100.00,0.2490,0.0608,-0.0020,1.0000,1 +4.907,51.18573512,-2.27435995,587.22,205.75,166.10,222.96,215.67,11.26,0.22,1183.90,100.00,0.2432,0.0608,-0.0039,1.0000,1 +5.469,51.18518807,-2.27414083,542.92,160.96,166.00,226.73,221.17,9.91,0.24,1187.84,100.00,0.1588,0.0608,-0.0137,1.0000,1 +6.016,51.18464013,-2.27392214,504.06,114.65,165.98,230.01,224.93,8.63,0.24,1187.26,100.00,0.1784,0.0608,-0.0098,1.0000,1 +6.563,51.18407910,-2.27369809,470.70,81.63,166.06,232.54,228.72,5.90,0.23,1190.04,100.00,0.2020,0.0608,0.0000,1.0000,1 +7.079,51.18353533,-2.27348131,447.00,58.25,166.06,234.65,231.67,3.49,0.23,1189.38,100.00,0.2843,0.0608,0.0000,1.0000,1 +7.688,51.18290051,-2.27322737,431.47,43.17,166.07,236.12,234.05,0.73,0.24,1191.38,100.00,0.0941,0.1353,0.0000,1.0000,1 +8.297,51.18226333,-2.27297410,425.59,35.13,166.01,237.33,235.30,-0.42,0.24,1191.33,100.00,0.1275,0.1334,0.0000,1.0000,1 +8.891,51.18164482,-2.27272721,424.66,32.69,165.98,238.20,236.16,-1.22,0.25,1191.34,100.00,0.0941,0.1334,0.0000,1.0000,1 +9.485,51.18100056,-2.27247148,427.33,35.47,165.95,238.87,236.78,-2.07,-2.25,1191.03,100.00,0.0000,0.1804,0.0000,1.0000,1 +10.079,51.18037943,-2.27222467,433.42,41.70,166.11,239.30,237.14,-2.91,-3.77,1190.70,100.00,-0.0508,-0.0957,-0.0215,1.0000,1 +10.688,51.17973735,-2.27197396,443.23,51.63,166.39,239.59,237.29,-3.75,4.26,1190.09,100.00,-0.1055,-0.3203,-0.0312,1.0000,1 +11.329,51.17903981,-2.27169859,457.33,64.96,166.04,239.59,237.32,-4.75,51.89,1189.47,100.00,0.0000,-0.5332,-0.0918,1.0000,1 +11.829,51.17849227,-2.27146630,466.80,66.30,162.18,239.57,237.68,-4.58,86.03,1188.77,100.00,0.3941,-0.1699,-0.0312,1.0000,1 +12.313,51.17797485,-2.27118818,471.42,69.48,147.92,237.93,237.18,-3.08,90.68,1188.22,100.00,0.6039,0.0000,-0.0332,1.0000,1 +12.860,51.17749758,-2.27070584,469.81,67.45,126.42,233.31,233.41,-0.88,92.04,1187.83,100.00,0.5863,0.0000,-0.0332,1.0000,1 +13.407,51.17714107,-2.26997351,458.94,56.49,109.84,230.98,229.88,1.28,85.26,1188.31,100.00,0.5490,0.2863,0.0000,1.0000,1 +14.016,51.17692264,-2.26900293,441.92,39.42,95.24,230.29,228.88,0.02,69.29,1188.72,100.00,0.5117,0.2726,0.0000,1.0000,1 +14.579,51.17686760,-2.26808244,434.13,28.21,84.28,229.73,228.67,-3.53,59.40,1190.11,100.00,0.3882,0.2765,0.0000,1.0000,1 +15.235,51.17694618,-2.26695846,440.00,43.10,78.68,230.24,228.52,-4.70,47.04,1190.33,100.00,0.2157,0.2530,0.0000,1.0000,1 +15.829,51.17706961,-2.26603098,451.83,57.33,76.25,230.72,228.66,-4.74,35.22,1190.46,100.00,0.0569,0.0000,-0.0195,1.0000,1 +16.375,51.17721845,-2.26509106,465.71,65.35,73.24,230.81,228.69,-7.18,46.55,1190.15,100.00,0.2706,-0.6055,-0.1621,1.0000,1 +16.969,51.17742132,-2.26410630,486.34,59.08,66.48,230.48,228.36,-7.65,79.94,1189.34,100.00,0.3784,-0.0332,-0.0547,1.0000,1 +17.454,51.17762754,-2.26335831,501.19,59.09,57.87,229.61,227.97,-6.91,83.10,1188.17,100.00,0.3961,0.0451,-0.0371,1.0000,1 +18.032,51.17792859,-2.26261712,515.35,57.56,42.09,227.51,226.71,-6.72,85.71,1187.76,100.00,0.6078,0.0353,-0.0488,1.0000,1 +18.594,51.17838876,-2.26194776,528.19,41.19,26.57,225.14,224.52,-5.31,86.96,1186.70,100.00,0.4392,0.2137,-0.0430,1.0000,1 +19.188,51.17894060,-2.26151915,535.83,37.56,16.36,224.39,223.54,-3.70,77.75,1186.82,100.00,0.0000,0.4569,0.0000,1.0000,1 +19.813,51.17950998,-2.26125016,539.43,39.55,14.26,225.74,224.57,-1.79,37.28,1186.20,100.00,-0.0879,0.4765,0.0000,1.0000,1 +20.344,51.18006383,-2.26103120,541.83,43.46,12.84,226.91,225.64,-1.94,-7.33,1186.21,100.00,0.1353,0.5176,0.0000,1.0000,1 +20.907,51.18065313,-2.26080475,546.65,47.29,15.13,227.71,226.50,-4.41,-55.34,1186.03,100.00,0.3647,0.4549,0.0000,1.0000,1 +21.485,51.18126283,-2.26050563,556.15,52.84,25.00,227.48,226.29,-5.47,-65.78,1185.70,100.00,0.2863,-0.4707,-0.1582,1.0000,1 +22.094,51.18180376,-2.26009646,570.26,51.70,27.28,227.99,226.24,-4.88,-0.91,1185.54,100.00,0.0000,-0.6367,-0.1777,1.0000,1 +22.688,51.18238642,-2.25963444,586.19,51.16,26.87,228.30,226.80,-5.52,71.13,1184.48,100.00,0.0687,-0.5625,-0.1777,1.0000,1 +23.266,51.18294074,-2.25920947,595.65,50.43,11.82,227.31,227.54,-1.24,103.77,1184.40,100.00,0.8078,0.2961,0.0000,1.0000,1 +23.797,51.18346268,-2.25904650,585.75,39.63,342.77,216.41,217.89,1.78,83.46,1183.48,100.00,0.5098,0.2843,-0.0625,1.0000,1 +24.454,51.18405703,-2.25932231,565.02,24.68,321.27,214.70,215.63,-1.36,74.78,1184.24,100.00,0.5823,0.1373,-0.0840,1.0000,1 +25.047,51.18453398,-2.25990338,555.05,24.96,312.26,215.31,214.72,-0.78,68.79,1185.91,99.97,0.2726,0.3255,-0.0469,1.0000,1 +25.641,51.18491251,-2.26057064,547.94,30.22,305.86,217.10,216.31,-1.47,62.93,1182.73,99.70,0.4059,-0.0742,-0.1758,1.0000,1 +26.188,51.18526130,-2.26135786,544.58,37.41,296.32,217.81,217.33,-2.67,82.57,1178.93,99.42,0.4059,-0.4590,-0.1680,1.0000,1 +26.922,51.18555454,-2.26237694,537.71,45.63,280.75,218.23,217.69,1.13,93.15,1174.78,99.10,0.4902,0.3706,0.0000,1.0000,1 +27.532,51.18568039,-2.26333832,519.83,46.62,272.04,219.69,217.99,2.93,73.10,1170.45,98.80,0.2941,0.3196,0.0000,1.0000,1 +28.110,51.18570051,-2.26430329,499.16,39.78,267.74,221.99,219.98,3.55,55.42,1167.03,98.51,0.0334,0.3628,0.0000,1.0000,1 +28.766,51.18566842,-2.26531971,476.91,28.84,265.38,223.66,221.43,4.05,10.89,573.92,98.48,0.0922,0.5490,0.0000,1.0000,1 +29.469,51.18561232,-2.26647378,455.42,23.82,267.52,222.80,221.15,-0.80,-28.45,565.71,98.68,0.4529,0.1157,-0.0391,1.0000,1 +30.204,51.18559368,-2.26767013,454.51,36.34,271.29,219.75,218.53,-2.76,-49.72,568.27,98.89,-0.0488,0.3000,0.0000,1.0000,1 +30.844,51.18562029,-2.26872054,457.27,50.95,273.37,217.61,216.51,-1.59,-58.18,569.51,99.07,-0.2266,-0.1035,0.0000,1.0000,1 +31.422,51.18566032,-2.26963627,454.13,59.72,274.46,216.25,215.07,0.72,-22.90,570.33,99.23,-0.0098,-0.6816,0.0000,1.0000,1 +32.032,51.18571107,-2.27058689,447.33,66.81,275.52,214.95,213.83,0.10,52.28,571.64,99.39,-0.0508,-0.5684,0.0000,1.0000,1 +32.625,51.18575517,-2.27154651,437.25,60.84,272.20,215.11,213.77,0.82,68.92,1169.19,99.32,0.2294,-0.0527,-0.0391,1.0000,1 +33.188,51.18576387,-2.27243863,425.28,44.97,252.08,213.50,214.34,-4.38,76.98,1182.05,99.06,0.9333,-0.2793,-0.0312,1.0000,1 +33.766,51.18558573,-2.27327555,425.33,45.60,220.21,201.49,203.96,-7.37,84.46,1177.09,98.79,0.6784,0.0000,-0.0645,1.0000,1 +34.329,51.18520863,-2.27380645,431.35,50.25,198.28,197.77,199.07,-5.90,87.63,1173.51,98.52,0.6569,0.1039,0.0000,1.0000,1 +34.907,51.18471772,-2.27408199,434.48,46.79,177.41,194.98,196.26,-3.80,89.92,1169.62,98.25,0.6471,0.0000,-0.0312,1.0000,1 +35.469,51.18419136,-2.27407140,430.80,42.54,171.83,196.56,195.95,-0.18,84.69,1165.93,97.98,0.1137,0.3529,0.0000,1.0000,1 +36.032,51.18370974,-2.27395729,420.54,32.08,166.93,198.45,197.49,1.04,64.76,582.78,97.94,0.3843,0.2647,-0.0703,1.0000,1 +36.594,51.18321739,-2.27376475,408.94,20.41,161.16,197.79,197.04,-0.71,46.91,563.21,98.09,0.3372,0.3431,0.0000,1.0000,1 +37.266,51.18262612,-2.27342584,404.69,16.73,157.50,196.90,196.24,-2.12,12.50,565.04,98.28,0.0883,0.4294,0.0000,1.0000,1 +37.844,51.18214183,-2.27310390,407.64,16.84,156.87,196.27,195.50,-2.40,-21.16,565.59,98.43,0.1314,0.4235,0.0000,1.0000,1 +38.422,51.18165326,-2.27278237,411.40,19.54,158.55,195.35,194.67,-3.19,-43.22,565.96,98.60,0.1863,0.3412,0.0000,1.0000,1 +38.985,51.18118402,-2.27250733,414.85,22.96,163.07,194.30,193.79,-4.14,-58.23,567.13,98.75,0.3177,0.2275,0.0000,1.0000,1 +39.500,51.18073653,-2.27230025,418.72,26.84,166.32,193.38,192.85,-3.28,-58.74,568.63,98.88,0.0000,-0.3223,0.0000,1.0000,2 +40.141,51.18018667,-2.27209801,420.91,28.95,168.39,192.87,192.26,-1.57,-7.83,569.82,99.05,0.0000,-0.3379,-0.0605,1.0000,2 +40.735,51.17968032,-2.27193703,422.32,30.39,168.72,192.38,191.79,-1.89,24.09,570.84,99.21,0.1843,-0.1230,-0.0762,1.0000,2 +41.297,51.17917975,-2.27176241,423.97,32.27,167.17,191.80,191.27,-2.53,38.24,572.00,99.36,0.1824,-0.3184,-0.1074,1.0000,2 +41.891,51.17866293,-2.27155521,425.57,26.58,163.71,191.12,190.74,-3.38,59.77,573.37,99.53,0.3902,-0.2715,-0.1055,1.0000,2 +42.485,51.17815881,-2.27129124,426.87,24.95,153.48,189.98,190.04,-5.06,74.15,939.75,99.65,0.5059,-0.2363,-0.1484,1.0000,2 +42.969,51.17777165,-2.27098299,429.24,27.33,139.89,189.08,189.79,-5.58,81.99,1187.77,99.43,0.5529,0.0000,-0.0762,1.0000,2 +43.532,51.17741570,-2.27051632,431.82,29.90,123.15,188.10,189.09,-5.31,83.06,1185.53,99.18,0.6059,0.2432,-0.0352,1.0000,2 +44.125,51.17712628,-2.26980980,434.16,32.23,105.59,186.29,187.08,-4.70,80.43,1180.32,98.89,0.1039,0.3804,-0.1211,1.0000,2 +44.672,51.17697756,-2.26906408,434.59,32.54,101.04,189.23,188.96,-2.95,73.35,1175.95,98.61,0.5255,-0.2051,-0.1484,1.0000,2 +45.266,51.17688487,-2.26824152,432.93,27.38,91.30,190.91,190.87,-3.23,74.37,1171.32,98.31,0.2941,0.1079,-0.0527,1.0000,2 +45.782,51.17687046,-2.26753096,431.01,29.25,83.99,192.74,192.60,-2.85,74.62,1167.63,98.06,0.3529,0.1236,0.0000,1.0000,2 +46.407,51.17692393,-2.26666012,427.37,32.15,80.48,194.15,193.59,-1.09,46.88,566.45,98.11,0.4000,0.3608,0.0000,1.0000,2 +47.047,51.17702927,-2.26577823,426.73,32.00,74.48,192.50,192.22,-6.03,36.91,562.36,98.28,0.2549,-0.0195,-0.0371,1.0000,2 +47.625,51.17717539,-2.26499086,439.15,40.57,71.18,190.77,189.75,-7.53,39.63,564.44,98.44,0.2412,-0.2695,-0.0410,1.0000,2 +48.172,51.17733992,-2.26425497,455.68,25.81,66.04,188.97,187.91,-9.72,63.96,566.38,98.59,0.3569,-0.4062,-0.0293,1.0000,2 +48.672,51.17752858,-2.26360696,472.31,31.27,60.85,188.61,187.63,-8.89,75.41,1156.11,98.50,0.2314,0.0000,-0.0430,1.0000,2 +49.188,51.17773846,-2.26299952,486.21,36.43,55.82,189.61,188.95,-7.56,77.69,1170.94,98.27,0.6157,0.0000,-0.0410,1.0000,2 +49.860,51.17807617,-2.26226872,500.59,32.35,37.30,188.64,189.19,-8.62,81.60,1165.23,97.96,0.5941,-0.1914,-0.0742,1.0000,2 +50.516,51.17854302,-2.26172195,515.34,23.24,19.37,187.05,187.39,-7.48,85.24,1159.14,97.64,0.3647,0.0000,-0.0781,1.0000,2 +51.063,51.17898990,-2.26145610,524.04,23.97,14.01,188.62,188.56,-4.79,81.72,1154.44,97.38,-0.1152,0.4922,0.0000,1.0000,2 +51.625,51.17943296,-2.26127291,526.76,26.45,13.95,190.05,189.80,-0.99,34.55,562.59,97.42,0.0000,0.6157,0.0000,1.0000,2 +52.219,51.17994574,-2.26108005,526.01,27.80,13.12,189.23,189.09,-4.14,-29.77,554.06,97.58,0.5412,0.5078,0.0000,1.0000,2 +52.797,51.18046138,-2.26086013,535.76,38.03,23.34,186.42,186.12,-8.98,-59.08,556.18,97.76,0.1608,0.0667,-0.0312,1.0000,2 +53.344,51.18089830,-2.26056749,552.55,49.30,24.64,185.31,184.56,-7.20,-61.80,558.40,97.90,-0.1504,0.0000,0.0000,1.0000,2 +53.907,51.18131497,-2.26025047,564.87,53.49,26.43,184.47,184.16,-5.18,-31.72,558.20,98.05,0.0000,-0.7559,-0.0410,1.0000,2 +54.532,51.18179829,-2.25985593,576.22,53.01,27.89,183.64,183.43,-4.77,61.41,558.77,98.22,0.0687,-0.5996,-0.0391,1.0000,2 +55.047,51.18219906,-2.25953765,581.15,44.64,23.12,182.80,183.19,-4.33,82.92,559.77,98.37,0.4235,0.0138,-0.0742,1.0000,2 +55.594,51.18262405,-2.25925789,580.75,36.07,11.64,182.96,183.71,-2.91,84.50,1144.33,98.30,0.6647,0.1981,-0.0586,1.0000,2 +56.172,51.18310936,-2.25910268,575.68,29.05,358.05,183.58,184.33,-2.01,79.31,1163.31,98.02,0.6118,0.3177,-0.0078,1.0000,2 +56.782,51.18361868,-2.25914576,569.35,25.80,341.47,183.65,184.70,-3.37,77.38,1158.18,97.73,0.5765,0.0000,-0.0840,1.0000,2 +57.391,51.18411059,-2.25940894,565.37,26.28,329.17,184.91,185.50,-2.85,78.85,1154.09,97.45,0.4647,0.0000,-0.1152,1.0000,2 +58.032,51.18456976,-2.25986731,560.42,29.49,313.44,185.88,186.83,-2.96,80.87,1150.00,97.16,0.5647,0.0000,-0.0820,1.0000,2 +58.625,51.18491327,-2.26041912,554.96,34.31,303.04,185.83,186.30,-1.37,81.91,558.29,97.21,0.3098,0.1588,-0.0977,1.0000,2 +59.188,51.18519905,-2.26110040,545.41,34.34,295.24,185.34,185.33,0.28,80.69,551.63,97.38,0.3882,0.1098,-0.0566,1.0000,2 +59.875,51.18544184,-2.26192264,528.89,28.21,288.72,185.59,184.92,1.91,70.53,553.65,97.56,0.4039,0.0000,-0.1680,1.0000,2 +60.547,51.18562233,-2.26280628,508.45,24.42,280.52,185.87,185.10,2.13,70.07,556.00,97.75,0.4255,0.0726,-0.1172,1.0000,2 +61.141,51.18570985,-2.26357714,489.98,20.55,272.90,186.06,185.27,1.94,66.14,558.65,97.91,0.3804,0.2373,-0.0605,1.0000,2 +61.766,51.18573232,-2.26442963,471.57,13.27,267.20,186.91,186.03,1.89,45.93,750.74,98.07,0.2471,0.5274,0.0000,1.0000,2 +62.594,51.18568575,-2.26553273,453.66,8.78,264.44,190.88,190.06,1.79,-11.63,1164.23,97.72,0.1628,0.4902,0.0000,1.0000,2 +63.422,51.18562499,-2.26669220,442.43,14.07,267.30,192.30,191.84,-0.83,-44.63,563.94,97.73,0.1726,0.3784,0.0000,1.0000,2 +64.219,51.18561448,-2.26785717,437.58,21.81,274.25,191.29,190.96,-2.14,-52.56,562.35,97.97,0.0000,-0.1074,-0.0273,1.0000,2 +64.938,51.18566702,-2.26884176,435.22,30.20,276.37,191.02,190.51,-0.38,-19.90,563.22,98.17,0.0039,-0.6309,-0.0234,1.0000,2 +65.735,51.18575313,-2.26999220,429.77,41.79,276.84,190.77,190.25,-0.34,51.30,564.51,98.40,0.1177,-0.3223,-0.0820,1.0000,2 +66.469,51.18581153,-2.27100980,420.10,44.43,270.88,190.56,190.11,-1.02,66.08,566.13,98.61,0.3882,-0.2520,-0.1191,1.0000,2 +67.188,51.18579708,-2.27202267,411.54,31.79,255.60,190.40,190.62,-3.63,73.78,1167.01,98.51,0.5098,-0.0293,-0.0645,1.0000,2 +67.875,51.18562627,-2.27295725,410.18,30.25,231.86,188.53,190.01,-6.82,78.19,1171.55,98.16,0.8431,0.0000,-0.0469,1.0000,2 +68.610,51.18521809,-2.27368928,420.19,39.41,198.30,180.46,182.36,-9.43,83.82,1165.95,97.80,0.6412,0.0000,-0.0625,1.0000,2 +69.250,51.18471454,-2.27397478,432.76,45.99,179.34,179.38,180.28,-7.36,87.29,1161.04,97.49,0.5765,0.0000,-0.0742,1.0000,2 +69.844,51.18420432,-2.27398716,439.47,51.61,165.71,179.95,180.57,-4.56,89.43,1156.48,97.18,0.4510,0.0000,-0.0879,1.0000,2 +70.407,51.18373317,-2.27382081,438.61,50.46,162.34,181.62,181.47,-1.24,64.42,566.36,97.16,-0.0332,0.6431,-0.0664,1.0000,2 +71.032,51.18326341,-2.27357214,432.90,44.74,160.37,181.75,181.32,0.44,11.24,553.98,97.31,0.0098,0.4392,-0.0195,1.0000,2 +71.594,51.18281130,-2.27331497,427.58,39.42,159.83,181.87,181.44,0.30,-21.78,556.28,97.48,0.1569,0.4569,0.0000,1.0000,2 +72.266,51.18228213,-2.27301927,421.89,31.63,163.03,181.98,181.75,-1.99,-35.86,558.66,97.65,0.1177,0.2490,0.0000,1.0000,2 +72.844,51.18180804,-2.27280176,420.46,28.39,164.44,182.13,181.81,-1.15,-37.93,559.91,97.81,0.1490,-0.1875,-0.0195,1.0000,2 +73.500,51.18126734,-2.27258536,417.38,25.25,166.86,182.30,181.94,-0.76,-25.60,560.89,97.99,0.1039,-0.4551,-0.0273,1.0000,2 +74.094,51.18077404,-2.27241517,414.72,22.65,168.38,182.44,182.06,-1.67,-9.75,561.76,98.15,0.2824,-0.2383,-0.0605,1.0000,3 +74.750,51.18023962,-2.27224324,417.44,25.62,169.01,182.00,181.55,-3.96,8.97,563.47,98.32,0.1393,-0.3340,-0.1172,1.0000,3 +75.344,51.17976447,-2.27209373,425.19,33.53,168.25,181.43,180.90,-4.86,28.37,564.78,98.48,0.1765,-0.4336,-0.1484,1.0000,3 +75.891,51.17929119,-2.27192099,434.04,42.54,166.03,180.76,180.33,-5.41,55.51,566.26,98.64,0.2020,-0.3438,-0.1523,1.0000,3 +76.485,51.17881019,-2.27170898,441.09,45.66,161.28,180.05,179.96,-5.06,73.33,567.02,98.79,0.2628,-0.2793,-0.1602,1.0000,3 +77.047,51.17836244,-2.27145684,443.66,41.66,155.10,180.32,180.39,-3.45,81.59,1057.63,98.86,0.4196,-0.1602,-0.0762,1.0000,3 +77.547,51.17797842,-2.27117406,441.49,39.32,147.56,181.88,181.99,-2.01,83.51,1177.35,98.62,0.4686,0.0000,-0.0781,1.0000,3 +78.125,51.17759592,-2.27078542,434.26,31.77,132.44,183.13,183.63,-1.37,84.08,1173.41,98.36,0.7608,0.1373,-0.0723,1.0000,3 +78.735,51.17725371,-2.27018279,424.58,22.36,107.96,179.55,181.51,-4.48,73.21,1168.68,98.06,0.7000,0.3628,0.0000,1.0000,3 +79.297,51.17707924,-2.26945460,423.09,21.13,100.02,180.99,181.07,-3.93,67.55,1164.78,97.77,0.1079,0.3079,-0.0391,1.0000,3 +79.922,51.17698989,-2.26863121,423.33,19.00,96.55,184.26,183.94,-2.85,50.08,1160.73,97.46,0.1628,0.2843,-0.0645,1.0000,3 +80.454,51.17694320,-2.26789703,423.92,20.51,92.08,186.56,186.30,-4.20,53.29,1156.51,97.20,0.3353,-0.0527,-0.1504,1.0000,3 +81.079,51.17693470,-2.26703595,428.43,31.11,86.37,188.11,187.77,-4.88,62.91,590.12,97.08,0.3431,-0.2227,-0.1758,1.0000,3 +81.704,51.17698603,-2.26616128,435.16,40.42,76.44,186.01,185.96,-6.56,67.46,554.45,97.25,0.3824,0.0490,-0.1426,1.0000,3 +82.250,51.17709444,-2.26543631,442.80,47.15,72.97,185.17,184.85,-4.93,59.94,557.37,97.39,0.2196,0.3765,-0.0215,1.0000,3 +82.829,51.17723928,-2.26472096,449.08,34.70,68.86,184.56,184.25,-5.43,44.11,556.78,97.54,0.2373,0.0922,-0.0723,1.0000,3 +83.375,51.17741787,-2.26401125,457.61,23.22,65.67,183.66,183.23,-6.09,47.83,557.02,97.69,0.3804,0.0000,-0.0898,1.0000,3 +84.032,51.17765246,-2.26322624,472.91,24.21,58.84,182.67,181.65,-9.99,50.03,1082.32,97.74,0.3647,-0.5957,-0.2227,1.0000,3 +84.594,51.17791334,-2.26258903,494.37,34.46,43.58,181.37,180.81,-12.90,82.49,1159.66,97.46,0.6314,0.0726,-0.0762,1.0000,3 +85.204,51.17827889,-2.26203957,518.56,37.21,28.17,179.95,179.62,-9.26,95.85,1154.69,97.17,0.4765,-0.2012,-0.1777,1.0000,3 +85.797,51.17871786,-2.26165114,531.88,35.89,22.32,181.09,181.10,-4.82,93.65,592.50,97.04,0.1726,0.1530,-0.1641,1.0000,3 +86.282,51.17909212,-2.26139598,534.38,33.11,20.62,180.52,180.66,-2.15,84.75,553.76,97.17,0.1804,0.3726,-0.0664,1.0000,3 +86.766,51.17947328,-2.26116845,530.90,26.97,17.56,181.14,181.14,-0.42,65.10,598.53,97.29,0.1353,0.5529,0.0000,1.0000,3 +87.438,51.17999586,-2.26091189,522.06,18.41,15.60,184.07,183.78,-0.64,-13.60,1150.26,97.05,0.4726,0.5569,0.0000,1.0000,3 +88.016,51.18047904,-2.26067367,528.11,23.91,24.98,182.37,181.89,-15.02,-31.20,1147.16,96.77,0.1216,0.3922,0.0000,1.0000,3 +88.563,51.18089303,-2.26038174,561.60,54.90,24.54,183.38,179.39,-12.26,-11.92,1142.48,96.51,-0.2637,-0.7227,-0.1582,1.0000,3 +89.188,51.18135426,-2.26003306,596.18,82.54,25.85,184.20,181.81,-9.71,35.65,1138.28,96.24,-0.0215,-0.5039,-0.1680,1.0000,3 +89.750,51.18180688,-2.25969677,620.98,93.61,24.84,185.25,184.15,-9.71,98.90,1131.30,95.96,0.2333,-0.5898,-0.1797,1.0000,3 +90.297,51.18222614,-2.25939610,636.06,98.09,14.31,185.78,186.43,-3.37,114.96,1127.62,95.70,0.5686,0.1765,0.0000,1.0000,3 +90.860,51.18268733,-2.25918039,633.23,86.89,4.95,187.11,187.37,2.10,100.42,1121.91,95.43,0.5020,0.2314,0.0000,1.0000,3 +91.375,51.18314671,-2.25910869,616.15,69.38,351.51,188.20,187.77,3.91,79.87,1118.34,95.17,0.4824,0.4726,0.0000,1.0000,3 +91.969,51.18364776,-2.25921959,592.11,49.09,339.55,190.43,189.66,2.74,74.11,1114.83,94.91,0.6078,0.0000,-0.0215,1.0000,3 +92.610,51.18414213,-2.25953272,570.74,33.19,325.31,191.50,191.35,0.16,67.37,1112.26,94.62,0.4627,0.0000,-0.1328,1.0000,3 +93.204,51.18458935,-2.26002589,557.74,29.85,314.58,193.05,193.22,-1.39,67.97,1108.38,94.33,0.3647,-0.1270,-0.1660,1.0000,3 +93.813,51.18495877,-2.26063395,550.07,32.85,305.61,194.95,194.95,-1.77,72.33,1105.74,94.06,0.4745,-0.0918,-0.1152,1.0000,3 +94.407,51.18528002,-2.26135606,543.05,35.38,298.59,196.84,196.59,-0.34,88.10,1101.39,93.76,0.2765,-0.3535,-0.1758,1.0000,3 +95.032,51.18554542,-2.26215471,529.19,31.57,287.80,198.95,198.31,2.38,87.71,1097.59,93.48,0.5843,0.2961,0.0000,1.0000,3 +95.641,51.18571456,-2.26300138,507.22,27.71,273.26,199.92,198.91,2.59,75.22,1093.97,93.18,0.5176,0.3667,0.0000,1.0000,3 +96.375,51.18574611,-2.26405997,484.10,22.02,264.90,202.28,201.11,2.35,36.90,1089.14,92.83,0.0000,0.5961,0.0000,1.0000,3 +97.047,51.18568206,-2.26507809,467.52,16.90,263.68,205.30,204.14,1.40,-15.55,1085.65,92.49,0.2079,0.4275,0.0000,1.0000,3 +97.657,51.18562833,-2.26597405,458.64,20.14,266.45,207.25,206.35,-0.87,-41.79,1081.96,92.20,0.2412,0.1196,-0.0723,1.0000,3 +98.344,51.18559823,-2.26704571,453.24,28.88,270.12,209.21,208.28,-0.88,-56.53,1076.95,91.86,0.2981,0.1569,0.0000,1.0000,3 +98.922,51.18560774,-2.26791970,448.53,33.25,273.52,210.68,209.68,-0.28,-44.99,1073.06,91.57,0.0000,-0.4785,-0.0254,1.0000,3 +99.547,51.18565129,-2.26888435,442.75,38.05,275.23,212.52,211.30,0.59,-1.35,1068.59,91.27,-0.0117,-0.4512,-0.0723,1.0000,3 +100.235,51.18571181,-2.26997640,436.13,47.68,275.05,214.29,213.08,0.11,42.84,1063.79,90.92,0.0000,-0.3906,-0.0762,1.0000,3 +100.797,51.18575094,-2.27086925,428.47,51.07,272.93,215.71,214.41,0.45,69.98,1059.85,90.64,0.2177,-0.2949,-0.0156,1.0000,3 +101.422,51.18576444,-2.27185554,415.89,36.88,259.13,215.87,215.28,-1.27,76.46,1055.53,90.34,0.5686,0.0334,0.0000,1.0000,3 +101.985,51.18565434,-2.27272749,408.80,28.70,237.18,211.85,212.70,-4.48,78.23,1052.31,90.07,0.6745,0.0020,-0.0117,1.0000,3 +102.657,51.18529606,-2.27356800,412.05,31.92,207.78,204.51,205.96,-7.42,81.90,1048.35,89.76,0.6902,0.0000,-0.0293,1.0000,3 +103.313,51.18473147,-2.27400683,423.16,37.46,182.37,198.92,199.64,-7.17,86.18,1044.22,89.41,0.6628,-0.0625,-0.0312,1.0000,3 +103.875,51.18421400,-2.27404940,431.33,43.51,169.17,197.08,196.98,-4.85,87.46,1039.98,89.14,0.3314,0.1941,-0.0273,1.0000,3 +104.500,51.18366803,-2.27389558,433.52,45.46,166.65,198.81,198.22,-1.66,73.36,1036.02,88.84,0.0824,0.3745,0.0000,1.0000,3 +105.125,51.18312941,-2.27368445,428.60,40.26,164.46,200.83,199.97,0.46,46.42,1031.45,88.55,0.0000,0.3922,0.0000,1.0000,3 +105.719,51.18259695,-2.27342817,419.95,31.47,162.44,202.90,201.87,0.82,38.16,1027.56,88.27,0.1432,0.1667,0.0000,1.0000,3 +106.313,51.18205278,-2.27314484,411.30,19.66,159.96,204.69,203.70,0.14,27.88,1023.19,87.96,0.1236,0.4549,0.0000,1.0000,3 +106.891,51.18153331,-2.27283125,405.94,13.83,158.53,206.31,205.29,-0.67,-2.20,1019.39,87.67,0.2157,0.4098,0.0000,1.0000,3 +107.625,51.18088181,-2.27242994,407.76,15.99,160.43,207.38,206.31,-4.35,-29.91,1014.35,87.33,0.0785,0.4039,0.0000,1.0000,4 +108.188,51.18038741,-2.27215593,416.62,24.92,161.79,207.98,206.79,-4.01,-40.27,1010.71,87.06,0.0941,-0.3379,-0.0254,1.0000,4 +108.829,51.17980536,-2.27187362,424.96,33.34,163.77,208.69,207.52,-3.73,-25.33,1005.97,86.76,0.1941,-0.0879,0.0000,1.0000,4 +109.391,51.17926856,-2.27163480,433.77,42.16,165.02,209.24,207.95,-3.75,2.52,1001.64,86.48,0.0000,-0.4707,-0.0195,1.0000,4 +110.016,51.17869425,-2.27138428,444.44,45.12,164.84,209.65,208.47,-4.25,48.54,996.85,86.20,0.1471,-0.5254,-0.0234,1.0000,4 +110.563,51.17816985,-2.27113667,451.83,50.13,155.15,209.23,208.84,-5.74,86.27,992.89,85.92,0.6235,-0.0156,0.0000,1.0000,4 +111.094,51.17771167,-2.27079084,455.73,53.75,137.59,205.98,206.63,-3.35,91.03,988.47,85.66,0.4059,0.2235,0.0000,1.0000,4 +111.735,51.17727032,-2.27016326,451.62,49.35,123.46,205.48,205.32,-0.58,87.46,984.47,85.37,0.5745,0.1726,0.0000,1.0000,4 +112.344,51.17695468,-2.26938719,440.71,38.36,101.39,202.67,203.73,-2.46,77.56,980.44,85.08,0.7980,0.2804,0.0000,1.0000,4 +112.891,51.17683135,-2.26859648,435.52,28.84,91.61,201.23,200.81,-2.37,71.11,977.25,84.80,0.5804,0.2765,0.0000,1.0000,4 +113.532,51.17683417,-2.26766316,434.33,28.80,80.72,200.90,200.49,-3.66,62.54,973.29,84.49,0.2177,0.4412,0.0000,1.0000,4 +114.125,51.17692630,-2.26680136,437.27,42.42,76.31,201.80,201.14,-4.68,42.49,969.24,84.20,0.3667,0.1883,0.0000,1.0000,4 +114.719,51.17706331,-2.26595370,446.66,51.79,73.38,202.14,201.07,-5.18,33.68,965.37,83.93,0.1667,0.3157,0.0079,1.0000,4 +115.313,51.17722562,-2.26510384,459.25,60.43,71.66,202.52,201.31,-5.22,21.37,960.80,83.63,0.1432,0.2118,0.0216,1.0000,4 +115.891,51.17741118,-2.26425563,473.94,41.87,69.48,202.55,201.35,-7.29,46.84,955.84,83.33,0.3079,-0.4180,-0.0254,1.0000,4 +116.500,51.17762746,-2.26342303,492.56,50.88,61.88,201.73,200.40,-9.54,69.67,951.31,83.03,0.3177,-0.3320,-0.0273,1.0000,4 +117.079,51.17788746,-2.26267636,512.44,53.96,50.72,200.52,199.70,-9.01,86.22,946.29,82.76,0.6745,-0.2676,-0.0098,1.0000,4 +117.766,51.17829029,-2.26195657,529.48,47.46,29.22,196.31,196.91,-5.92,91.94,941.87,82.43,0.4549,0.1118,0.0000,1.0000,4 +118.469,51.17884145,-2.26146247,534.38,33.31,17.73,195.99,196.08,-2.39,82.08,936.11,82.10,0.3216,0.3765,0.0000,1.0000,4 +119.188,51.17941013,-2.26116681,529.99,25.33,14.84,197.61,197.13,0.05,32.53,931.51,81.79,0.0844,0.5000,0.0000,1.0000,4 +119.891,51.18007272,-2.26089893,524.20,21.84,13.56,199.22,198.79,-2.48,-12.13,926.32,81.41,0.3843,0.4569,0.0000,1.0000,4 +120.500,51.18065215,-2.26065822,532.13,29.27,18.86,198.86,198.06,-8.13,-49.73,921.78,81.09,0.3294,0.4843,0.0471,1.0000,4 +121.047,51.18112851,-2.26039118,549.46,42.56,23.08,198.47,197.27,-7.80,-58.64,917.81,80.82,0.0216,-0.5723,-0.1270,1.0000,4 +121.579,51.18156625,-2.26008711,565.50,49.42,24.63,198.54,197.35,-6.18,-14.33,914.28,80.57,0.0000,-0.5820,-0.1230,1.0000,4 +122.204,51.18207928,-2.25970820,583.48,53.00,25.40,198.43,197.47,-6.27,47.90,908.81,80.28,0.0000,-0.5723,-0.1191,1.0000,4 +122.782,51.18256350,-2.25935779,596.16,54.86,19.43,198.19,198.11,-5.93,90.94,904.76,79.99,0.6059,0.0000,0.0000,1.0000,4 +123.469,51.18315730,-2.25907872,599.56,52.72,353.64,193.84,195.74,-1.34,95.28,899.25,79.67,0.6804,0.1412,0.0000,1.0000,4 +124.188,51.18378210,-2.25919551,584.31,41.63,334.37,191.85,191.93,1.96,82.77,894.54,79.33,0.5216,0.3549,0.0745,1.0000,4 +124.844,51.18428291,-2.25957910,563.50,27.20,319.47,191.81,191.61,1.42,76.38,890.40,79.03,0.4863,0.1588,0.0000,1.0000,4 +125.657,51.18480616,-2.26030180,540.07,17.44,304.85,192.57,192.37,0.35,69.85,886.22,78.63,0.4255,0.1294,0.0000,1.0000,4 +126.469,51.18521955,-2.26130871,522.41,14.67,294.35,193.93,193.69,-0.62,63.20,881.58,78.23,0.3353,0.1490,0.0000,1.0000,4 +127.266,51.18550013,-2.26237095,511.25,19.11,284.66,195.07,194.91,-1.88,63.96,876.36,77.82,0.3784,0.0000,-0.0195,1.0000,4 +128.047,51.18566005,-2.26346773,505.50,34.99,275.39,195.65,195.50,-2.43,73.24,870.63,77.41,0.3196,-0.3730,-0.0234,1.0000,4 +128.766,51.18571006,-2.26449405,498.67,41.64,268.02,196.51,196.14,-0.01,84.18,865.51,77.04,0.0432,0.3020,0.0000,1.0000,4 +129.469,51.18568795,-2.26554706,481.55,36.29,265.46,198.46,197.22,3.30,62.77,860.36,76.67,0.0000,0.3647,0.0000,1.0000,4 +130.266,51.18562071,-2.26665511,455.04,25.65,262.52,200.92,199.15,3.76,5.25,855.94,76.31,0.2804,0.5333,0.0863,1.0000,4 +131.016,51.18553753,-2.26776697,445.53,28.80,271.33,199.13,199.07,-9.58,-46.49,850.22,75.91,0.2451,0.4745,0.1216,1.0000,4 +131.610,51.18554278,-2.26868908,467.14,57.34,270.92,198.66,197.16,-7.27,-75.18,846.53,75.58,-0.2031,0.4255,0.0981,1.0000,4 +132.188,51.18556049,-2.26952825,480.42,84.43,272.66,198.30,197.66,-5.02,-93.42,841.55,75.30,-0.1250,-0.2031,0.0706,1.0000,4 +132.797,51.18558243,-2.27042887,484.45,102.17,267.76,198.64,198.24,1.72,-44.77,837.25,75.01,-0.5156,-0.5918,0.0000,1.0000,4 +133.422,51.18557787,-2.27131866,466.33,86.93,268.25,200.07,197.37,7.88,11.06,832.58,74.72,0.1412,-0.6035,-0.0508,1.0000,4 +134.032,51.18555712,-2.27220511,435.93,55.17,263.82,201.84,199.23,3.55,56.84,828.69,74.43,0.5588,-0.1777,-0.0059,1.0000,4 +134.610,51.18547334,-2.27306799,418.07,38.09,234.30,193.83,197.92,-10.82,77.81,825.36,74.14,0.9137,-0.6582,-0.0508,1.0000,4 +135.204,51.18519021,-2.27371237,419.85,37.37,202.03,180.71,185.19,-1.49,102.69,823.13,73.86,0.6157,0.4922,0.1098,1.0000,4 +135.969,51.18461489,-2.27409531,402.90,14.92,171.71,173.24,177.42,-10.25,66.39,817.66,73.50,0.5784,-0.7012,-0.2402,1.0000,4 +136.610,51.18409213,-2.27399765,411.69,23.89,165.96,173.99,173.79,-5.47,74.83,812.88,73.17,-0.1172,0.5804,-0.0977,1.0000,4 +137.188,51.18363535,-2.27380751,415.77,27.82,164.07,175.31,175.14,-2.73,50.96,808.48,72.89,-0.1699,0.4157,0.0000,1.0000,4 +137.844,51.18311956,-2.27355412,415.38,27.31,162.19,177.05,176.73,-0.89,28.96,803.72,72.57,-0.1055,0.2530,0.0000,1.0000,4 +138.500,51.18260697,-2.27327799,412.37,24.12,160.71,178.77,178.38,-0.22,3.63,798.86,72.24,0.0981,0.2490,0.0000,1.0000,4 +139.016,51.18220475,-2.27305219,409.70,19.09,160.47,180.05,179.63,-0.35,0.20,795.33,72.00,0.1922,0.0000,0.0000,1.0000,4 +139.579,51.18175400,-2.27279877,408.56,16.66,160.66,181.02,180.72,-3.56,-0.11,791.28,71.72,0.2961,-0.0332,0.0000,1.0000,4 +140.188,51.18127548,-2.27252912,416.68,25.12,160.67,181.39,180.62,-5.71,-0.11,787.13,71.44,0.0981,0.0000,0.0000,1.0000,4 diff --git a/track_data/flight_20260831_212001_021145.csv b/track_data/flight_20260831_212001_021145.csv new file mode 100644 index 00000000..c121992b --- /dev/null +++ b/track_data/flight_20260831_212001_021145.csv @@ -0,0 +1,235 @@ +# aircraft_title=Extra 300S Paint2 +# recorded_at=2026-08-31T21:20:01 +# laps_s=29.727,34.962,33.650,33.107 +timestamp_s,latitude,longitude,altitude_ft,agl_ft,heading_deg,airspeed_kts,groundspeed_kts,pitch_deg,bank_deg,torque,health_pct,stick_pitch,stick_roll,stick_yaw,stick_throttle,lap +0.359,51.18973573,-2.27594680,981.31,617.52,166.17,183.70,182.53,6.75,0.13,1092.84,100.00,-0.0898,0.0000,0.0000,1.0000,1 +0.875,51.18928891,-2.27577239,957.63,588.73,166.03,187.69,186.15,8.18,0.15,1169.98,100.00,-0.2617,0.0000,0.0000,1.0000,1 +1.406,51.18886718,-2.27560681,930.71,558.44,165.84,191.51,188.73,11.64,0.18,1174.05,100.00,-0.2969,0.0000,0.0000,1.0000,1 +1.922,51.18842454,-2.27543242,893.89,517.42,165.79,196.06,190.67,14.36,0.20,1174.63,100.00,-0.1836,0.0000,0.0000,1.0000,1 +2.563,51.18787883,-2.27521565,838.37,456.49,165.92,201.92,194.76,14.79,0.20,1177.39,100.00,0.0196,0.0000,0.0000,1.0000,1 +3.078,51.18741193,-2.27503064,788.85,406.91,165.93,206.87,199.00,14.91,0.20,1176.80,100.00,0.1647,0.0000,-0.0273,1.0000,1 +3.609,51.18693337,-2.27484065,738.18,356.29,166.04,211.29,203.78,13.02,0.20,1180.50,100.00,0.2726,0.0000,-0.0117,1.0000,1 +4.188,51.18639108,-2.27462453,687.95,306.32,165.99,215.99,209.82,12.11,0.21,1181.19,100.00,0.0941,0.0294,-0.0254,1.0000,1 +4.719,51.18588986,-2.27442448,644.15,262.54,166.00,219.78,213.76,11.22,0.21,1183.20,100.00,0.1843,0.0275,-0.0137,1.0000,1 +5.281,51.18535022,-2.27420967,602.12,220.66,166.01,223.52,218.37,9.79,0.22,1185.10,100.00,0.1745,0.0275,-0.0098,1.0000,1 +5.828,51.18480606,-2.27399269,563.73,175.57,165.99,226.74,222.13,8.85,0.22,1185.64,100.00,0.1784,0.0510,0.0000,1.0000,1 +6.375,51.18425302,-2.27377263,529.54,140.44,166.00,229.76,225.64,7.55,0.22,1187.72,100.00,0.1726,0.0981,0.0000,1.0000,1 +6.875,51.18374217,-2.27356901,501.36,112.25,165.97,232.10,228.43,6.89,0.23,1187.52,100.00,0.1608,0.1137,0.0000,1.0000,1 +7.422,51.18319273,-2.27335027,473.93,84.89,165.97,234.50,231.08,5.76,-0.37,1189.40,100.00,0.1628,0.1628,0.0000,1.0000,1 +8.016,51.18256829,-2.27310178,449.20,59.69,166.07,236.59,233.78,3.64,-0.66,1189.07,100.00,0.2549,0.0000,-0.0176,1.0000,1 +8.641,51.18192277,-2.27284295,431.40,38.98,166.12,238.39,236.00,2.05,-0.67,1190.84,100.00,0.1255,0.0059,0.0000,1.0000,1 +9.234,51.18127961,-2.27259125,419.96,27.63,166.15,239.78,237.46,1.10,-0.67,1190.42,100.00,0.1647,0.0000,-0.0020,1.0000,1 +9.828,51.18065094,-2.27234387,412.79,20.64,166.18,240.82,238.59,0.20,-0.33,1190.98,100.00,0.2079,-0.1289,-0.0078,1.0000,1 +10.422,51.18002973,-2.27210015,410.00,17.98,166.22,241.60,239.38,-0.94,8.14,1191.08,100.00,0.1569,-0.2637,-0.0117,1.0000,1 +11.047,51.17935235,-2.27182485,412.28,20.66,165.18,242.03,239.81,-3.66,24.78,1190.86,100.00,0.3039,-0.2773,-0.0137,1.0000,1 +11.641,51.17869005,-2.27152972,423.39,26.51,163.17,241.95,239.51,-4.67,56.80,1190.59,100.00,0.1686,-0.4102,-0.1250,1.0000,1 +12.172,51.17812594,-2.27123600,433.33,31.67,155.00,241.39,239.51,-5.68,81.43,1190.45,100.00,0.6431,-0.0762,-0.1191,1.0000,1 +12.656,51.17765154,-2.27086893,442.11,40.42,136.47,237.21,236.52,-5.97,86.15,1189.43,100.00,0.5510,-0.1250,-0.1172,1.0000,1 +13.219,51.17721674,-2.27020501,451.30,49.69,117.07,233.01,232.64,-4.74,88.65,1189.67,100.00,0.5117,0.0687,-0.0684,1.0000,1 +13.859,51.17689631,-2.26923620,455.66,53.64,103.41,231.95,230.97,-2.24,89.38,1189.27,100.00,0.5235,0.1706,-0.0273,1.0000,1 +14.391,51.17675843,-2.26834123,452.27,40.68,90.53,230.98,229.91,-0.99,83.18,1189.27,100.00,0.5078,0.2784,0.0000,1.0000,1 +14.922,51.17674987,-2.26747314,446.36,40.65,76.51,229.93,229.12,-2.53,74.02,1189.30,100.00,0.6431,0.2177,-0.0312,1.0000,1 +15.531,51.17689363,-2.26648659,445.66,50.69,71.47,230.16,228.56,-1.62,43.04,1189.90,100.00,-0.0781,0.4667,0.0000,1.0000,1 +16.188,51.17711801,-2.26544061,446.94,50.18,69.86,231.44,229.74,-1.31,14.87,1190.01,100.00,0.1020,-0.1836,-0.1270,1.0000,1 +16.844,51.17737021,-2.26438765,453.11,28.21,66.82,231.86,230.05,-6.79,32.55,1189.74,100.00,0.3372,-0.2109,-0.1699,1.0000,1 +17.391,51.17760665,-2.26353423,472.23,29.90,62.93,231.52,228.74,-8.63,61.32,1189.47,100.00,0.4275,-0.3789,-0.1914,1.0000,1 +17.906,51.17786641,-2.26276937,496.38,47.17,49.11,229.26,226.60,-11.07,78.60,1189.05,100.00,0.4765,-0.1348,-0.1875,1.0000,1 +18.547,51.17830428,-2.26199129,529.65,56.45,32.63,226.08,223.60,-10.45,88.81,1187.33,100.00,0.5098,-0.3750,-0.1914,1.0000,1 +19.109,51.17878334,-2.26150775,554.10,54.97,16.30,222.94,221.91,-6.26,100.62,1187.05,100.00,0.4020,-0.0078,-0.1758,1.0000,1 +19.688,51.17938158,-2.26118977,565.61,61.65,14.72,223.76,222.72,-2.85,89.99,1185.23,100.00,-0.0332,0.3549,0.0000,1.0000,1 +20.188,51.17987204,-2.26098354,566.80,63.90,13.40,224.97,223.90,-0.56,58.66,1185.26,100.00,-0.1621,0.5882,0.0000,1.0000,1 +20.859,51.18053835,-2.26074138,562.38,59.72,11.32,226.76,225.59,0.23,-22.58,1184.83,100.00,0.1196,0.6255,0.0000,1.0000,1 +21.422,51.18112347,-2.26052978,559.22,55.57,22.46,226.67,226.48,-6.28,-57.44,1184.82,100.00,0.6333,0.0334,-0.0645,1.0000,1 +22.031,51.18167922,-2.26016045,575.98,62.66,27.16,225.77,223.50,-7.03,-16.44,1185.23,100.00,0.0000,-0.6270,-0.1738,1.0000,1 +22.625,51.18224385,-2.25970307,599.70,70.97,27.80,225.85,223.78,-7.27,65.81,1184.28,100.00,0.0000,-0.6367,-0.1699,1.0000,1 +23.219,51.18279652,-2.25926181,617.19,72.82,16.41,225.33,224.80,-3.12,110.57,1184.07,100.00,0.6686,0.3039,-0.0156,1.0000,1 +23.781,51.18335844,-2.25900827,610.81,62.83,348.69,218.88,220.06,5.18,97.12,1182.63,100.00,0.8745,0.2412,-0.1035,1.0000,1 +24.375,51.18390342,-2.25917700,579.51,36.80,322.79,212.93,211.50,3.64,69.55,1183.15,100.00,0.7059,0.4255,-0.0781,1.0000,1 +24.969,51.18437879,-2.25971783,556.07,23.09,310.70,212.96,212.28,0.22,61.90,1185.51,100.00,0.3569,0.0726,-0.1973,1.0000,1 +25.609,51.18478186,-2.26048172,544.36,24.84,306.89,215.50,214.43,1.13,55.91,1182.24,99.68,0.0000,-0.1504,-0.1816,1.0000,1 +26.234,51.18516568,-2.26131244,531.95,24.09,302.34,217.86,216.67,0.92,66.87,1178.15,99.33,0.4020,-0.3145,-0.1758,1.0000,1 +26.938,51.18550553,-2.26223969,518.75,23.90,287.84,218.55,217.98,-1.03,78.05,1174.00,98.99,0.4980,-0.1855,-0.1641,1.0000,1 +27.609,51.18570117,-2.26325585,508.68,34.90,275.42,219.22,218.37,-0.32,80.89,1168.92,98.62,0.3647,0.0412,-0.1289,1.0000,1 +28.156,51.18576158,-2.26421214,498.08,37.72,268.96,220.66,219.41,1.05,73.01,1165.01,98.32,0.1765,0.3314,-0.0176,1.0000,1 +28.750,51.18575179,-2.26515024,483.97,33.81,266.23,222.06,220.32,2.29,54.88,582.55,98.25,0.3059,0.0451,-0.1836,1.0000,1 +29.359,51.18570283,-2.26610339,468.61,31.64,262.73,221.48,219.94,2.05,19.87,561.89,98.43,0.0000,0.5863,0.0000,1.0000,1 +30.000,51.18562051,-2.26714021,456.34,32.83,262.95,220.04,218.65,-0.02,-45.98,564.71,98.62,0.3922,0.4118,-0.0273,1.0000,1 +30.547,51.18556842,-2.26807076,451.84,37.97,272.81,217.19,216.40,-3.28,-65.21,566.41,98.80,0.1824,0.4020,0.0000,1.0000,1 +31.109,51.18559050,-2.26894956,453.34,49.80,273.80,215.56,214.54,-1.34,-68.38,568.31,98.96,-0.0547,-0.2734,0.0000,1.0000,1 +31.766,51.18564194,-2.26994015,448.28,59.27,276.19,214.24,213.07,0.76,-20.68,569.95,99.15,-0.1191,-0.5820,-0.0938,1.0000,1 +32.391,51.18570845,-2.27092290,440.98,63.76,277.24,213.01,211.90,0.41,61.65,571.41,99.33,0.0000,-0.6895,-0.1387,1.0000,1 +32.938,51.18576567,-2.27179187,430.39,51.82,270.50,212.30,211.10,1.06,87.55,959.30,99.47,0.6608,0.0490,-0.0762,1.0000,1 +33.547,51.18573521,-2.27275189,412.12,31.53,236.59,203.84,206.11,-0.97,79.95,1184.22,99.16,0.7078,0.4196,-0.0234,1.0000,1 +34.125,51.18546268,-2.27345141,401.38,21.14,210.77,197.86,199.78,-4.59,77.85,1180.21,98.87,0.7529,0.1608,-0.1816,1.0000,1 +34.781,51.18495861,-2.27391801,402.80,19.15,187.78,193.92,194.62,-6.14,79.07,1175.53,98.55,0.5333,0.0000,-0.2207,1.0000,1 +35.313,51.18448164,-2.27403977,408.82,21.04,179.49,195.06,194.59,-4.47,78.29,1171.51,98.25,0.3333,0.0000,-0.2363,1.0000,1 +35.938,51.18391857,-2.27401846,413.49,25.65,167.69,196.15,195.92,-3.73,79.68,893.58,97.94,0.4647,-0.0215,-0.2227,1.0000,1 +36.484,51.18341677,-2.27385515,414.94,26.91,162.70,196.23,195.68,-1.76,73.83,564.29,98.07,0.0451,0.4373,-0.1270,1.0000,1 +37.047,51.18295838,-2.27362013,411.75,23.60,160.86,195.63,194.90,0.07,41.28,562.22,98.22,0.1196,0.4843,-0.0723,1.0000,1 +37.656,51.18243425,-2.27332688,405.55,16.26,159.26,195.30,194.49,0.50,10.51,564.33,98.40,0.1824,0.2824,-0.1426,1.0000,1 +38.219,51.18195318,-2.27303153,401.43,9.40,158.81,194.93,194.26,-1.09,-7.09,566.89,98.57,0.1863,0.3686,-0.0078,1.0000,1 +38.844,51.18143005,-2.27271790,403.09,11.41,160.80,194.01,193.39,-4.91,-26.79,568.22,98.75,0.2510,0.3431,0.0000,1.0000,1 +39.438,51.18093412,-2.27245195,412.82,21.15,162.20,192.89,192.05,-4.70,-46.97,568.97,98.93,0.1471,0.1843,0.0000,1.0000,2 +40.031,51.18041906,-2.27220580,420.47,28.65,163.91,191.93,191.33,-3.37,-50.20,569.98,99.11,0.0981,0.0000,0.0000,1.0000,2 +40.641,51.17990816,-2.27199090,423.12,31.13,165.65,191.37,190.87,-1.77,-37.15,571.52,99.28,0.0961,-0.5391,-0.1641,1.0000,2 +41.234,51.17939722,-2.27179693,422.82,30.95,167.20,191.05,190.53,-2.52,4.18,572.96,99.47,0.3177,-0.3945,-0.1738,1.0000,2 +41.859,51.17885435,-2.27159619,427.82,31.96,165.82,190.18,189.61,-4.05,52.37,574.50,99.65,0.1863,-0.5039,-0.1875,1.0000,2 +42.391,51.17839277,-2.27139435,432.07,30.14,161.16,189.78,189.47,-4.02,74.19,918.55,99.78,0.3922,0.0000,-0.0996,1.0000,2 +42.875,51.17797428,-2.27115958,433.36,31.39,151.53,190.25,190.36,-4.27,77.77,1189.38,99.54,0.5392,-0.1387,-0.1602,1.0000,2 +43.391,51.17759749,-2.27084070,434.07,32.10,137.05,190.01,190.69,-4.58,82.70,1186.76,99.27,0.6529,0.0000,-0.1641,1.0000,2 +43.969,51.17722746,-2.27030066,434.55,32.57,115.07,186.90,188.66,-4.98,83.45,1181.38,98.98,0.6549,0.3549,0.0000,1.0000,2 +44.641,51.17697731,-2.26943779,436.02,34.11,96.64,185.81,186.48,-5.04,78.43,1175.58,98.60,0.2804,0.1588,-0.0898,1.0000,2 +45.203,51.17691108,-2.26867769,437.89,34.05,90.27,188.07,187.97,-3.80,72.25,1171.31,98.30,0.3372,0.0000,-0.0938,1.0000,2 +45.719,51.17690568,-2.26794192,437.76,32.76,88.03,189.84,189.48,-1.78,70.85,590.62,98.24,0.3216,0.2353,-0.0039,1.0000,2 +46.328,51.17693227,-2.26713223,433.20,33.71,82.04,189.14,188.89,-1.99,58.51,564.38,98.41,0.2981,0.3098,0.0000,1.0000,2 +46.953,51.17701543,-2.26626565,430.12,35.10,77.67,188.71,188.37,-2.34,38.22,567.14,98.60,0.2451,0.3196,0.0000,1.0000,2 +47.547,51.17713507,-2.26547252,432.86,36.71,73.07,187.73,187.48,-7.08,31.72,568.01,98.78,0.3804,0.0000,-0.0605,1.0000,2 +48.141,51.17729009,-2.26469185,448.10,29.49,70.50,186.30,185.05,-7.74,36.04,568.12,98.96,0.2490,-0.1230,-0.1582,1.0000,2 +48.688,51.17746197,-2.26396779,466.16,30.38,67.20,184.72,183.57,-9.21,49.75,569.62,99.13,0.2922,-0.3906,-0.1582,1.0000,2 +49.219,51.17764513,-2.26330784,484.39,38.92,60.90,184.80,183.72,-10.50,70.88,1174.01,98.96,0.5490,-0.3770,-0.1230,1.0000,2 +49.828,51.17790110,-2.26261716,504.84,49.55,46.02,184.17,183.99,-9.64,88.82,1176.90,98.65,0.5980,0.0510,-0.0781,1.0000,2 +50.469,51.17825921,-2.26200914,519.78,37.16,31.70,184.17,184.54,-6.04,92.49,1169.51,98.32,0.5255,0.0138,-0.1348,1.0000,2 +51.016,51.17865575,-2.26159686,524.30,26.11,22.85,185.43,185.84,-3.20,87.00,1164.08,98.03,0.4039,0.3255,-0.0469,1.0000,2 +51.531,51.17906692,-2.26130104,522.08,17.31,14.87,187.24,187.47,-1.82,78.55,1159.59,97.75,0.3628,0.3628,-0.0293,1.0000,2 +52.156,51.17958498,-2.26107582,515.32,10.73,12.17,190.63,190.15,0.14,22.79,1155.25,97.44,0.3157,0.6255,0.0000,1.0000,2 +52.797,51.18012020,-2.26089691,513.10,11.68,13.31,192.95,192.73,-5.89,-33.03,1150.41,97.11,0.5137,0.4588,-0.0312,1.0000,2 +53.422,51.18069187,-2.26065093,529.44,26.21,21.35,193.60,192.32,-8.94,-64.64,1145.32,96.75,0.1843,0.1510,0.0000,1.0000,2 +54.016,51.18118611,-2.26033927,548.50,41.11,22.54,194.71,193.78,-6.60,-57.74,712.47,96.51,0.0275,-0.3652,-0.0977,1.0000,2 +54.609,51.18167039,-2.26000511,562.01,43.58,24.41,194.05,193.45,-4.76,-17.91,551.14,96.66,0.0236,-0.5938,-0.1230,1.0000,2 +55.219,51.18216561,-2.25963792,573.61,42.00,25.44,192.79,192.34,-4.54,46.49,547.83,96.84,0.0471,-0.6016,-0.1406,1.0000,2 +55.859,51.18269243,-2.25926777,581.39,37.57,14.09,190.41,191.18,-5.67,86.88,548.64,97.03,0.6510,-0.1699,-0.0938,1.0000,2 +56.375,51.18313334,-2.25909217,582.84,36.22,355.69,186.29,188.10,-2.98,91.78,1084.47,97.03,0.4000,0.3608,0.0000,1.0000,2 +56.891,51.18358026,-2.25911405,577.35,33.01,344.39,187.46,188.07,-1.07,84.33,1144.96,96.76,0.8157,0.0000,-0.0664,1.0000,2 +57.531,51.18409816,-2.25937324,566.26,26.42,324.85,185.89,186.70,-0.56,83.39,1139.58,96.44,0.4059,0.1647,-0.0898,1.0000,2 +58.156,51.18454723,-2.25985294,551.92,20.51,316.56,188.93,188.58,1.01,74.04,1135.12,96.11,0.4941,-0.0078,-0.1680,1.0000,2 +58.766,51.18491967,-2.26043039,537.22,16.48,303.78,188.93,189.12,-0.21,73.78,549.14,96.19,0.4510,0.1902,-0.0742,1.0000,2 +59.359,51.18521306,-2.26111846,524.52,13.68,295.16,188.25,188.11,-0.12,69.89,543.69,96.37,0.4275,0.2451,-0.0645,1.0000,2 +59.969,51.18543086,-2.26187710,512.89,12.11,286.95,187.56,187.41,-0.56,67.42,545.74,96.55,0.3529,0.0000,-0.1230,1.0000,2 +60.578,51.18557623,-2.26267292,502.98,16.95,278.76,186.95,186.96,-1.21,68.02,548.29,96.74,0.4020,0.0079,-0.0957,1.0000,2 +61.250,51.18565764,-2.26362392,492.97,24.62,271.46,186.66,186.49,-0.53,69.01,550.58,96.95,0.2922,0.0000,-0.1523,1.0000,2 +61.891,51.18566584,-2.26447967,482.21,24.69,267.53,186.95,186.45,1.14,51.81,552.32,97.14,0.1236,0.3745,0.0000,1.0000,2 +62.750,51.18562094,-2.26561174,464.45,20.75,264.91,187.74,186.95,2.40,-17.04,554.51,97.38,0.1863,0.6353,0.0000,1.0000,2 +63.578,51.18556989,-2.26675807,447.33,19.75,270.03,188.07,187.48,0.66,-60.45,557.00,97.65,0.3275,-0.0664,-0.1348,1.0000,2 +64.422,51.18558692,-2.26787485,432.98,17.53,275.45,188.22,187.71,-0.01,-24.02,559.49,97.90,0.1432,-0.2246,-0.1738,1.0000,2 +65.266,51.18566832,-2.26907980,424.47,22.32,276.83,188.41,187.80,0.52,-25.83,561.85,98.16,0.1628,-0.0508,-0.0840,1.0000,2 +66.063,51.18576441,-2.27017833,414.80,29.98,278.44,188.70,187.99,1.16,8.53,564.04,98.41,0.1667,-0.5879,-0.1348,1.0000,2 +66.844,51.18585878,-2.27128869,405.85,32.70,270.44,187.85,187.99,-4.73,62.81,566.47,98.67,0.5863,-0.2559,-0.1523,1.0000,2 +67.469,51.18585060,-2.27215974,408.54,28.99,250.16,184.89,186.01,-8.83,75.22,1150.81,98.63,0.7294,-0.0938,-0.0820,1.0000,2 +68.172,51.18564672,-2.27305210,421.33,41.61,229.75,183.25,184.00,-7.69,86.66,1173.79,98.24,0.6804,0.0000,-0.0273,1.0000,2 +68.906,51.18524545,-2.27377290,430.19,48.76,203.34,179.61,181.00,-5.14,88.90,1166.56,97.84,0.5274,0.2530,0.0000,1.0000,2 +69.641,51.18470923,-2.27413827,429.18,41.05,181.42,178.09,179.51,-3.09,80.93,604.26,97.63,0.4843,0.4843,0.0000,1.0000,2 +70.281,51.18413731,-2.27417565,424.55,36.44,170.73,176.72,177.09,-3.52,66.54,558.73,97.83,0.4157,0.4294,0.0000,1.0000,2 +70.844,51.18367767,-2.27405008,423.62,35.65,164.23,175.99,176.14,-3.94,60.20,561.53,98.01,0.2432,0.0000,-0.1484,1.0000,2 +71.422,51.18321948,-2.27383645,423.62,35.54,162.00,176.47,176.36,-1.90,58.35,563.61,98.17,0.0549,0.2216,0.0000,1.0000,2 +72.031,51.18274691,-2.27357419,418.83,30.58,159.70,177.13,176.83,-0.09,48.79,563.69,98.35,0.1216,0.3138,0.0000,1.0000,2 +72.719,51.18223458,-2.27324847,410.06,19.52,156.94,177.88,177.51,-0.28,7.94,565.16,98.55,0.2294,0.5078,0.0000,1.0000,2 +73.344,51.18177113,-2.27293121,406.56,14.49,157.24,178.18,177.89,-1.97,-33.25,566.85,98.72,0.2922,0.4373,0.0000,1.0000,2 +73.906,51.18132665,-2.27265368,406.70,14.78,161.75,177.98,177.84,-3.84,-52.69,568.52,98.89,0.4255,0.2432,-0.0039,1.0000,2 +74.563,51.18080257,-2.27240280,410.48,18.61,167.48,177.54,177.33,-3.84,-58.67,570.19,99.08,0.0628,-0.2285,-0.0273,1.0000,3 +75.125,51.18037353,-2.27225868,412.43,20.45,169.19,177.70,177.46,-2.30,-31.61,571.42,99.23,0.2569,-0.5234,-0.0664,1.0000,3 +75.750,51.17986325,-2.27211783,415.11,23.33,170.35,177.61,177.32,-4.66,36.26,572.63,99.41,0.2804,-0.6250,-0.1113,1.0000,3 +76.313,51.17940338,-2.27197489,421.24,29.56,166.72,177.21,177.05,-5.29,59.20,573.98,99.58,0.2392,-0.1230,-0.1582,1.0000,3 +76.922,51.17889696,-2.27176435,426.39,32.44,162.07,179.41,179.26,-4.31,67.85,1187.94,99.32,0.4118,0.0000,-0.1016,1.0000,3 +77.484,51.17845536,-2.27152292,428.04,26.06,155.80,181.62,181.61,-3.87,71.81,1183.96,99.02,0.3490,-0.0801,-0.1309,1.0000,3 +77.953,51.17809240,-2.27126178,427.93,25.91,149.29,183.45,183.50,-3.64,74.23,1179.23,98.76,0.4275,-0.0215,-0.0996,1.0000,3 +78.469,51.17772165,-2.27090713,426.92,24.88,139.82,184.96,185.20,-3.85,76.48,1174.74,98.48,0.5196,0.0000,-0.0684,1.0000,3 +79.031,51.17738082,-2.27044838,425.86,23.82,126.11,185.66,186.28,-4.62,78.48,1170.38,98.19,0.6020,-0.0527,-0.0547,1.0000,3 +79.688,51.17704396,-2.26967602,426.88,24.96,105.27,184.41,185.31,-5.46,80.29,1165.39,97.84,0.5941,0.2294,-0.0566,1.0000,3 +80.250,51.17691106,-2.26895797,429.19,27.28,93.73,184.83,185.24,-4.71,77.74,1161.07,97.53,0.2451,0.3490,-0.0625,1.0000,3 +80.766,51.17687021,-2.26820348,430.34,24.74,90.76,187.64,187.28,-2.83,62.67,1156.66,97.24,0.3628,0.0687,-0.1543,1.0000,3 +81.297,51.17686956,-2.26749180,430.20,28.58,84.57,188.57,188.43,-3.86,61.62,570.43,97.24,0.3431,0.1647,-0.0762,1.0000,3 +81.906,51.17692809,-2.26665186,433.01,38.15,77.28,186.92,186.79,-5.17,57.82,555.27,97.42,0.3020,0.2530,0.0000,1.0000,3 +82.547,51.17704704,-2.26584029,439.36,44.77,74.03,186.06,185.65,-4.46,32.62,557.40,97.58,0.2059,0.4314,0.0000,1.0000,3 +83.125,51.17719820,-2.26505603,448.97,48.93,71.55,185.07,184.37,-6.62,22.36,559.81,97.77,0.0628,-0.1562,-0.1309,1.0000,3 +83.750,51.17737788,-2.26423926,466.17,38.86,69.82,183.80,182.76,-7.14,37.89,560.42,97.96,0.0000,-0.3047,-0.1621,1.0000,3 +84.266,51.17753893,-2.26357842,480.10,38.10,67.09,182.98,182.29,-7.74,58.67,880.63,98.09,0.3059,-0.3164,-0.1113,1.0000,3 +84.797,51.17771703,-2.26294505,492.56,40.67,57.71,183.20,183.11,-9.04,78.57,1162.92,97.85,0.6000,-0.3105,-0.0605,1.0000,3 +85.391,51.17797075,-2.26231030,506.29,40.70,36.20,180.21,181.58,-9.25,85.63,1160.55,97.56,0.5745,0.0000,-0.0664,1.0000,3 +86.031,51.17841134,-2.26178287,519.38,29.05,24.82,180.78,181.00,-6.12,85.70,1153.64,97.20,0.1961,0.2784,-0.0586,1.0000,3 +86.625,51.17887920,-2.26142604,524.51,22.77,21.09,183.05,183.13,-3.89,66.84,595.95,97.04,0.2981,0.0000,-0.1973,1.0000,3 +87.203,51.17932845,-2.26115686,526.14,20.03,16.47,182.31,182.40,-3.23,62.96,683.01,97.20,0.2667,0.2824,0.0000,1.0000,3 +87.859,51.17983917,-2.26093023,525.99,20.75,13.77,184.85,184.68,-1.24,21.89,1147.82,96.94,0.0000,0.6862,0.0000,1.0000,3 +88.484,51.18036456,-2.26072871,525.40,21.62,15.72,187.30,187.34,-5.92,-42.34,1144.33,96.61,0.5941,0.5314,0.0000,1.0000,3 +89.078,51.18088504,-2.26046944,536.61,31.05,23.48,188.13,187.51,-7.09,-63.55,1138.79,96.27,0.0000,-0.4180,-0.1699,1.0000,3 +89.609,51.18129073,-2.26018637,548.18,35.40,24.43,189.83,189.26,-5.00,-14.15,1134.23,96.00,0.2157,-0.6543,-0.1719,1.0000,3 +90.219,51.18178513,-2.25981906,560.65,35.45,25.02,191.73,191.15,-5.18,62.04,1129.48,95.69,0.1745,-0.6074,-0.1738,1.0000,3 +90.797,51.18226259,-2.25948124,567.74,29.52,19.81,193.30,193.38,-3.63,96.71,1123.96,95.38,0.3353,0.2451,-0.0059,1.0000,3 +91.313,51.18269661,-2.25922786,566.28,21.06,7.12,194.07,194.74,-2.68,79.43,1119.98,95.11,0.7549,0.0000,-0.1953,1.0000,3 +91.922,51.18322998,-2.25914746,564.64,20.06,345.37,190.55,191.78,-4.81,78.97,1115.21,94.79,0.5784,0.0765,-0.2012,1.0000,3 +92.563,51.18376088,-2.25937130,567.11,27.24,330.61,190.73,191.37,-4.15,80.78,1110.80,94.47,0.5020,0.0961,-0.1895,1.0000,3 +93.172,51.18422660,-2.25979103,568.18,36.48,318.02,191.28,191.78,-3.14,82.29,1106.57,94.16,0.4431,0.1177,-0.1758,1.0000,3 +93.766,51.18461770,-2.26034856,565.78,43.81,308.50,192.61,192.80,-1.37,83.41,1102.11,93.85,0.3138,0.0000,-0.1816,1.0000,3 +94.422,51.18497475,-2.26103965,556.38,45.39,303.74,195.50,194.92,1.57,84.32,1097.25,93.52,0.3471,0.0863,-0.1602,1.0000,3 +95.094,51.18532190,-2.26187057,536.78,35.98,294.99,198.37,197.24,3.01,79.52,1092.23,93.15,0.5137,0.0745,-0.0859,1.0000,3 +95.813,51.18559418,-2.26285770,508.91,26.55,282.48,200.95,199.51,3.41,76.30,1087.82,92.76,0.4137,0.1588,-0.1426,1.0000,3 +96.516,51.18572458,-2.26386899,481.43,16.83,269.35,202.81,201.75,1.48,61.80,1082.37,92.39,0.2981,0.4157,-0.1016,1.0000,3 +97.234,51.18571473,-2.26493119,464.17,11.86,265.02,205.46,204.55,0.31,13.11,1078.57,92.00,0.1941,0.5333,-0.0469,1.0000,3 +97.922,51.18565628,-2.26598783,459.29,20.94,264.69,207.58,206.70,-0.91,-39.74,1073.71,91.61,0.1765,0.5196,0.0000,1.0000,3 +98.609,51.18561428,-2.26703398,456.33,31.96,270.58,209.08,208.31,-1.92,-64.42,1068.31,91.25,0.1432,-0.0664,-0.0977,1.0000,3 +99.156,51.18561990,-2.26789172,453.24,37.45,272.08,210.54,209.55,-0.10,-60.58,1063.89,90.94,0.0294,-0.3047,-0.0664,1.0000,3 +99.750,51.18564791,-2.26878088,444.78,38.81,273.89,212.37,211.07,1.74,-26.35,1059.70,90.64,0.1549,-0.5195,-0.0566,1.0000,3 +100.359,51.18569311,-2.26975071,433.93,41.34,275.06,214.23,212.85,1.64,21.83,1054.75,90.30,0.1157,-0.4785,-0.1035,1.0000,3 +100.953,51.18573814,-2.27069424,422.53,42.90,273.29,215.89,214.42,0.90,59.37,1050.45,89.97,0.3216,-0.2285,-0.0527,1.0000,3 +101.516,51.18575329,-2.27158939,411.83,35.23,263.94,216.40,215.43,-1.68,66.21,1045.78,89.66,0.4314,-0.2090,-0.1016,1.0000,3 +102.109,51.18567779,-2.27254639,407.89,27.91,250.04,215.52,215.11,-4.50,73.57,1042.02,89.34,0.5784,-0.1992,-0.0684,1.0000,3 +102.734,51.18546001,-2.27342534,412.95,33.29,224.25,210.02,211.47,-8.56,80.43,1037.69,89.02,0.8019,-0.1602,-0.0586,1.0000,3 +103.359,51.18504242,-2.27404754,427.50,47.15,191.14,198.74,200.86,-7.62,94.59,1033.74,88.70,0.7510,-0.1133,-0.0547,1.0000,3 +103.922,51.18450761,-2.27423878,434.40,46.43,174.45,195.67,196.03,-3.45,86.13,1029.02,88.38,0.4078,0.3980,0.0000,1.0000,3 +104.594,51.18394077,-2.27415416,432.67,44.53,165.23,196.67,196.37,-2.18,70.98,1024.73,88.05,0.2882,0.3804,0.0000,1.0000,3 +105.234,51.18337228,-2.27391221,428.35,40.15,163.00,198.77,198.01,0.02,40.90,1019.49,87.70,0.0000,0.4020,0.0000,1.0000,3 +105.844,51.18282895,-2.27363937,421.97,33.80,161.09,200.76,199.87,0.11,30.52,1014.91,87.36,0.1941,0.0000,-0.0703,1.0000,3 +106.469,51.18230400,-2.27333187,416.96,26.89,158.66,202.33,201.52,-1.55,28.19,1010.18,87.03,0.1471,0.2686,0.0000,1.0000,3 +107.063,51.18175999,-2.27298455,416.45,24.46,157.21,203.77,202.85,-1.39,5.27,1005.62,86.71,0.2177,0.3961,0.0000,1.0000,3 +107.641,51.18125688,-2.27264911,419.70,28.00,157.98,204.52,203.57,-5.14,-28.52,1001.01,86.39,0.3059,0.4843,0.0000,1.0000,3 +108.219,51.18074047,-2.27233285,432.07,40.55,161.78,204.71,203.46,-6.42,-61.33,996.67,86.10,0.3138,0.0000,-0.0098,1.0000,4 +108.828,51.18019624,-2.27206100,445.20,53.62,164.22,205.08,203.93,-4.75,-49.62,991.54,85.78,0.1177,-0.1992,-0.0410,1.0000,4 +109.500,51.17961951,-2.27181224,455.34,63.63,166.17,205.84,204.83,-3.39,-17.77,986.56,85.45,0.1784,-0.5723,-0.0488,1.0000,4 +110.234,51.17891590,-2.27154892,464.95,69.75,167.06,206.58,205.67,-3.38,54.32,979.90,85.04,0.1334,-0.5117,-0.0723,1.0000,4 +110.828,51.17834119,-2.27131347,467.94,65.93,161.45,207.12,206.59,-3.08,87.64,974.91,84.72,0.5431,-0.3379,-0.0352,1.0000,4 +111.297,51.17790505,-2.27107626,465.13,62.87,145.51,205.47,206.05,-0.74,93.29,971.08,84.46,0.6608,0.2235,0.0000,1.0000,4 +111.922,51.17745217,-2.27057996,451.41,48.89,127.00,202.85,202.44,1.90,83.36,966.92,84.15,0.5216,0.4216,0.0000,1.0000,4 +112.563,51.17710379,-2.26981779,432.64,30.27,108.02,201.08,201.30,-1.38,71.81,962.54,83.82,0.5784,0.1020,-0.0293,1.0000,4 +113.156,51.17692062,-2.26895721,424.90,22.82,93.80,199.87,199.99,-4.13,66.02,959.22,83.48,0.4863,0.2471,0.0000,1.0000,4 +113.719,51.17688399,-2.26813378,427.56,23.45,85.79,199.70,199.23,-5.86,57.74,954.99,83.18,0.3628,0.2667,0.0000,1.0000,4 +114.328,51.17693211,-2.26725339,438.83,39.74,80.30,199.75,198.73,-6.88,51.57,950.67,82.85,0.2412,0.1686,0.0000,1.0000,4 +114.844,51.17701728,-2.26649075,452.09,57.61,77.05,199.76,198.57,-7.08,49.90,946.21,82.56,0.2118,0.1784,0.0000,1.0000,4 +115.406,51.17713885,-2.26570870,467.22,71.08,74.53,199.84,198.58,-6.55,49.87,941.52,82.27,0.1255,-0.0312,-0.0254,1.0000,4 +116.016,51.17730087,-2.26483523,482.02,71.75,72.09,200.01,199.07,-5.83,52.87,935.96,81.95,0.2490,-0.0059,-0.0254,1.0000,4 +116.594,51.17747846,-2.26402549,493.89,57.68,67.43,200.05,199.26,-6.59,61.99,931.27,81.63,0.3138,-0.2773,-0.0273,1.0000,4 +117.109,51.17766846,-2.26332172,504.72,58.08,61.28,199.79,199.22,-6.73,77.34,926.38,81.35,0.4961,-0.3262,-0.0273,1.0000,4 +117.781,51.17796238,-2.26253121,515.92,51.99,43.29,197.61,198.23,-6.91,83.82,921.59,81.01,0.6412,-0.1660,-0.0645,1.0000,4 +118.469,51.17840966,-2.26188056,525.63,37.98,21.93,193.16,194.21,-5.63,85.94,915.95,80.65,0.3882,0.2451,-0.0801,1.0000,4 +119.016,51.17889095,-2.26154247,529.54,31.05,18.34,193.95,193.80,-2.76,71.03,911.32,80.33,0.0000,0.4824,-0.0566,1.0000,4 +119.688,51.17944905,-2.26125537,528.03,27.17,16.20,195.45,195.05,-0.36,13.01,906.76,80.01,0.0510,0.5980,0.0000,1.0000,4 +120.375,51.18002910,-2.26098379,526.17,25.84,19.43,196.23,196.17,-5.74,-38.83,901.68,79.65,0.5216,0.2628,-0.0234,1.0000,4 +120.922,51.18051744,-2.26070316,537.74,35.50,23.85,195.65,194.53,-7.33,-42.33,896.98,79.33,0.0000,-0.1836,-0.0957,1.0000,4 +121.547,51.18102862,-2.26033028,556.02,45.52,25.10,195.70,194.75,-5.88,-32.09,892.58,79.02,0.0000,-0.5547,-0.1621,1.0000,4 +122.125,51.18150121,-2.25996559,570.06,49.33,26.45,195.82,195.02,-4.96,20.59,887.14,78.71,0.0471,-0.5879,-0.1660,1.0000,4 +122.750,51.18199124,-2.25958286,582.36,48.00,23.73,195.78,195.51,-6.12,77.67,882.45,78.39,0.3804,-0.5469,-0.1152,1.0000,4 +123.359,51.18251126,-2.25925364,589.38,44.62,8.30,194.15,195.01,-3.81,92.54,877.05,78.07,0.3922,0.2490,-0.0469,1.0000,4 +124.078,51.18313108,-2.25911394,584.88,38.68,353.73,193.65,194.06,-0.25,86.17,871.76,77.70,0.5235,0.3314,0.0000,1.0000,4 +124.828,51.18377826,-2.25926328,569.19,27.51,335.16,192.71,192.89,0.42,80.59,866.11,77.29,0.6274,0.1353,-0.0352,1.0000,4 +125.547,51.18433460,-2.25969883,552.06,18.40,317.20,191.59,191.80,-0.16,77.49,861.55,76.92,0.5294,0.0961,-0.1055,1.0000,4 +126.234,51.18480517,-2.26038860,536.56,15.02,305.92,192.32,192.13,-0.31,70.64,856.92,76.52,0.5216,0.0157,-0.1152,1.0000,4 +127.063,51.18520883,-2.26136830,523.46,16.61,294.95,192.88,192.66,-0.93,59.77,850.87,76.08,0.1569,-0.1582,-0.1816,1.0000,4 +127.875,51.18549284,-2.26243549,515.46,25.16,283.65,193.49,193.52,-2.58,70.91,844.93,75.63,0.4098,-0.2930,-0.1543,1.0000,4 +128.672,51.18564012,-2.26353613,509.73,40.51,273.26,193.72,193.56,-0.66,85.05,838.65,75.17,0.1726,0.0000,-0.1699,1.0000,4 +129.406,51.18567946,-2.26456656,495.25,38.60,270.74,195.51,194.49,3.07,60.15,832.79,74.75,0.0765,0.6039,0.0000,1.0000,4 +130.234,51.18567824,-2.26577587,469.08,27.06,268.74,198.01,196.60,3.04,-10.31,826.99,74.31,0.2922,0.1177,-0.0449,1.0000,4 +130.953,51.18567226,-2.26680120,453.56,26.11,270.24,199.32,198.43,1.14,-20.43,821.16,73.90,0.1353,0.0000,-0.1543,1.0000,4 +131.609,51.18568256,-2.26780558,443.55,26.55,271.58,200.34,199.44,0.88,-24.97,816.45,73.51,0.1941,-0.0566,-0.0840,1.0000,4 +132.297,51.18571115,-2.26882852,435.93,30.32,274.29,200.96,200.25,-1.27,-23.35,811.28,73.13,0.1549,-0.2812,-0.1445,1.0000,4 +132.984,51.18576424,-2.26984684,434.94,44.49,275.22,201.37,200.56,-0.85,8.74,805.57,72.74,0.0000,-0.4746,-0.1660,1.0000,4 +133.594,51.18581505,-2.27074190,433.58,55.73,274.79,201.60,200.87,-1.32,75.39,800.81,72.40,0.2353,-0.5996,-0.1680,1.0000,4 +134.234,51.18584548,-2.27172690,424.82,48.16,260.35,200.74,200.52,0.46,88.12,795.47,72.04,0.6490,0.4118,0.0000,1.0000,4 +134.813,51.18575375,-2.27256571,410.29,29.81,242.35,197.95,197.87,-0.21,75.19,790.88,71.72,0.6039,0.3000,-0.0508,1.0000,4 +135.438,51.18549993,-2.27333230,400.04,19.91,218.81,192.97,194.58,-5.55,73.60,786.94,71.40,0.7333,0.1412,-0.1582,1.0000,4 +136.031,51.18509628,-2.27385005,404.47,22.45,198.27,187.30,188.15,-8.25,76.54,782.26,71.08,0.5804,0.0000,-0.1914,1.0000,4 +136.672,51.18455996,-2.27411213,418.39,30.93,178.47,183.54,184.02,-8.99,80.78,777.08,70.71,0.5823,-0.1152,-0.1855,1.0000,4 +137.219,51.18408965,-2.27411145,430.98,43.35,170.38,182.27,181.94,-6.67,86.20,771.92,70.41,0.2589,-0.2500,-0.1953,1.0000,4 +137.828,51.18357612,-2.27397572,438.77,50.84,165.68,182.71,182.53,-3.30,88.78,766.70,70.06,0.2706,0.0000,-0.1777,1.0000,4 +138.328,51.18317403,-2.27381872,437.80,49.65,162.06,183.32,183.14,-1.00,84.01,762.33,69.80,0.1745,0.4196,-0.0586,1.0000,4 +138.938,51.18268224,-2.27356665,428.70,40.37,160.11,184.84,184.15,1.77,46.70,757.46,69.47,0.1686,0.5196,0.0000,1.0000,4 +139.516,51.18225708,-2.27330546,417.39,26.61,157.68,186.23,185.47,0.68,24.66,753.56,69.18,0.3353,0.1118,-0.1289,1.0000,4 +140.156,51.18171271,-2.27294186,410.94,18.95,155.42,187.01,186.60,-2.54,14.76,748.06,68.80,0.2098,0.2451,0.0000,1.0000,4 +140.703,51.18128687,-2.27262292,414.11,22.32,154.52,187.34,186.77,-3.72,6.61,744.60,68.52,0.2177,0.2314,0.0000,1.0000,4 diff --git a/track_data/flight_20260831_213057_021348.csv b/track_data/flight_20260831_213057_021348.csv new file mode 100644 index 00000000..716b3f48 --- /dev/null +++ b/track_data/flight_20260831_213057_021348.csv @@ -0,0 +1,239 @@ +# aircraft_title=Extra 300S Paint2 +# recorded_at=2026-08-31T21:30:57 +# laps_s=29.500,34.500,36.390,33.093 +timestamp_s,latitude,longitude,altitude_ft,agl_ft,heading_deg,airspeed_kts,groundspeed_kts,pitch_deg,bank_deg,torque,health_pct,stick_pitch,stick_roll,stick_yaw,stick_throttle,lap +0.344,51.18973532,-2.27594718,981.56,617.77,166.17,183.67,182.52,6.73,0.12,1098.64,100.00,-0.2031,0.0000,0.0000,1.0000,1 +0.860,51.18930407,-2.27577870,958.23,589.12,165.99,187.60,185.83,8.75,0.16,1169.85,100.00,-0.2090,0.0000,0.0000,1.0000,1 +1.360,51.18888536,-2.27561434,930.87,558.34,165.95,191.41,188.71,10.31,0.17,1174.01,100.00,-0.1816,0.0000,-0.0078,1.0000,1 +1.860,51.18845974,-2.27544634,899.27,523.36,165.99,195.52,192.01,10.72,0.17,1174.64,100.00,-0.1074,0.0000,0.0000,1.0000,1 +2.438,51.18802783,-2.27527567,861.49,480.48,165.99,200.07,196.13,10.93,0.18,1177.19,100.00,-0.0449,0.0000,0.0000,1.0000,1 +3.000,51.18746920,-2.27505453,820.94,439.46,165.99,204.70,200.34,11.04,0.18,1176.81,100.00,0.0000,0.0883,0.0000,1.0000,1 +3.625,51.18690856,-2.27483358,775.75,394.21,165.98,209.42,204.84,11.05,0.19,1179.49,100.00,0.0000,0.0863,0.0000,1.0000,1 +4.250,51.18633197,-2.27460078,729.88,348.39,165.97,214.10,209.21,10.93,0.19,1180.47,100.00,0.0000,0.0883,0.0000,1.0000,1 +4.766,51.18582125,-2.27440042,690.61,309.15,165.96,217.78,212.85,10.76,0.20,1181.23,100.00,0.0000,0.0883,0.0000,1.0000,1 +5.313,51.18531176,-2.27419766,651.55,269.95,165.95,221.48,216.34,10.51,0.20,1183.18,100.00,0.0000,0.0883,0.0000,1.0000,1 +5.813,51.18481546,-2.27400136,613.59,225.27,165.95,224.66,219.60,10.20,0.21,1183.39,100.00,0.0000,0.0902,0.0000,1.0000,1 +6.328,51.18429732,-2.27379282,576.33,187.02,165.94,227.97,222.79,9.83,0.21,1185.58,100.00,0.0000,0.0902,0.0000,1.0000,1 +6.907,51.18371122,-2.27356013,535.56,146.24,165.93,231.18,226.18,9.35,0.22,1185.57,100.00,0.0000,0.0922,0.0000,1.0000,1 +7.422,51.18319207,-2.27335263,501.19,111.40,165.93,234.03,229.01,8.87,0.22,1188.12,100.00,0.0000,0.1059,0.0000,1.0000,1 +8.016,51.18259686,-2.27311389,463.20,73.77,165.92,236.79,232.02,8.26,-0.27,1187.79,100.00,0.1275,0.1471,0.0000,1.0000,1 +8.610,51.18195098,-2.27285637,428.01,35.26,166.34,239.00,235.85,1.68,-0.56,1190.30,100.00,0.3980,0.1098,0.0000,1.0000,1 +9.203,51.18132813,-2.27261030,419.83,27.95,166.36,239.61,237.54,-2.42,-0.54,1189.77,100.00,0.1236,0.0471,0.0000,1.0000,1 +9.797,51.18069261,-2.27236158,426.61,34.91,166.23,240.03,237.73,-3.08,-6.26,1191.59,100.00,-0.0566,0.1079,0.0000,1.0000,1 +10.328,51.18012820,-2.27214447,435.61,43.98,166.51,240.18,237.84,-3.47,-7.44,1190.86,100.00,-0.1621,-0.1426,0.0000,1.0000,1 +10.953,51.17945772,-2.27189172,447.23,55.61,167.22,240.45,238.12,-2.88,25.73,1190.20,100.00,-0.1680,-0.6172,-0.0898,1.0000,1 +11.485,51.17887131,-2.27166901,454.01,59.06,166.38,240.56,238.63,-3.30,76.00,1189.23,100.00,0.0883,-0.3379,-0.0195,1.0000,1 +11.985,51.17834701,-2.27145220,455.56,53.54,159.23,240.71,239.01,-2.41,84.57,1188.98,100.00,0.5725,-0.0059,-0.0352,1.0000,1 +12.485,51.17782311,-2.27111699,454.08,51.99,139.55,236.89,236.80,-2.73,86.23,1188.60,100.00,0.5843,0.0000,-0.0371,1.0000,1 +13.016,51.17739118,-2.27053248,451.32,49.22,123.49,233.93,233.15,-2.17,82.34,1188.75,100.00,0.5294,0.2059,0.0000,1.0000,1 +13.641,51.17702343,-2.26963063,448.51,46.50,108.31,232.47,231.50,-3.31,74.37,1189.24,100.00,0.4706,0.0314,-0.0195,1.0000,1 +14.188,51.17684348,-2.26875760,450.99,49.07,95.27,231.21,230.27,-5.24,74.53,1189.43,100.00,0.5333,0.0000,-0.0586,1.0000,1 +14.703,51.17679021,-2.26784423,459.58,45.78,83.64,229.66,228.40,-6.18,75.70,1189.59,100.00,0.2490,0.1726,0.0000,1.0000,1 +15.313,51.17685182,-2.26688147,471.00,76.31,81.04,230.19,228.48,-4.11,65.26,1189.73,100.00,0.0236,0.2432,0.0000,1.0000,1 +15.797,51.17693684,-2.26606587,477.60,82.78,79.20,230.85,229.25,-3.04,63.04,1188.91,100.00,0.0981,0.0647,0.0000,1.0000,1 +16.328,51.17705141,-2.26518525,482.20,87.47,70.47,230.69,229.61,-6.16,63.94,1188.88,100.00,0.5078,0.0000,-0.0410,1.0000,1 +16.891,51.17726400,-2.26427638,497.01,69.23,61.99,229.59,227.51,-7.56,66.45,1188.25,100.00,0.2608,-0.0957,-0.0605,1.0000,1 +17.360,51.17750457,-2.26356213,512.84,70.62,59.24,229.60,227.57,-6.59,79.15,1187.77,100.00,0.1137,-0.3418,-0.0996,1.0000,1 +17.907,51.17777648,-2.26285402,525.86,74.08,50.53,229.34,228.10,-5.61,87.56,1187.22,100.00,0.4784,0.0000,-0.0195,1.0000,1 +18.703,51.17832902,-2.26186881,536.18,46.26,25.97,225.77,226.06,-3.36,89.67,1186.17,100.00,0.5725,0.1039,-0.0371,1.0000,1 +19.313,51.17892247,-2.26140345,535.08,32.83,16.97,225.39,224.40,-0.96,73.24,1186.37,100.00,0.1334,0.4922,0.0000,1.0000,1 +20.000,51.17958397,-2.26108405,528.77,24.30,14.45,227.37,226.06,-0.60,-2.81,1186.21,100.00,0.2589,0.5804,0.0000,1.0000,1 +20.672,51.18027447,-2.26078097,529.98,26.68,18.49,228.30,227.07,-4.50,-48.29,1186.64,100.00,0.2961,0.2530,0.0000,1.0000,1 +21.250,51.18083763,-2.26045528,540.38,34.10,22.16,228.71,227.15,-4.71,-48.15,1186.64,100.00,0.1510,-0.2891,-0.0684,1.0000,1 +21.797,51.18138150,-2.26010166,551.57,35.05,23.83,229.34,227.75,-3.85,-23.63,1185.95,100.00,0.0373,-0.4238,-0.1348,1.0000,1 +22.313,51.18188830,-2.25973640,561.78,34.90,24.91,229.83,228.26,-3.97,21.83,1185.89,100.00,0.1471,-0.5957,-0.1465,1.0000,1 +22.907,51.18246495,-2.25932804,572.78,31.39,21.76,230.19,228.95,-5.38,74.59,1184.86,100.00,0.3588,-0.3379,-0.0723,1.0000,1 +23.453,51.18302674,-2.25901081,581.75,33.21,2.09,227.58,227.93,-6.94,83.62,1184.38,100.00,0.8039,-0.1387,-0.0391,1.0000,1 +24.000,51.18353888,-2.25898567,592.31,45.69,335.24,218.68,220.34,-7.37,88.05,1184.11,100.00,0.6725,-0.0977,-0.0449,1.0000,1 +24.641,51.18413242,-2.25942359,603.47,64.86,317.58,215.72,215.70,-4.29,90.62,1184.15,100.00,0.4706,0.0000,-0.0547,1.0000,1 +25.219,51.18457099,-2.26005885,605.25,78.24,309.99,216.59,216.10,-1.30,91.74,1182.74,99.87,0.2510,0.0000,-0.0547,1.0000,1 +25.828,51.18495825,-2.26078132,597.21,82.25,303.36,218.34,217.52,1.43,92.41,1178.06,99.55,0.3431,-0.0879,-0.0273,1.0000,1 +26.453,51.18530616,-2.26162499,577.72,73.39,295.27,220.47,218.60,4.34,92.68,1173.39,99.22,0.3451,0.0000,-0.0254,1.0000,1 +27.078,51.18556168,-2.26248399,547.72,57.02,283.25,222.16,219.18,6.98,90.66,1169.80,98.91,0.4647,0.1804,0.0000,1.0000,1 +27.641,51.18570482,-2.26342310,508.25,35.94,271.76,224.17,219.58,7.49,67.49,1166.66,98.57,0.4922,0.5216,0.0000,1.0000,1 +28.297,51.18571912,-2.26445370,473.89,15.90,261.90,225.08,223.26,0.88,30.92,1162.45,98.22,0.3235,0.4922,0.0000,1.0000,1 +28.938,51.18562264,-2.26552852,465.69,21.47,260.87,226.69,225.29,-2.64,-27.15,1159.61,97.83,0.2353,0.5765,0.0000,1.0000,1 +29.532,51.18553425,-2.26650732,470.22,38.74,265.37,227.30,225.98,-3.42,-90.38,1154.66,97.50,0.4255,0.3726,0.0000,1.0000,1 +30.157,51.18549971,-2.26756838,467.91,49.35,276.55,227.42,226.04,0.59,-74.69,1149.57,97.14,0.1393,-0.7383,-0.2148,1.0000,1 +30.766,51.18556193,-2.26852805,458.48,50.01,277.68,229.00,227.23,1.41,-24.15,1145.21,96.83,-0.0156,-0.2500,-0.1699,1.0000,1 +31.375,51.18565384,-2.26955074,448.94,53.43,278.62,229.65,227.93,1.00,-7.89,572.39,96.72,-0.0020,-0.3633,-0.1641,1.0000,1 +31.985,51.18575339,-2.27057098,441.83,61.70,278.93,228.44,226.83,0.10,42.48,549.81,96.92,0.0020,-0.5820,-0.1621,1.0000,1 +32.563,51.18583784,-2.27154066,434.69,59.60,274.26,226.45,225.02,-0.50,76.73,854.43,97.08,0.3667,-0.1387,-0.0312,1.0000,1 +33.125,51.18586922,-2.27247356,425.89,45.65,245.87,220.14,222.74,-5.59,80.43,1148.61,96.80,1.0000,-0.1074,-0.0684,1.0000,1 +33.750,51.18560721,-2.27333315,432.55,53.09,211.11,199.27,202.00,-12.09,83.89,1143.95,96.45,0.3706,0.0000,-0.1602,1.0000,1 +34.313,51.18516736,-2.27378420,446.09,65.44,205.71,202.29,201.47,-6.45,88.12,1140.02,96.15,0.5823,0.0236,-0.1074,1.0000,1 +34.953,51.18462799,-2.27414530,454.28,66.40,178.31,197.94,199.81,-4.06,93.52,1133.67,95.79,0.7098,0.0000,-0.1055,1.0000,1 +35.532,51.18410029,-2.27414568,450.40,62.03,167.51,197.40,196.98,0.25,89.12,1129.40,95.48,0.1726,0.4255,0.0000,1.0000,1 +36.172,51.18353934,-2.27395556,436.42,47.75,163.27,200.48,199.36,1.58,42.00,1124.42,95.13,0.2177,0.5490,0.0000,1.0000,1 +36.750,51.18301218,-2.27369920,425.26,36.99,161.19,202.90,201.93,0.17,18.83,746.07,94.84,0.2020,0.0000,-0.1816,1.0000,1 +37.344,51.18249545,-2.27340901,421.92,33.49,159.36,202.52,201.88,-3.08,17.72,539.23,95.00,0.1883,-0.0078,-0.1758,1.0000,1 +37.907,51.18199108,-2.27310057,427.35,35.82,158.35,201.19,200.26,-3.29,15.16,536.79,95.18,-0.0527,0.3314,0.0000,1.0000,1 +38.453,51.18152366,-2.27279903,434.47,42.86,157.56,199.74,198.78,-3.62,-12.23,538.42,95.35,0.0000,0.4804,0.0000,1.0000,1 +39.157,51.18094537,-2.27242609,443.88,52.10,158.98,198.01,197.31,-4.25,-57.30,539.99,95.56,0.3353,0.3784,0.0000,1.0000,2 +39.735,51.18043965,-2.27214822,449.64,57.83,167.15,196.20,195.85,-4.78,-68.50,541.68,95.75,0.2941,0.0000,0.0000,1.0000,2 +40.360,51.17989639,-2.27194689,455.22,63.31,169.25,195.07,194.57,-2.60,-60.71,543.10,95.94,-0.1680,-0.3613,-0.0430,1.0000,2 +40.922,51.17939065,-2.27180688,454.82,62.87,170.35,194.57,194.01,-0.09,-22.19,544.21,96.12,-0.0547,-0.6406,-0.1016,1.0000,2 +41.563,51.17881313,-2.27166221,450.19,53.25,171.60,194.18,193.60,-0.05,53.42,545.76,96.32,0.0000,-0.6035,-0.1055,1.0000,2 +42.125,51.17831159,-2.27152269,441.83,39.45,166.85,193.77,193.21,0.01,78.99,547.35,96.50,0.4647,-0.2129,-0.1699,1.0000,2 +42.703,51.17780764,-2.27130079,429.61,27.23,142.35,188.52,190.52,-2.37,79.94,549.09,96.68,0.7411,0.2804,-0.0723,1.0000,2 +43.219,51.17745088,-2.27090312,422.31,20.04,128.24,184.82,185.47,-3.11,75.40,550.85,96.84,0.7411,0.2824,-0.1328,1.0000,2 +43.907,51.17711630,-2.27015806,420.09,18.22,108.65,180.41,181.09,-5.79,73.64,553.04,97.05,0.5078,0.0844,-0.1816,1.0000,2 +44.563,51.17694157,-2.26931522,425.16,23.37,97.27,181.05,181.14,-5.70,70.65,1151.17,96.80,0.2922,0.0549,-0.1484,1.0000,2 +45.110,51.17688104,-2.26860700,429.49,24.42,94.93,183.29,183.01,-3.52,70.60,1148.23,96.50,0.3235,0.0000,-0.0820,1.0000,2 +45.610,51.17684844,-2.26790918,430.08,20.07,89.03,185.17,185.09,-3.29,67.78,1143.01,96.20,0.0118,0.4412,0.0000,1.0000,2 +46.266,51.17685988,-2.26702526,428.62,31.46,85.24,188.13,187.76,-2.63,46.74,1137.38,95.87,0.3353,0.2451,0.0000,1.0000,2 +46.813,51.17690827,-2.26624783,429.64,34.77,80.96,188.70,188.38,-4.57,43.90,550.86,95.93,0.4431,-0.0312,-0.0547,1.0000,2 +47.438,51.17700666,-2.26542069,439.50,45.14,72.71,186.18,185.61,-10.26,50.27,544.05,96.11,0.4235,-0.3301,-0.1250,1.0000,2 +48.047,51.17717504,-2.26461488,462.60,46.92,66.91,183.71,182.24,-10.08,75.47,545.66,96.32,0.2589,-0.4023,-0.1836,1.0000,2 +48.563,51.17735159,-2.26396676,479.46,43.57,62.93,183.09,182.23,-8.09,83.86,1035.56,96.37,0.2941,0.1216,0.0000,1.0000,2 +49.094,51.17755309,-2.26334557,490.94,44.59,57.70,184.01,183.76,-6.10,81.36,1139.43,96.09,0.3608,0.2784,0.0000,1.0000,2 +49.766,51.17784749,-2.26262086,499.63,40.90,41.26,184.05,184.96,-8.01,75.20,1134.17,95.75,0.6372,-0.0273,-0.1621,1.0000,2 +50.344,51.17822175,-2.26208842,510.84,34.26,26.01,182.32,182.80,-8.37,79.62,730.21,95.47,0.3392,0.0000,-0.1699,1.0000,2 +50.891,51.17863180,-2.26174725,521.79,30.25,21.19,181.36,181.37,-5.95,80.80,544.36,95.62,0.1530,0.0353,-0.1621,1.0000,2 +51.516,51.17911980,-2.26144386,527.98,28.33,18.96,181.12,181.13,-2.99,42.96,541.98,95.81,-0.0098,0.6784,0.0000,1.0000,2 +52.157,51.17962258,-2.26118023,530.82,30.13,17.34,180.95,180.86,-2.09,-19.73,540.88,96.01,0.1490,0.2745,0.0000,1.0000,2 +52.766,51.18009702,-2.26092650,533.28,31.99,19.42,180.51,180.52,-3.62,-31.60,542.57,96.20,0.3118,0.1922,-0.0020,1.0000,2 +53.344,51.18056400,-2.26065259,538.82,33.86,22.25,179.80,179.70,-4.75,-33.81,544.46,96.39,0.1471,-0.0312,-0.1094,1.0000,2 +53.938,51.18100757,-2.26034875,546.66,37.05,23.50,179.31,179.19,-3.82,-23.70,546.35,96.58,0.0000,-0.3379,-0.1602,1.0000,2 +54.532,51.18146039,-2.26002000,553.31,33.41,24.57,180.30,180.09,-2.79,12.50,1096.53,96.59,0.1490,-0.5273,-0.1562,1.0000,2 +55.157,51.18194604,-2.25967408,558.88,26.92,22.52,182.36,182.38,-5.22,47.99,1139.15,96.24,0.3333,-0.4824,-0.1484,1.0000,2 +55.688,51.18238772,-2.25940103,565.68,25.65,15.18,183.74,184.01,-6.44,79.01,1133.73,95.93,0.4510,-0.4160,-0.1602,1.0000,2 +56.203,51.18280211,-2.25922551,570.32,25.51,4.75,184.66,185.33,-4.02,91.86,1128.82,95.65,0.5451,0.0314,-0.1602,1.0000,2 +56.735,51.18324904,-2.25915444,568.48,23.66,352.29,185.40,186.17,-1.42,87.73,1124.61,95.37,0.5941,0.3216,-0.0586,1.0000,2 +57.360,51.18377011,-2.25927255,559.02,17.70,334.83,185.31,186.20,-1.03,79.92,1119.62,95.04,0.5039,0.3294,-0.1250,1.0000,2 +57.922,51.18420187,-2.25958252,549.59,13.30,323.65,186.78,187.21,-1.76,72.68,1115.26,94.74,0.5451,0.0000,-0.1895,1.0000,2 +58.563,51.18465520,-2.26012656,542.43,16.65,309.57,187.89,188.38,-2.84,73.43,1111.04,94.39,0.4922,0.0000,-0.1816,1.0000,2 +59.157,51.18498661,-2.26076989,539.08,24.08,299.37,189.35,189.62,-2.60,74.56,1106.61,94.05,0.3333,0.1510,-0.1816,1.0000,2 +59.719,51.18523908,-2.26144900,535.16,29.22,296.59,191.80,191.52,-0.20,71.36,1102.19,93.74,0.2235,0.0785,-0.1738,1.0000,2 +60.407,51.18549743,-2.26232960,524.41,30.88,289.74,194.69,194.19,0.54,71.20,1096.98,93.37,0.3804,-0.0625,-0.1816,1.0000,2 +61.110,51.18569082,-2.26324116,510.78,35.82,278.16,196.68,196.43,-0.51,71.90,1091.94,93.01,0.4392,-0.1172,-0.1719,1.0000,2 +61.844,51.18577877,-2.26431602,497.61,38.47,269.94,199.21,198.46,0.76,72.87,1086.13,92.58,0.2804,0.0000,-0.1738,1.0000,2 +62.641,51.18576642,-2.26547082,478.96,32.74,265.05,202.53,201.31,2.51,38.76,1080.31,92.13,0.1765,0.5059,0.0000,1.0000,2 +63.500,51.18568875,-2.26672590,459.70,31.54,263.84,205.95,204.88,0.22,-27.94,1074.35,91.67,0.2843,0.5000,0.0000,1.0000,2 +64.282,51.18562438,-2.26794020,450.52,35.39,268.52,207.03,206.21,-0.30,-70.69,515.66,91.55,0.2608,0.0000,-0.0625,1.0000,2 +65.063,51.18562168,-2.26910177,437.55,35.61,273.68,205.66,204.56,1.36,-30.71,507.92,91.81,0.1765,-0.6035,-0.1543,1.0000,2 +65.844,51.18567360,-2.27028879,425.22,41.67,274.59,204.48,203.49,1.02,23.94,510.93,92.06,0.0608,-0.3477,-0.1582,1.0000,2 +66.625,51.18571914,-2.27147978,413.34,36.34,270.30,203.21,202.32,-0.09,76.67,513.83,92.33,0.3275,-0.4512,-0.1797,1.0000,2 +67.266,51.18570098,-2.27244280,399.93,19.36,249.28,201.16,201.76,-1.69,77.86,1081.47,92.13,0.6862,0.2981,-0.0195,1.0000,2 +67.891,51.18550159,-2.27327002,394.70,15.06,220.70,193.96,196.43,-9.24,72.53,1079.76,91.76,0.7470,0.0000,-0.1973,1.0000,2 +68.625,51.18499000,-2.27390705,417.36,35.68,185.30,183.66,184.29,-14.64,80.50,1073.88,91.34,0.6784,-0.0742,-0.2070,1.0000,2 +69.313,51.18442334,-2.27400181,450.93,63.90,171.28,179.51,177.93,-10.56,97.25,514.45,91.38,0.3843,-0.3594,-0.2402,1.0000,2 +69.891,51.18391953,-2.27389417,469.90,82.27,166.63,177.96,177.60,-5.96,100.59,508.62,91.58,0.0785,0.3804,-0.1289,1.0000,2 +70.422,51.18350676,-2.27374673,476.09,88.18,165.44,177.40,177.36,-3.07,86.86,506.54,91.74,0.0608,0.1863,-0.1406,1.0000,2 +71.016,51.18303387,-2.27355347,473.99,85.75,163.56,177.27,177.18,-0.31,78.39,508.64,91.93,0.0000,0.4784,-0.0879,1.0000,2 +71.625,51.18257003,-2.27332979,463.74,74.90,161.48,177.92,177.39,2.18,43.39,510.83,92.11,0.0000,0.4902,-0.0664,1.0000,2 +72.266,51.18205526,-2.27304117,448.13,56.24,159.65,178.96,178.18,3.20,-1.15,512.85,92.32,0.0451,0.5314,0.0000,1.0000,2 +72.844,51.18162348,-2.27278531,434.75,42.31,159.53,179.71,178.86,3.22,-37.15,513.73,92.49,0.1765,0.3412,0.0000,1.0000,2 +73.344,51.18122101,-2.27256215,420.74,28.20,162.77,180.29,179.40,1.90,-42.35,515.66,92.65,0.3275,-0.2559,-0.2012,1.0000,2 +73.922,51.18076624,-2.27235246,408.88,16.67,167.92,180.14,180.00,-2.37,-25.60,517.39,92.83,0.3706,-0.4062,-0.2070,1.0000,3 +74.547,51.18025599,-2.27218604,410.19,18.34,168.76,179.74,179.33,-3.14,-11.52,519.69,93.02,0.0236,-0.1445,-0.2031,1.0000,3 +75.141,51.17977454,-2.27204670,415.48,23.63,169.19,179.29,178.88,-3.24,13.32,521.09,93.20,0.1902,-0.5039,-0.2031,1.0000,3 +75.735,51.17928413,-2.27189707,421.51,30.09,167.70,178.68,178.33,-4.25,53.00,522.41,93.40,0.1981,-0.4180,-0.2031,1.0000,3 +76.297,51.17883105,-2.27172494,426.03,30.47,163.37,178.02,177.94,-4.38,63.18,523.67,93.57,0.3118,-0.0938,-0.1973,1.0000,3 +76.828,51.17841772,-2.27152130,428.53,26.60,157.19,177.36,177.43,-4.34,70.00,524.97,93.73,0.3510,-0.2617,-0.2051,1.0000,3 +77.391,51.17797275,-2.27121390,429.96,27.98,148.32,176.45,176.73,-4.19,73.95,526.44,93.92,0.4980,-0.1855,-0.2031,1.0000,3 +77.891,51.17762998,-2.27087871,430.45,28.46,137.78,174.99,175.57,-4.42,77.73,527.77,94.08,0.5216,0.0000,-0.1816,1.0000,3 +78.453,51.17730957,-2.27041833,430.51,28.51,124.35,172.91,173.84,-4.51,79.72,529.13,94.25,0.5078,0.0000,-0.1836,1.0000,3 +79.032,51.17704148,-2.26981377,429.59,27.49,110.74,171.14,172.02,-4.20,79.21,530.71,94.45,0.6412,0.0000,-0.1758,1.0000,3 +79.657,51.17686059,-2.26904690,427.52,25.41,95.59,168.83,169.71,-4.07,76.40,532.50,94.64,0.4706,0.2333,-0.1699,1.0000,3 +80.219,51.17680839,-2.26837075,425.49,16.06,87.94,168.52,168.92,-3.88,62.37,533.89,94.82,0.5117,0.3138,-0.1387,1.0000,3 +80.844,51.17682934,-2.26759431,426.50,23.03,81.83,168.19,168.36,-5.53,42.95,535.60,95.01,0.2863,0.1059,-0.1387,1.0000,3 +81.422,51.17689998,-2.26688516,433.20,37.74,78.42,167.95,167.85,-6.17,40.29,537.05,95.19,0.1393,0.2412,-0.0938,1.0000,3 +82.063,51.17701033,-2.26610464,443.01,48.29,76.53,167.88,167.63,-4.69,29.78,538.75,95.40,0.1510,0.2922,-0.0137,1.0000,3 +82.625,51.17712135,-2.26542753,450.28,54.09,74.94,167.87,167.70,-4.36,24.44,539.89,95.58,0.1745,-0.1855,-0.1641,1.0000,3 +83.297,51.17726002,-2.26465409,459.68,41.59,71.72,167.45,167.27,-7.39,34.94,541.52,95.77,0.3216,-0.3555,-0.1855,1.0000,3 +83.907,51.17742011,-2.26394671,473.55,38.25,67.28,166.69,166.27,-8.77,53.09,542.78,95.97,0.3059,-0.3828,-0.1855,1.0000,3 +84.485,51.17760606,-2.26327803,488.40,42.98,60.40,165.74,165.64,-8.97,73.73,543.82,96.15,0.5823,0.0000,-0.1543,1.0000,3 +85.047,51.17782086,-2.26269845,500.56,43.50,46.84,163.70,164.44,-9.11,81.26,545.14,96.32,0.6157,0.0314,-0.1680,1.0000,3 +85.657,51.17812009,-2.26218023,510.73,38.02,31.95,161.14,162.29,-7.59,85.26,546.14,96.51,0.5431,0.0451,-0.1777,1.0000,3 +86.407,51.17859485,-2.26169413,516.47,21.16,24.60,161.16,161.76,-4.25,67.08,547.96,96.75,0.1883,0.4000,-0.1660,1.0000,3 +87.032,51.17902223,-2.26138257,516.59,14.29,21.11,161.99,162.38,-3.44,44.08,549.31,96.94,0.4059,0.2039,-0.1211,1.0000,3 +87.641,51.17945262,-2.26113718,518.12,13.14,18.11,162.51,162.77,-3.56,27.59,550.95,97.14,0.2726,0.3138,-0.0039,1.0000,3 +88.219,51.17985902,-2.26093841,521.91,17.45,16.47,163.06,163.19,-3.29,3.60,552.61,97.33,0.1745,0.5157,0.0000,1.0000,3 +88.891,51.18035230,-2.26071339,527.98,23.29,17.02,163.45,163.60,-5.01,-34.10,554.32,97.54,0.3549,0.4647,0.0000,1.0000,3 +89.422,51.18075743,-2.26050154,534.85,28.05,21.88,163.26,163.54,-6.47,-62.41,555.77,97.72,0.3745,0.3804,0.0000,1.0000,3 +89.985,51.18114961,-2.26024158,541.51,28.65,24.89,163.39,163.71,-4.66,-57.00,557.10,97.90,0.0451,-0.6211,-0.1816,1.0000,3 +90.610,51.18157698,-2.25990821,545.06,21.34,27.12,163.98,164.31,-4.39,-9.95,558.42,98.10,0.2608,-0.5078,-0.1816,1.0000,3 +91.219,51.18197696,-2.25957563,553.03,18.32,26.41,163.94,163.92,-7.29,36.73,559.97,98.28,0.3529,-0.6016,-0.2070,1.0000,3 +91.828,51.18240700,-2.25926208,567.92,23.93,14.53,162.08,162.53,-10.69,82.72,561.34,98.48,0.5294,-0.3984,-0.2070,1.0000,3 +92.391,51.18282589,-2.25908810,580.44,32.98,2.60,162.39,163.31,-6.70,93.63,1159.86,98.33,0.5255,0.0000,-0.1895,1.0000,3 +92.907,51.18319390,-2.25904228,584.02,36.82,353.04,163.86,164.94,-3.03,94.40,1164.85,98.05,0.5510,0.2314,-0.0977,1.0000,3 +93.532,51.18366081,-2.25912458,577.80,34.11,336.76,165.12,166.56,-1.08,83.21,1159.21,97.72,0.5431,0.3765,0.0000,1.0000,3 +94.078,51.18404685,-2.25936252,567.67,27.81,324.65,166.63,167.63,-1.50,73.56,1154.49,97.42,0.5725,0.2804,-0.0391,1.0000,3 +94.641,51.18442571,-2.25978090,557.94,25.47,317.03,169.55,169.94,-1.00,67.33,1150.44,97.09,0.1530,0.0000,-0.1641,1.0000,3 +95.266,51.18475848,-2.26028607,547.82,24.72,312.80,173.49,173.45,-0.06,52.78,1146.04,96.78,0.5431,0.0000,-0.1855,1.0000,3 +95.813,51.18507984,-2.26085728,539.79,25.55,304.03,176.10,176.54,-2.57,67.91,1141.77,96.44,0.3882,-0.4688,-0.2188,1.0000,3 +96.485,51.18536932,-2.26155387,533.70,28.37,294.17,179.12,179.34,-0.76,82.41,1136.77,96.10,0.4961,-0.0039,-0.1895,1.0000,3 +97.157,51.18559577,-2.26239871,520.91,28.43,280.27,181.48,181.60,0.84,80.84,1131.49,95.72,0.3804,0.2882,-0.1543,1.0000,3 +97.766,51.18569437,-2.26317625,505.58,29.25,274.59,184.96,184.22,1.44,53.30,1127.47,95.39,0.4392,0.3726,-0.1074,1.0000,3 +98.485,51.18573128,-2.26418576,491.10,30.51,269.14,188.55,188.09,-0.25,39.19,1121.31,94.98,0.2961,0.0000,-0.1719,1.0000,3 +99.188,51.18570915,-2.26513765,485.03,35.55,265.96,191.64,191.20,-0.56,27.18,1116.48,94.59,-0.1543,0.4412,0.0000,1.0000,3 +99.782,51.18566510,-2.26601200,480.63,42.17,264.70,194.49,193.82,1.38,-7.66,1111.45,94.23,-0.1055,0.4843,0.0000,1.0000,3 +100.469,51.18561211,-2.26697829,470.45,45.11,264.81,197.57,196.72,1.46,-58.22,1105.58,93.84,0.1118,0.5176,0.0000,1.0000,3 +101.047,51.18557578,-2.26782506,456.04,39.37,269.18,200.21,198.95,2.72,-82.81,1100.93,93.51,0.2961,-0.1602,-0.0762,1.0000,3 +101.625,51.18556758,-2.26868720,434.96,27.21,271.33,203.44,201.13,5.23,-40.66,1096.54,93.19,0.2706,-0.5840,-0.1680,1.0000,3 +102.203,51.18558807,-2.26955288,412.96,16.64,275.16,206.04,204.40,1.96,-21.12,1092.61,92.87,0.2941,-0.5059,-0.1387,1.0000,3 +102.907,51.18564553,-2.27057424,402.10,21.59,273.69,208.06,207.20,-3.46,39.51,1088.12,92.47,0.3726,-0.4512,-0.1719,1.0000,3 +103.532,51.18566727,-2.27158990,406.84,29.81,265.36,208.66,207.83,-5.30,73.53,1082.44,92.09,0.4255,-0.1953,-0.1758,1.0000,3 +104.125,51.18560921,-2.27250029,413.42,33.60,250.61,207.99,207.72,-4.97,85.16,1077.80,91.77,0.6608,0.0000,-0.1328,1.0000,3 +104.735,51.18540946,-2.27335101,416.55,36.54,221.28,201.51,203.82,-3.73,89.08,1072.71,91.44,0.7863,0.2784,-0.0664,1.0000,3 +105.328,51.18501839,-2.27392889,414.43,32.31,195.88,194.97,196.64,-4.41,81.96,1068.49,91.12,0.7686,0.0000,-0.1641,1.0000,3 +105.969,51.18446612,-2.27415344,415.18,27.32,173.64,189.92,190.81,-5.33,77.22,1063.84,90.75,0.0294,0.3706,-0.1113,1.0000,3 +106.532,51.18397794,-2.27409055,417.44,29.45,170.95,192.40,191.96,-3.09,70.73,1059.78,90.44,0.4784,0.0000,-0.1660,1.0000,3 +107.141,51.18342307,-2.27392697,417.26,29.22,164.61,194.02,193.54,-2.39,63.08,1054.66,90.08,0.3216,0.2471,-0.0586,1.0000,3 +107.703,51.18294467,-2.27370154,415.86,27.79,161.49,195.84,195.21,-1.47,50.82,1050.16,89.77,0.1334,0.3275,0.0000,1.0000,3 +108.407,51.18235925,-2.27337341,411.98,22.45,159.33,198.20,197.39,-0.00,18.36,1044.84,89.41,0.1490,0.4490,0.0000,1.0000,3 +109.032,51.18180943,-2.27303641,407.67,15.56,158.55,200.32,199.44,-0.81,-10.65,1039.46,89.04,0.2824,0.2412,0.0000,1.0000,3 +109.563,51.18135608,-2.27275829,407.73,15.84,159.48,201.56,200.68,-2.31,-25.62,1035.14,88.75,0.0765,0.4333,0.0000,1.0000,3 +110.219,51.18077837,-2.27242916,410.19,18.19,160.80,203.04,202.21,-1.97,-56.39,1029.95,88.38,0.1765,0.2647,0.0000,1.0000,4 +110.828,51.18023160,-2.27214881,407.79,15.64,164.31,204.56,203.68,-0.99,-63.94,1024.94,88.05,0.2255,0.1039,0.0000,1.0000,4 +111.485,51.17964507,-2.27190449,401.33,9.15,168.57,206.25,205.25,-1.61,-18.55,1019.85,87.70,0.4078,-0.5918,-0.1953,1.0000,4 +112.141,51.17903188,-2.27170285,407.70,15.15,166.91,206.33,204.96,-9.08,35.13,1014.83,87.34,0.3333,-0.6055,-0.2012,1.0000,4 +112.688,51.17850882,-2.27148892,430.44,31.97,160.26,205.58,203.43,-9.91,89.67,1010.23,87.02,0.3451,-0.5430,-0.2109,1.0000,4 +113.188,51.17804941,-2.27122433,448.04,46.46,150.44,204.73,203.91,-5.89,100.05,1005.23,86.73,0.4961,0.3608,0.0000,1.0000,4 +113.688,51.17764449,-2.27087250,454.29,52.31,135.89,202.99,203.43,-2.37,93.46,1001.13,86.44,0.6216,0.4588,0.0000,1.0000,4 +114.344,51.17722451,-2.27020669,450.87,48.76,114.15,199.24,200.05,-3.47,77.63,994.82,86.08,0.5902,0.3275,-0.0332,1.0000,4 +114.953,51.17699928,-2.26938920,451.26,49.39,97.54,196.72,197.04,-5.10,76.26,990.15,85.72,0.4000,0.0039,-0.1699,1.0000,4 +115.516,51.17692272,-2.26858198,455.06,49.10,90.98,197.46,197.11,-4.23,68.77,985.94,85.40,0.3412,0.2745,-0.0020,1.0000,4 +116.125,51.17691790,-2.26767405,459.42,59.76,84.85,198.16,197.69,-4.68,54.45,980.84,85.05,0.3039,0.3138,0.0000,1.0000,4 +116.657,51.17696431,-2.26691793,466.28,70.95,80.96,198.62,197.92,-5.80,40.62,976.62,84.75,0.2549,0.2745,0.0000,1.0000,4 +117.282,51.17706197,-2.26603193,480.24,85.66,78.07,199.06,197.94,-6.33,38.38,971.44,84.42,0.2059,-0.0996,-0.1973,1.0000,4 +117.813,51.17717287,-2.26524837,494.44,98.20,75.31,199.24,198.09,-6.70,48.31,966.83,84.11,0.2098,-0.3145,-0.1992,1.0000,4 +118.422,51.17732430,-2.26439366,510.50,89.94,68.99,199.20,198.37,-7.16,77.74,961.27,83.77,0.4353,-0.1113,-0.1523,1.0000,4 +118.985,51.17751762,-2.26361970,522.72,80.50,60.78,198.70,198.28,-6.06,82.00,955.82,83.45,0.2824,-0.1719,-0.1543,1.0000,4 +119.532,51.17774793,-2.26294463,529.91,76.47,52.76,198.94,198.80,-4.04,88.91,951.57,83.16,0.6961,0.2079,-0.0059,1.0000,4 +120.172,51.17811072,-2.26223243,530.10,55.74,35.14,196.57,197.15,-2.23,80.30,946.31,82.82,0.3000,0.6137,0.0000,1.0000,4 +120.813,51.17859642,-2.26168001,527.56,32.09,23.84,196.30,196.59,-5.19,60.71,940.82,82.43,0.3079,0.0726,-0.1094,1.0000,4 +121.407,51.17909641,-2.26133645,534.12,31.67,17.92,196.66,196.38,-5.74,60.97,936.38,82.10,0.3275,0.0000,-0.1523,1.0000,4 +122.063,51.17963361,-2.26107391,543.56,39.57,14.42,197.13,196.65,-4.40,42.40,931.33,81.77,-0.1328,0.5608,0.0000,1.0000,4 +122.828,51.18028989,-2.26081764,554.51,52.72,12.70,197.93,197.46,-4.33,-62.07,925.46,81.36,0.2765,0.7039,0.0000,1.0000,4 +123.453,51.18088160,-2.26058278,558.84,54.84,17.28,198.52,198.39,-2.53,-79.33,919.30,80.97,0.1706,-0.6387,-0.2090,1.0000,4 +124.219,51.18152735,-2.26023980,556.12,41.76,20.80,199.87,199.52,-1.50,-35.11,913.05,80.54,0.1843,0.0000,0.0000,1.0000,4 +124.953,51.18213111,-2.25985838,555.41,26.86,22.61,200.97,200.55,-2.31,11.01,906.97,80.13,0.2589,-0.5547,-0.1699,1.0000,4 +125.735,51.18284837,-2.25941868,565.77,25.36,15.62,200.54,200.04,-7.61,74.49,900.00,79.63,0.3647,-0.5547,-0.1758,1.0000,4 +126.344,51.18339448,-2.25919678,577.85,34.90,0.52,198.83,199.31,-5.60,91.44,894.48,79.27,0.6294,0.0000,-0.0449,1.0000,4 +126.969,51.18395197,-2.25920551,580.73,39.10,335.94,193.32,195.32,-1.92,91.93,889.69,78.93,0.5235,0.3275,0.0000,1.0000,4 +127.688,51.18454058,-2.25961350,569.39,33.58,315.09,191.32,192.31,-0.22,82.41,883.65,78.51,0.5706,0.3510,0.0000,1.0000,4 +128.485,51.18502050,-2.26043176,553.14,32.61,298.27,190.34,190.45,-0.51,74.83,878.17,78.06,0.3412,0.0941,-0.0332,1.0000,4 +129.266,51.18533719,-2.26139617,537.35,30.30,292.64,192.49,191.89,1.06,58.83,872.57,77.61,0.3412,0.0000,-0.0352,1.0000,4 +130.032,51.18558966,-2.26241521,522.06,30.82,282.27,193.55,193.44,-1.51,65.17,865.99,77.15,0.3804,-0.2852,-0.0723,1.0000,4 +130.719,51.18571189,-2.26340047,514.20,42.35,272.30,193.88,193.91,-1.89,72.69,860.75,76.74,0.2961,-0.0273,-0.0840,1.0000,4 +131.391,51.18573546,-2.26437442,505.79,47.26,269.37,195.30,194.75,0.83,62.38,855.45,76.34,0.0000,0.2137,0.0000,1.0000,4 +132.110,51.18571918,-2.26534928,490.62,42.88,266.84,197.11,196.03,2.88,35.11,849.84,75.95,0.0000,0.5647,0.0000,1.0000,4 +132.844,51.18567130,-2.26644452,470.51,38.12,265.53,199.07,197.90,2.67,-47.69,843.70,75.49,0.2961,0.3490,0.0000,1.0000,4 +133.453,51.18564257,-2.26737082,452.78,30.88,272.76,200.25,199.14,1.58,-61.79,839.23,75.13,0.4137,-0.2227,-0.0293,1.0000,4 +134.078,51.18567274,-2.26825462,437.63,25.36,276.31,201.37,200.18,2.48,-26.38,834.05,74.78,0.0844,-0.6055,-0.1484,1.0000,4 +134.766,51.18574909,-2.26930321,423.75,24.26,276.56,202.45,201.51,0.13,37.59,828.98,74.37,0.2686,-0.3965,-0.2012,1.0000,4 +135.453,51.18580592,-2.27030585,417.14,34.25,272.42,202.98,202.13,-1.05,43.21,823.84,73.97,0.2745,-0.3223,-0.2422,1.0000,4 +136.047,51.18581823,-2.27120567,416.07,41.77,265.88,202.74,202.07,-3.43,64.71,818.80,73.62,0.2922,-0.5371,-0.2715,1.0000,4 +136.610,51.18577573,-2.27205759,418.06,38.47,255.55,201.99,201.63,-2.83,82.70,814.33,73.30,0.5412,0.0196,-0.2012,1.0000,4 +137.203,51.18563803,-2.27289246,416.69,36.57,234.83,198.63,199.51,-2.83,83.93,809.55,72.97,0.8019,0.3608,-0.1504,1.0000,4 +137.828,51.18531186,-2.27359522,415.20,34.59,202.11,186.89,189.93,-6.34,81.36,804.42,72.61,0.6431,0.2530,-0.1914,1.0000,4 +138.438,51.18483412,-2.27392497,419.76,34.48,184.06,182.91,183.67,-5.90,80.55,799.37,72.27,0.5098,0.1961,-0.2109,1.0000,4 +139.000,51.18435099,-2.27399157,424.44,36.55,173.60,182.49,182.69,-4.36,81.37,794.45,71.94,0.5039,0.1275,-0.1914,1.0000,4 +139.641,51.18381030,-2.27389418,425.91,37.91,166.60,182.83,182.66,-2.66,61.88,788.94,71.59,0.2706,0.3882,-0.2012,1.0000,4 +140.235,51.18333335,-2.27370965,425.62,37.59,163.85,183.78,183.42,-1.39,47.94,784.22,71.27,0.1314,0.0765,-0.1875,1.0000,4 +140.813,51.18286361,-2.27348155,423.03,34.87,162.19,184.88,184.43,-0.26,42.30,779.33,70.93,0.1137,0.4412,-0.0703,1.0000,4 +141.453,51.18233731,-2.27319413,416.81,26.86,160.01,186.19,185.68,-0.27,22.51,774.23,70.57,0.1628,0.2530,-0.0742,1.0000,4 +142.016,51.18188646,-2.27292556,413.50,21.48,158.53,187.00,186.58,-2.32,10.19,769.53,70.25,0.2608,0.2882,-0.0742,1.0000,4 +142.625,51.18139560,-2.27261215,417.68,25.98,158.05,187.26,186.66,-5.09,-4.19,764.72,69.91,0.2882,0.2726,-0.0723,1.0000,4 diff --git a/track_data/flight_20260831_213452_021270.csv b/track_data/flight_20260831_213452_021270.csv new file mode 100644 index 00000000..f810d456 --- /dev/null +++ b/track_data/flight_20260831_213452_021270.csv @@ -0,0 +1,234 @@ +# aircraft_title=Extra 300S Paint2 +# recorded_at=2026-08-31T21:34:52 +# laps_s=29.362,34.334,34.768,34.233 +timestamp_s,latitude,longitude,altitude_ft,agl_ft,heading_deg,airspeed_kts,groundspeed_kts,pitch_deg,bank_deg,torque,health_pct,stick_pitch,stick_roll,stick_yaw,stick_throttle,lap +0.359,51.18961909,-2.27590108,974.71,608.93,166.11,184.79,183.47,7.28,0.14,1147.21,100.00,-0.1152,0.0000,0.0000,1.0000,1 +0.937,51.18912502,-2.27570760,947.03,576.89,166.03,189.14,187.40,8.27,0.15,1171.44,100.00,-0.1367,0.0000,0.0000,1.0000,1 +1.515,51.18864125,-2.27551537,916.75,542.73,166.01,193.55,191.24,9.05,0.16,1174.44,100.00,0.0000,0.0000,0.0000,1.0000,1 +2.015,51.18820617,-2.27534697,888.29,511.22,165.99,197.37,194.56,9.65,0.17,1175.68,100.00,0.0000,0.0000,0.0000,1.0000,1 +2.656,51.18764229,-2.27512342,847.74,466.59,165.97,202.31,198.88,10.32,0.18,1176.70,100.00,-0.0508,0.0098,-0.0137,1.0000,1 +3.203,51.18715288,-2.27493050,811.47,429.95,165.96,206.52,202.47,10.79,0.18,1178.88,100.00,-0.0938,0.0098,-0.0176,1.0000,1 +3.703,51.18668081,-2.27474298,774.48,393.12,165.95,210.36,205.86,11.16,0.19,1178.52,100.00,-0.1016,0.0118,-0.0156,1.0000,1 +4.218,51.18620271,-2.27455252,736.17,354.73,165.93,214.28,209.20,11.47,0.20,1180.76,100.00,-0.1523,0.0118,-0.0176,1.0000,1 +4.781,51.18566446,-2.27433907,691.81,310.39,165.89,218.33,212.62,12.22,0.20,1180.24,100.00,-0.1641,0.0118,-0.0117,1.0000,1 +5.312,51.18517904,-2.27414526,649.59,266.86,165.87,222.17,215.45,12.96,0.21,1183.14,100.00,-0.1660,0.0118,-0.0078,1.0000,1 +5.921,51.18457780,-2.27390599,594.12,204.21,165.88,226.77,219.04,13.13,0.22,1182.43,100.00,0.0883,0.0275,0.0000,1.0000,1 +6.421,51.18408295,-2.27370766,548.13,158.40,165.89,230.15,222.27,12.79,0.22,1186.05,100.00,0.1726,0.0412,-0.0195,1.0000,1 +6.937,51.18356447,-2.27350014,501.94,112.15,165.99,233.60,226.29,10.62,0.22,1185.36,100.00,0.2628,0.0471,-0.0215,1.0000,1 +7.531,51.18294796,-2.27325329,457.20,68.00,166.05,236.48,231.60,7.35,0.21,1189.12,100.00,0.2294,0.0490,-0.0332,1.0000,1 +8.125,51.18233210,-2.27300828,425.62,34.85,166.08,238.85,235.36,4.03,0.21,1190.09,100.00,0.2882,0.0647,-0.0039,1.0000,1 +8.703,51.18171289,-2.27275994,408.87,16.61,166.17,239.95,237.78,-0.71,0.19,1191.32,100.00,0.2882,0.0647,0.0000,1.0000,1 +9.234,51.18115293,-2.27253627,409.38,17.60,166.07,240.45,238.20,-2.52,0.22,1191.92,100.00,0.0726,0.0000,0.0000,1.0000,1 +9.812,51.18053681,-2.27229174,416.57,24.84,165.98,240.82,238.46,-2.69,0.26,1191.80,100.00,-0.1289,-0.0898,0.0000,1.0000,1 +10.343,51.17997060,-2.27206641,424.20,32.49,166.05,241.04,238.71,-2.84,9.27,1190.99,100.00,-0.0078,-0.3867,-0.0742,1.0000,1 +10.984,51.17928035,-2.27178701,433.41,41.89,165.75,241.25,239.05,-3.09,44.31,1190.60,100.00,0.0000,-0.4082,-0.0703,1.0000,1 +11.515,51.17871493,-2.27154077,438.54,39.76,163.57,241.51,239.49,-3.02,69.31,1189.72,100.00,0.3314,-0.0215,-0.0762,1.0000,1 +12.015,51.17816324,-2.27126043,441.18,39.29,154.91,241.18,239.50,-3.91,76.42,1189.55,100.00,0.5314,-0.1895,-0.0820,1.0000,1 +12.515,51.17768046,-2.27088448,445.65,43.88,137.15,237.84,237.37,-5.49,86.15,1189.01,100.00,0.5980,-0.3730,-0.0801,1.0000,1 +13.046,51.17725765,-2.27026501,450.60,48.63,115.80,232.83,233.05,-2.87,89.87,1189.21,100.00,0.5745,0.4000,0.0000,1.0000,1 +13.625,51.17697098,-2.26937545,449.10,47.03,103.72,231.56,230.37,-2.18,76.89,1189.16,100.00,0.4000,0.2216,-0.0352,1.0000,1 +14.250,51.17682419,-2.26838071,448.56,39.43,91.91,231.08,230.05,-4.70,66.60,1189.50,100.00,0.5098,-0.0273,-0.0645,1.0000,1 +14.828,51.17681128,-2.26737817,458.46,55.19,81.18,229.88,228.23,-7.04,67.38,1189.72,100.00,0.3451,0.1216,0.0000,1.0000,1 +15.437,51.17691558,-2.26635127,475.78,81.30,77.21,230.01,227.97,-5.92,67.93,1189.24,100.00,0.2745,0.0804,0.0000,1.0000,1 +16.015,51.17705770,-2.26542696,490.15,95.66,71.30,229.96,228.27,-6.06,68.90,1189.21,100.00,0.1726,0.0000,-0.0918,1.0000,1 +16.531,51.17723743,-2.26458908,502.28,83.07,67.08,230.15,228.46,-5.61,72.72,1187.77,100.00,0.2628,-0.2148,-0.1055,1.0000,1 +17.078,51.17747052,-2.26373020,513.05,74.44,62.91,230.42,228.97,-4.33,85.53,1187.75,100.00,0.2020,-0.2441,-0.1172,1.0000,1 +17.609,51.17771679,-2.26296674,517.73,64.60,53.41,230.50,229.61,-2.58,90.74,1186.80,100.00,0.5373,0.0000,-0.0234,1.0000,1 +18.343,51.17820920,-2.26204987,512.74,31.64,24.76,224.08,224.49,-1.09,78.87,1186.74,100.00,0.3980,0.5902,0.0000,1.0000,1 +19.015,51.17879181,-2.26162067,510.04,13.54,12.25,222.95,222.62,-8.74,33.01,1187.08,100.00,0.3138,0.6078,0.0000,1.0000,1 +19.562,51.17937511,-2.26140097,535.44,37.98,11.17,223.08,219.65,-9.13,-15.96,1187.69,100.00,0.0569,0.5686,0.0000,1.0000,1 +20.125,51.17993854,-2.26121533,564.46,71.06,13.10,222.69,219.84,-10.18,-63.25,1187.85,100.00,0.3392,0.1863,-0.1836,1.0000,1 +20.703,51.18053511,-2.26097437,593.06,99.00,16.97,222.58,220.19,-8.79,-86.36,1185.45,100.00,0.2510,0.2843,-0.1602,1.0000,1 +21.203,51.18101512,-2.26073739,611.31,113.40,19.99,222.64,221.19,-6.50,-110.71,1185.50,100.00,0.1628,0.4392,-0.1543,1.0000,1 +21.734,51.18151638,-2.26045644,623.47,117.58,20.86,223.34,222.37,-4.07,-148.05,1183.25,100.00,0.0961,0.4726,-0.1484,1.0000,1 +22.343,51.18211074,-2.26013072,630.42,110.77,20.82,224.51,223.66,-0.77,165.12,1182.75,100.00,0.0000,0.5196,-0.0762,1.0000,1 +22.921,51.18270230,-2.25979628,629.37,96.47,19.72,226.11,225.09,2.22,107.78,1182.15,100.00,0.1059,0.4667,-0.1113,1.0000,1 +23.515,51.18328762,-2.25948338,616.01,76.27,7.75,226.97,225.72,4.60,96.05,1182.12,100.00,0.5647,-0.0312,-0.1797,1.0000,1 +24.031,51.18381009,-2.25936628,591.87,50.79,347.48,225.06,223.50,6.29,87.99,1182.14,100.00,0.8137,0.2745,0.0000,1.0000,1 +24.703,51.18441326,-2.25963869,555.26,19.86,312.74,214.10,215.31,0.15,67.24,1184.37,100.00,0.4706,0.4667,-0.0293,1.0000,1 +25.296,51.18483666,-2.26029999,540.38,18.28,303.71,215.98,215.33,-1.57,62.11,1181.29,99.71,0.4020,-0.1348,-0.2031,1.0000,1 +25.843,51.18516676,-2.26108167,536.89,26.09,297.44,217.14,216.38,-1.88,67.00,1178.93,99.37,0.1079,-0.3145,-0.2090,1.0000,1 +26.515,51.18546544,-2.26199796,532.98,33.99,292.20,219.05,218.12,-0.21,89.36,1174.05,99.01,0.3745,-0.2637,-0.1328,1.0000,1 +27.234,51.18571093,-2.26302659,517.98,39.20,274.63,219.34,218.46,2.36,86.10,1168.40,98.62,0.5784,0.2981,0.0000,1.0000,1 +27.890,51.18577136,-2.26415190,494.02,32.95,266.33,220.77,218.88,3.40,48.87,1163.27,98.21,0.1157,0.5451,0.0000,1.0000,1 +28.593,51.18572122,-2.26526431,472.42,23.60,264.36,223.70,221.57,3.92,-10.76,1158.88,97.82,0.0941,0.4471,0.0000,1.0000,1 +29.203,51.18566887,-2.26622518,454.06,18.45,266.62,225.75,223.73,1.45,-38.56,1154.02,97.48,0.4000,-0.0332,-0.1836,1.0000,1 +29.906,51.18564418,-2.26742391,443.69,23.46,271.70,225.34,224.04,-2.70,-23.83,555.75,97.53,0.0726,-0.1738,-0.1211,1.0000,1 +30.515,51.18566651,-2.26842264,448.67,38.04,272.33,223.23,221.79,-1.84,-20.69,559.41,97.73,0.0000,-0.1699,0.0000,1.0000,1 +31.187,51.18570225,-2.26951945,452.78,56.19,273.57,220.83,219.49,-1.89,11.59,560.76,97.96,-0.0508,-0.4102,-0.0547,1.0000,1 +31.796,51.18573657,-2.27051116,456.40,75.90,273.04,218.62,217.46,-2.27,51.91,561.63,98.16,0.0000,-0.5742,-0.0742,1.0000,1 +32.453,51.18575947,-2.27159642,455.14,78.80,266.63,217.49,216.63,-0.95,95.44,1148.26,98.11,0.5373,0.2686,0.0000,1.0000,1 +33.031,51.18571844,-2.27248685,444.43,63.91,246.04,215.02,215.03,0.20,83.31,1163.05,97.80,0.7353,0.0883,-0.0840,1.0000,1 +33.656,51.18545440,-2.27337938,431.21,50.79,217.78,208.55,209.59,-2.71,78.53,1157.02,97.45,0.7255,0.2177,-0.0605,1.0000,1 +34.265,51.18502846,-2.27390884,427.74,45.43,191.10,202.16,204.33,-6.47,79.51,1152.10,97.11,0.6843,0.0000,-0.1621,1.0000,1 +34.828,51.18448850,-2.27409018,433.71,45.99,173.87,199.71,199.93,-6.05,81.96,1148.08,96.76,0.5117,-0.0410,-0.1758,1.0000,1 +35.484,51.18389260,-2.27397818,441.05,53.21,162.88,199.41,199.01,-3.91,83.77,569.81,96.65,0.2589,0.1373,-0.1738,1.0000,1 +36.031,51.18341083,-2.27374878,442.23,54.16,160.61,198.46,197.90,-1.47,73.54,552.40,96.82,0.0510,0.3177,-0.0195,1.0000,1 +36.703,51.18285261,-2.27342143,436.08,47.88,158.33,198.10,197.32,0.85,15.50,553.93,97.03,0.0000,0.6392,0.0000,1.0000,1 +37.359,51.18228311,-2.27306621,428.04,37.54,158.54,197.77,196.99,-0.04,-46.26,554.95,97.24,0.2882,0.4039,0.0000,1.0000,1 +37.953,51.18177057,-2.27276973,420.64,28.37,165.52,197.05,196.53,-1.89,-55.00,556.05,97.44,0.5412,-0.3008,-0.0781,1.0000,1 +38.593,51.18122201,-2.27256360,420.57,28.74,172.16,195.40,194.83,-4.12,-30.42,557.57,97.64,0.1530,-0.6035,-0.1543,1.0000,1 +39.281,51.18062050,-2.27242781,430.04,38.43,172.72,194.19,193.38,-3.49,24.99,559.65,97.87,0.0157,-0.3867,-0.1719,1.0000,2 +39.843,51.18009342,-2.27231192,436.65,44.82,171.59,193.19,192.60,-3.30,54.06,560.98,98.06,0.1863,-0.3418,-0.1797,1.0000,2 +40.484,51.17953287,-2.27215074,439.96,48.07,166.28,192.09,191.78,-3.94,59.33,562.73,98.27,0.2451,0.0608,-0.0957,1.0000,2 +41.015,51.17907228,-2.27196702,442.01,49.33,162.76,191.48,191.13,-3.46,60.41,564.09,98.45,0.4177,0.0000,-0.1758,1.0000,2 +41.609,51.17856331,-2.27169477,444.00,43.64,155.76,190.32,190.11,-4.13,65.91,565.56,98.64,0.2863,-0.3770,-0.1992,1.0000,2 +42.140,51.17814265,-2.27138891,445.97,43.99,149.06,190.31,190.14,-3.39,81.15,1106.32,98.65,0.5627,-0.0273,-0.1309,1.0000,2 +42.625,51.17776801,-2.27102581,444.99,42.91,137.09,190.34,190.66,-2.91,83.75,1173.70,98.37,0.5529,0.0628,-0.0840,1.0000,2 +43.265,51.17738269,-2.27043054,440.22,37.94,119.11,189.97,190.74,-2.77,79.44,1167.66,98.02,0.5980,0.2333,-0.0449,1.0000,2 +43.828,51.17711793,-2.26972845,435.96,33.88,103.24,189.39,190.22,-3.86,77.45,1162.35,97.67,0.4647,0.2628,-0.0977,1.0000,2 +44.453,51.17698288,-2.26889123,433.70,31.60,97.48,191.83,191.49,-2.26,65.61,1156.95,97.31,0.3235,0.3059,-0.0664,1.0000,2 +45.000,51.17692307,-2.26811251,431.30,28.38,92.29,194.03,193.62,-2.86,55.09,1152.43,97.00,0.4078,0.1177,-0.1094,1.0000,2 +45.640,51.17691379,-2.26721284,433.43,33.51,85.31,195.65,195.19,-5.11,54.78,1064.30,96.64,0.3275,0.0000,-0.0938,1.0000,2 +46.156,51.17695722,-2.26646981,440.03,45.35,80.95,195.81,195.20,-5.69,55.72,554.81,96.74,0.3431,0.0000,-0.0703,1.0000,2 +46.718,51.17704670,-2.26566130,449.75,55.08,75.36,193.77,193.15,-6.88,56.89,550.72,96.92,0.3706,0.0000,-0.0742,1.0000,2 +47.343,51.17720074,-2.26480197,464.24,61.45,69.34,191.64,190.80,-7.74,58.32,552.86,97.12,0.2961,-0.0020,-0.0664,1.0000,2 +47.937,51.17738876,-2.26403837,479.50,47.85,64.78,190.18,189.35,-7.62,63.34,759.47,97.30,0.2922,-0.2324,-0.1172,1.0000,2 +48.531,51.17761828,-2.26330624,493.67,46.02,58.75,190.69,190.16,-7.37,72.99,1151.84,97.01,0.3961,-0.1758,-0.0527,1.0000,2 +49.218,51.17792641,-2.26253099,507.49,45.23,46.56,191.12,191.01,-7.73,77.14,1146.07,96.65,0.5078,-0.1289,-0.0273,1.0000,2 +49.843,51.17833052,-2.26187949,520.85,34.17,29.53,190.29,190.85,-7.60,84.46,1139.07,96.28,0.6137,-0.0469,-0.0293,1.0000,2 +50.453,51.17879996,-2.26146105,530.75,30.62,15.11,189.61,190.02,-5.61,86.43,1133.54,95.91,0.3431,0.2059,-0.0547,1.0000,2 +51.093,51.17931166,-2.26122153,535.03,31.26,13.16,192.05,191.77,-1.92,30.32,992.79,95.58,0.0726,0.7137,0.0000,1.0000,2 +51.671,51.17982456,-2.26103332,536.46,34.77,13.30,192.35,192.25,-4.46,-28.51,544.49,95.71,0.4255,0.3431,-0.0703,1.0000,2 +52.265,51.18034030,-2.26081679,549.90,49.25,20.78,189.19,188.12,-11.76,-39.12,539.70,95.91,0.2392,0.4471,0.0000,1.0000,2 +52.812,51.18079705,-2.26054525,576.05,72.57,21.69,186.94,184.94,-10.66,-71.07,541.15,96.09,0.0530,0.4745,0.0000,1.0000,2 +53.312,51.18119912,-2.26028090,596.20,86.92,23.13,185.24,184.37,-9.09,-106.93,543.56,96.26,0.1451,0.5000,-0.0605,1.0000,2 +53.875,51.18162018,-2.26000585,610.57,92.74,24.27,184.10,184.01,-6.07,-146.74,543.92,96.44,0.1451,0.5137,-0.0547,1.0000,2 +54.437,51.18207148,-2.25971309,620.14,88.72,24.46,184.43,184.41,-2.49,160.40,1077.69,96.46,0.1706,0.5922,-0.0098,1.0000,2 +55.046,51.18255840,-2.25939290,622.93,80.96,18.01,186.66,186.71,3.74,113.20,1134.09,96.12,0.5961,0.4392,-0.0645,1.0000,2 +55.593,51.18301844,-2.25917353,607.03,60.65,355.09,184.95,185.37,5.69,87.30,1128.80,95.82,0.6686,0.3686,-0.0137,1.0000,2 +56.140,51.18346153,-2.25920290,582.49,38.48,339.71,184.98,184.55,2.40,69.78,1124.90,95.53,0.6392,0.2530,-0.0488,1.0000,2 +56.765,51.18396040,-2.25950391,562.20,24.38,324.11,185.27,185.88,-1.64,66.03,1119.86,95.18,0.4255,0.1510,-0.1035,1.0000,2 +57.343,51.18437539,-2.25997384,553.60,24.82,317.58,187.82,187.88,-1.20,66.37,1117.59,94.86,0.2667,0.0000,-0.1602,1.0000,2 +57.968,51.18477488,-2.26056241,544.69,26.03,313.21,190.68,190.33,0.23,67.23,675.98,94.60,0.4549,0.0079,-0.1289,1.0000,2 +58.609,51.18513247,-2.26121455,533.93,24.94,302.03,189.43,189.72,-2.04,68.00,534.66,94.77,0.4784,-0.1348,-0.1523,1.0000,2 +59.296,51.18544491,-2.26203610,527.42,29.07,291.62,188.04,188.29,-2.46,73.57,535.51,95.01,0.3902,-0.2715,-0.1641,1.0000,2 +59.906,51.18563984,-2.26284123,522.26,39.58,278.90,186.45,187.07,-2.05,83.81,536.27,95.22,0.4667,-0.1875,-0.1484,1.0000,2 +60.515,51.18572835,-2.26367615,513.04,45.32,268.91,185.44,185.48,0.53,86.65,536.59,95.42,0.3098,0.0118,-0.1504,1.0000,2 +61.171,51.18572593,-2.26454769,495.86,38.57,266.47,186.25,185.13,4.00,48.55,538.69,95.62,0.0922,0.6196,0.0000,1.0000,2 +62.000,51.18567479,-2.26561965,467.79,23.70,265.06,187.63,186.22,2.99,-13.07,541.13,95.88,0.3922,0.2569,-0.0664,1.0000,2 +62.828,51.18562557,-2.26676919,454.61,27.38,268.90,190.03,189.66,-2.96,-28.49,1132.46,95.60,0.2020,0.3039,0.0000,1.0000,2 +63.671,51.18561906,-2.26795516,457.12,42.43,269.85,192.98,192.46,-1.13,-47.25,913.82,95.13,0.0000,-0.1191,-0.0918,1.0000,2 +64.531,51.18563678,-2.26917518,449.76,48.93,272.14,192.97,192.31,1.09,-27.12,538.55,95.36,0.0000,-0.1973,-0.0176,1.0000,2 +65.359,51.18567801,-2.27038325,436.89,54.49,273.76,193.11,192.26,2.12,19.82,540.06,95.64,0.1157,-0.6113,-0.1777,1.0000,2 +66.062,51.18571416,-2.27137393,422.38,45.95,271.64,193.22,192.16,1.66,62.18,542.47,95.87,0.3549,-0.2637,-0.1621,1.0000,2 +66.812,51.18569989,-2.27246153,405.45,25.28,248.29,191.93,193.22,-4.69,73.50,1133.85,95.56,0.7255,-0.2148,-0.1328,1.0000,2 +67.531,51.18545117,-2.27337448,406.65,26.81,218.19,185.99,187.83,-7.36,83.24,1128.15,95.12,0.7784,0.0000,-0.0684,1.0000,2 +68.218,51.18501097,-2.27391235,413.75,31.80,186.78,179.15,181.81,-7.00,87.99,1122.03,94.72,0.7333,0.0961,-0.0762,1.0000,2 +68.953,51.18437509,-2.27406618,417.34,29.34,175.81,180.92,181.07,-4.16,75.33,863.49,94.30,0.5922,0.0687,-0.0723,1.0000,2 +69.625,51.18381540,-2.27397520,418.59,30.66,163.25,178.55,178.85,-4.70,68.01,534.96,94.47,0.1432,0.3490,-0.0703,1.0000,2 +70.171,51.18337096,-2.27376801,419.96,31.95,162.07,178.96,178.75,-1.96,42.05,535.26,94.66,0.0177,0.3706,0.0000,1.0000,2 +70.765,51.18291388,-2.27351048,418.56,30.49,160.44,179.21,178.88,-0.53,22.48,533.91,94.85,0.0000,0.3804,0.0000,1.0000,2 +71.343,51.18246118,-2.27324700,415.44,26.34,159.30,179.33,178.92,0.18,-1.84,535.70,95.04,0.1490,0.3275,0.0000,1.0000,2 +71.984,51.18196682,-2.27295359,410.82,18.80,159.64,179.52,179.15,-0.80,-15.49,537.73,95.25,0.2216,0.2745,0.0000,1.0000,2 +72.546,51.18152095,-2.27269649,409.39,17.44,161.11,179.43,179.14,-2.19,-23.24,539.95,95.43,0.0118,0.1118,0.0000,1.0000,2 +73.375,51.18086784,-2.27235897,410.88,18.91,162.83,179.34,179.02,-1.95,-29.24,541.77,95.69,0.1706,0.2530,0.0000,1.0000,3 +73.921,51.18041932,-2.27215478,411.10,19.09,164.67,179.30,179.03,-2.21,-36.92,543.46,95.87,0.2765,0.1883,0.0000,1.0000,3 +74.484,51.17998929,-2.27198244,411.27,19.28,166.64,179.24,178.96,-1.85,-34.00,544.46,96.04,0.1490,-0.3965,-0.0898,1.0000,3 +75.062,51.17951673,-2.27181936,411.28,19.32,168.54,179.23,178.94,-2.90,-1.26,546.01,96.22,0.3569,-0.3281,-0.1660,1.0000,3 +75.671,51.17902775,-2.27165861,417.35,24.47,167.55,178.53,178.06,-5.88,38.94,547.69,96.42,0.2490,-0.5605,-0.1855,1.0000,3 +76.234,51.17857372,-2.27148426,427.10,27.00,163.27,177.69,177.42,-6.13,79.41,549.11,96.61,0.3216,-0.4707,-0.1953,1.0000,3 +76.796,51.17810384,-2.27124730,432.43,30.47,151.72,178.40,178.89,-3.90,91.33,1141.87,96.41,0.5882,0.1157,-0.0781,1.0000,3 +77.296,51.17773985,-2.27094503,430.12,27.84,138.38,178.58,179.33,-1.80,83.86,1142.37,96.10,0.5882,0.5412,0.0000,1.0000,3 +77.875,51.17740214,-2.27049277,423.58,21.20,124.46,178.97,179.73,-3.03,72.65,1137.14,95.79,0.4843,0.2686,-0.0684,1.0000,3 +78.484,51.17709927,-2.26980488,419.90,17.94,108.46,179.48,180.59,-6.65,70.00,1131.91,95.42,0.7118,0.0216,-0.1270,1.0000,3 +79.125,51.17693255,-2.26898499,427.81,26.12,97.94,179.80,179.50,-6.94,64.78,1126.41,95.05,0.2294,0.0961,-0.1602,1.0000,3 +79.734,51.17686754,-2.26817512,438.05,32.33,92.96,182.06,181.70,-5.92,64.70,1121.18,94.69,0.3412,-0.0098,-0.1816,1.0000,3 +80.375,51.17684844,-2.26733570,446.65,44.41,86.67,183.96,183.68,-5.58,66.08,1115.39,94.35,0.3490,-0.0254,-0.1758,1.0000,3 +80.937,51.17688023,-2.26656591,453.50,58.72,80.71,183.73,183.65,-5.42,67.33,538.99,94.44,0.3529,0.0000,-0.1719,1.0000,3 +81.531,51.17696490,-2.26578386,459.73,64.95,73.91,182.57,182.55,-5.45,65.50,531.09,94.63,0.3412,0.2863,0.0000,1.0000,3 +82.140,51.17711398,-2.26499318,466.30,65.87,68.18,181.27,181.17,-5.76,54.93,532.66,94.82,0.3235,0.2804,0.0000,1.0000,3 +82.781,51.17731657,-2.26421951,475.89,46.65,63.19,180.13,179.80,-6.88,44.35,534.62,95.03,0.2981,0.1588,-0.0254,1.0000,3 +83.328,51.17753772,-2.26355956,488.24,45.16,59.65,181.27,180.64,-7.22,47.92,1117.92,94.78,0.3020,-0.1465,-0.1934,1.0000,3 +83.953,51.17780203,-2.26287450,503.66,50.29,53.03,182.43,181.77,-8.98,62.88,1116.19,94.44,0.5098,-0.2988,-0.1836,1.0000,3 +84.531,51.17810988,-2.26225701,521.84,49.51,40.18,181.97,181.57,-10.31,82.22,726.57,94.18,0.5451,-0.3672,-0.1836,1.0000,3 +85.140,51.17850244,-2.26173636,539.79,49.78,23.43,177.91,178.62,-8.03,90.88,531.36,94.35,0.5686,-0.0801,-0.1738,1.0000,3 +85.734,51.17895042,-2.26140725,548.77,47.53,12.82,175.98,176.68,-4.25,92.75,531.57,94.55,0.1981,0.0961,-0.1699,1.0000,3 +86.312,51.17939569,-2.26122521,547.84,45.20,11.75,176.33,176.49,-0.66,58.38,529.94,94.74,-0.1621,0.6961,-0.0703,1.0000,3 +86.968,51.17991712,-2.26106458,540.43,40.86,9.94,177.10,176.96,0.46,-25.37,531.59,94.95,0.4216,0.6000,-0.0078,1.0000,3 +87.640,51.18046291,-2.26087587,539.19,41.53,23.03,176.85,177.86,-11.15,-44.13,1116.32,94.66,0.5392,0.0000,-0.2422,1.0000,3 +88.265,51.18093780,-2.26056421,562.77,60.13,23.60,177.90,176.45,-10.02,-57.63,1111.62,94.30,0.0000,0.4000,-0.1855,1.0000,3 +88.796,51.18134076,-2.26026422,580.99,70.49,24.36,179.21,178.73,-8.45,-82.58,1106.38,93.99,0.0000,0.4608,-0.1875,1.0000,3 +89.328,51.18174218,-2.25996687,591.99,70.83,25.29,180.77,180.99,-6.59,-117.89,1100.75,93.68,0.0000,0.5274,-0.1816,1.0000,3 +89.937,51.18220275,-2.25964169,598.79,65.82,26.76,183.00,183.27,-3.37,171.49,1095.31,93.35,0.0000,0.8431,-0.0703,1.0000,3 +90.562,51.18269491,-2.25929248,603.28,59.35,14.60,184.96,185.56,2.72,96.04,1089.47,92.99,0.7686,0.1863,-0.1289,1.0000,3 +91.093,51.18313102,-2.25911448,593.74,47.11,354.17,181.62,182.55,2.88,92.10,1085.07,92.69,0.3882,0.0000,-0.1934,1.0000,3 +91.656,51.18358365,-2.25915657,574.16,29.65,341.91,184.13,183.44,3.20,75.30,1081.05,92.38,0.7255,0.3843,0.0000,1.0000,3 +92.250,51.18405295,-2.25940203,553.92,14.58,322.95,182.19,183.23,-1.55,70.80,1076.59,92.07,0.4941,0.0510,-0.1562,1.0000,3 +92.921,51.18451344,-2.25995505,543.72,14.71,313.08,184.49,184.88,-3.43,52.91,1072.87,91.68,0.3529,-0.1309,-0.2109,1.0000,3 +93.593,51.18491127,-2.26064261,543.87,27.11,307.22,186.98,186.96,-2.78,63.91,1067.84,91.29,0.2765,-0.0996,-0.2188,1.0000,3 +94.140,51.18520185,-2.26126422,543.43,35.00,302.31,188.87,188.82,-1.80,82.79,1063.26,90.97,0.3569,-0.3574,-0.1816,1.0000,3 +94.828,51.18550401,-2.26206569,535.49,36.70,290.13,190.68,190.64,1.07,92.58,1057.87,90.61,0.5784,0.1432,0.0000,1.0000,3 +95.500,51.18570471,-2.26295094,514.47,33.53,272.37,191.26,190.70,3.25,79.81,1052.53,90.21,0.5863,0.4137,0.0000,1.0000,3 +96.234,51.18573739,-2.26393783,488.36,24.68,266.65,194.33,192.90,4.36,24.39,1046.88,89.81,0.0216,0.5667,-0.0645,1.0000,3 +96.890,51.18569512,-2.26491522,468.03,15.30,265.27,197.54,196.44,2.11,-1.38,1042.43,89.41,0.3490,0.3255,-0.0078,1.0000,3 +97.609,51.18564577,-2.26595955,457.84,18.92,266.45,199.96,199.24,-0.79,-35.62,1037.04,88.98,0.2765,0.3412,0.0000,1.0000,3 +98.296,51.18562176,-2.26698499,455.31,30.33,271.89,201.56,200.97,-2.93,-52.76,1031.11,88.57,0.3157,0.1902,-0.1250,1.0000,3 +98.953,51.18565298,-2.26797845,457.42,42.39,275.07,202.92,202.18,-2.00,-55.38,1025.44,88.17,-0.1875,-0.3320,-0.0293,1.0000,3 +99.578,51.18570844,-2.26888451,453.28,47.90,273.28,204.62,203.51,4.69,-11.26,1020.06,87.81,-0.2148,-0.7383,-0.1387,1.0000,3 +100.203,51.18575508,-2.26982042,433.30,41.23,274.73,207.20,205.07,5.15,20.73,1014.59,87.44,0.1490,0.0000,-0.0996,1.0000,3 +100.906,51.18579937,-2.27088377,407.98,30.83,271.07,209.55,208.02,0.08,31.70,1009.38,87.03,0.4627,-0.1328,-0.1680,1.0000,3 +101.593,51.18578913,-2.27195030,404.62,26.20,259.35,208.86,208.34,-7.77,64.06,1004.37,86.62,0.5471,-0.4902,-0.1680,1.0000,3 +102.125,51.18568364,-2.27277529,417.81,38.36,239.52,205.24,205.45,-9.20,87.15,999.98,86.29,0.7118,-0.0293,-0.0469,1.0000,3 +102.750,51.18538742,-2.27353831,432.63,52.97,208.48,196.19,198.33,-6.32,92.45,994.57,85.95,0.7353,0.2039,0.0000,1.0000,3 +103.359,51.18492480,-2.27398400,437.29,53.37,189.48,193.38,194.30,-4.20,86.61,990.38,85.61,0.7588,0.0667,-0.0566,1.0000,3 +103.921,51.18441647,-2.27412974,436.45,48.34,170.89,190.16,191.14,-2.96,86.24,984.85,85.26,0.5137,0.2784,-0.0781,1.0000,3 +104.468,51.18394042,-2.27402960,431.72,43.53,161.66,190.74,190.65,-1.48,76.20,980.72,84.95,0.4078,0.3353,0.0000,1.0000,3 +105.062,51.18345578,-2.27377457,424.95,36.72,157.52,192.40,191.84,-0.04,45.92,976.16,84.62,-0.0605,0.5647,0.0000,1.0000,3 +105.671,51.18295550,-2.27342981,417.40,29.07,155.86,194.59,193.78,0.19,-14.60,971.39,84.25,0.2314,0.5882,0.0000,1.0000,3 +106.250,51.18247802,-2.27310222,412.78,23.67,158.92,195.92,195.36,-1.99,-54.26,966.55,83.92,0.3216,0.2765,0.0000,1.0000,3 +106.828,51.18197901,-2.27282016,411.21,19.39,164.93,196.91,196.37,-2.99,-46.04,961.93,83.58,0.2412,-0.5059,-0.1465,1.0000,3 +107.468,51.18141073,-2.27258436,414.57,22.74,166.30,198.15,197.32,-1.90,-3.13,956.62,83.21,0.0902,-0.5020,-0.1660,1.0000,3 +108.062,51.18088034,-2.27238008,417.52,25.60,166.39,199.25,198.41,-1.69,3.79,951.69,82.88,0.2490,0.1157,0.0000,1.0000,4 +108.656,51.18036853,-2.27217988,420.70,28.92,166.23,200.13,199.26,-2.25,2.11,946.66,82.55,0.1294,0.2451,0.0000,1.0000,4 +109.234,51.17983105,-2.27197135,425.37,33.53,166.11,200.93,200.03,-3.00,7.66,941.68,82.21,0.1196,-0.2949,-0.1641,1.0000,4 +109.890,51.17924837,-2.27173790,431.97,40.30,165.48,201.67,200.77,-2.51,32.59,935.97,81.83,0.1471,-0.3008,-0.1836,1.0000,4 +110.453,51.17874112,-2.27151556,435.40,37.27,163.33,202.31,201.52,-2.88,48.31,931.27,81.51,0.3000,-0.4043,-0.2031,1.0000,4 +111.031,51.17820435,-2.27123851,438.84,36.98,156.14,202.44,201.91,-4.54,71.55,925.79,81.16,0.5020,-0.3340,-0.1797,1.0000,4 +111.640,51.17770967,-2.27085878,444.09,42.29,139.67,200.23,200.56,-5.94,80.13,920.67,80.81,0.5569,-0.1035,-0.1426,1.0000,4 +112.281,51.17728381,-2.27027522,451.05,49.39,121.43,197.64,198.19,-6.07,84.14,915.76,80.47,0.6412,-0.2266,-0.0664,1.0000,4 +112.859,51.17699703,-2.26953289,456.71,54.81,104.23,194.15,194.78,-4.49,86.59,910.68,80.12,0.2392,0.2196,-0.0156,1.0000,4 +113.421,51.17685074,-2.26874861,456.95,53.91,98.85,195.22,194.95,-2.10,82.06,905.96,79.77,0.6314,0.1588,0.0000,1.0000,4 +114.031,51.17677428,-2.26787655,452.25,36.07,85.92,194.68,194.79,-2.70,70.52,900.77,79.42,0.4255,0.3902,0.0000,1.0000,4 +114.687,51.17682033,-2.26694962,451.48,55.70,76.98,194.59,194.35,-4.77,53.75,895.73,79.05,0.4000,0.2079,0.0000,1.0000,4 +115.265,51.17693926,-2.26615506,457.48,62.78,74.44,195.09,194.47,-3.78,47.86,890.93,78.71,0.1059,0.2765,0.0000,1.0000,4 +115.875,51.17709376,-2.26533144,462.81,67.07,71.74,195.72,195.16,-3.83,35.29,885.82,78.37,0.2608,0.2157,0.0000,1.0000,4 +116.484,51.17727562,-2.26449603,471.07,49.73,68.25,195.75,195.04,-6.48,35.05,880.48,77.99,0.2902,-0.2227,-0.0078,1.0000,4 +117.031,51.17747509,-2.26373867,485.56,47.53,64.60,195.44,194.38,-8.08,57.64,875.18,77.66,0.3314,-0.4219,-0.0391,1.0000,4 +117.625,51.17772265,-2.26297111,503.33,52.82,53.32,193.96,193.48,-9.27,83.05,870.11,77.32,0.4863,-0.3555,-0.0371,1.0000,4 +118.281,51.17805105,-2.26226554,519.22,51.78,37.54,191.76,192.14,-6.73,86.93,864.33,76.96,0.5627,0.3686,0.0000,1.0000,4 +118.890,51.17848350,-2.26173434,527.72,36.13,25.80,190.50,190.82,-5.36,82.59,858.38,76.59,0.5373,0.0000,-0.0371,1.0000,4 +119.531,51.17899619,-2.26134613,532.41,28.71,19.46,190.41,190.32,-3.01,58.82,853.02,76.21,0.1137,0.4843,0.0000,1.0000,4 +120.171,51.17954106,-2.26105604,533.37,27.06,14.54,191.17,191.14,-4.30,53.50,847.26,75.83,0.5882,-0.4785,-0.0430,1.0000,4 +120.781,51.18002770,-2.26087245,538.54,34.61,10.27,191.14,190.87,-4.11,33.82,842.60,75.52,-0.0586,0.6353,0.0000,1.0000,4 +121.453,51.18063575,-2.26070994,547.84,45.92,9.07,191.56,191.09,-5.42,-29.51,836.54,75.10,0.2706,0.5961,0.0000,1.0000,4 +122.031,51.18114526,-2.26056029,561.72,59.60,16.65,190.58,189.96,-10.01,-55.19,831.19,74.76,0.4059,0.4627,0.0000,1.0000,4 +122.703,51.18168803,-2.26029764,585.63,77.31,21.58,189.55,188.64,-8.39,-105.50,825.69,74.39,0.2765,0.5627,0.0000,1.0000,4 +123.421,51.18227270,-2.25993075,600.37,74.21,24.83,189.66,189.66,-1.69,-178.37,818.42,73.96,0.1549,0.6667,0.0000,1.0000,4 +124.125,51.18285210,-2.25954883,600.36,61.69,19.98,190.79,190.64,3.38,99.17,811.55,73.53,0.7118,0.2608,-0.0293,1.0000,4 +124.828,51.18343128,-2.25928923,582.19,39.26,347.34,184.41,186.10,3.90,88.67,806.03,73.14,0.7059,0.2902,0.0000,1.0000,4 +125.625,51.18406817,-2.25951323,550.63,12.58,328.68,183.64,183.74,-1.45,60.35,799.67,72.69,0.6667,-0.1758,-0.0547,1.0000,4 +126.359,51.18459017,-2.26006904,546.41,20.07,310.34,180.08,180.93,-7.37,65.30,794.79,72.24,0.1412,-0.1523,-0.1914,1.0000,4 +127.015,51.18495927,-2.26074681,555.12,40.05,307.95,181.23,181.26,-4.21,77.10,788.46,71.81,0.3902,-0.3965,-0.0918,1.0000,4 +127.750,51.18531805,-2.26153079,555.40,50.20,294.27,181.12,181.84,-1.66,92.74,782.49,71.40,0.5059,-0.0215,0.0000,1.0000,4 +128.453,51.18556490,-2.26239575,541.44,49.25,278.19,180.89,181.01,2.77,92.56,776.32,70.99,0.5078,0.1118,0.0000,1.0000,4 +129.281,51.18566818,-2.26342765,509.36,37.21,271.71,183.67,181.73,5.65,49.21,769.78,70.50,0.2235,0.4883,0.0471,1.0000,4 +130.140,51.18567729,-2.26464258,474.11,18.24,268.45,186.92,185.79,2.73,4.64,763.46,69.97,0.2922,0.3706,0.0000,1.0000,4 +130.921,51.18565706,-2.26571611,461.05,18.95,268.25,188.38,187.92,-0.12,-8.73,756.59,69.47,0.0000,0.3824,0.0000,1.0000,4 +131.640,51.18564116,-2.26671624,455.75,27.50,268.29,189.44,188.93,0.46,-45.26,750.73,69.02,0.1255,0.4549,0.0000,1.0000,4 +132.328,51.18564183,-2.26771275,446.07,28.07,273.30,190.39,189.90,-0.12,-52.11,744.74,68.60,0.1334,-0.1973,0.0000,1.0000,4 +133.015,51.18568263,-2.26865868,435.68,27.67,275.55,191.62,190.82,1.08,-27.54,738.89,68.19,0.2843,-0.4707,0.0000,1.0000,4 +133.687,51.18574750,-2.26958896,426.01,30.76,276.93,192.60,191.88,1.08,15.79,733.05,67.78,0.2000,-0.4688,-0.0195,1.0000,4 +134.296,51.18580647,-2.27047287,419.55,38.70,274.52,193.04,192.55,-1.97,50.90,728.19,67.41,0.3020,-0.4844,-0.0176,1.0000,4 +134.875,51.18583507,-2.27130129,416.67,42.91,266.31,192.72,192.60,-3.11,80.67,722.80,67.05,0.4569,-0.3828,-0.0117,1.0000,4 +135.468,51.18579440,-2.27214022,411.43,31.35,252.80,191.54,191.66,-1.08,87.09,718.03,66.71,0.5137,0.2373,0.0000,1.0000,4 +136.031,51.18565063,-2.27289713,401.54,21.22,229.94,187.64,189.41,-3.28,78.22,713.53,66.39,0.8941,0.1157,0.0000,1.0000,4 +136.687,51.18528862,-2.27353883,399.49,18.78,193.69,173.48,177.65,-11.10,78.10,708.13,66.01,0.7392,0.0000,-0.1582,1.0000,4 +137.359,51.18477871,-2.27375475,412.26,26.98,184.75,171.82,171.56,-7.55,63.70,702.57,65.62,0.0687,0.4431,-0.0254,1.0000,4 +137.921,51.18431853,-2.27380586,422.17,34.46,179.27,172.16,172.10,-6.98,72.28,697.39,65.26,0.5117,-0.4199,-0.1914,1.0000,4 +138.546,51.18383071,-2.27377752,430.04,42.30,169.11,171.53,171.82,-5.87,78.34,691.50,64.91,0.2941,0.3059,-0.0176,1.0000,4 +139.125,51.18337525,-2.27364193,433.55,45.58,165.93,172.28,172.35,-3.28,67.46,686.05,64.56,0.0000,0.4333,-0.0801,1.0000,4 +139.765,51.18288454,-2.27343451,431.28,43.11,164.21,173.71,173.51,-0.36,40.45,680.68,64.20,0.1490,0.4196,0.0000,1.0000,4 +140.375,51.18241952,-2.27320845,425.78,36.39,161.58,174.96,174.76,-0.92,26.89,675.94,63.88,0.3471,0.2608,0.0000,1.0000,4 +140.953,51.18196461,-2.27295327,423.42,31.59,159.59,175.82,175.66,-2.54,16.54,670.69,63.52,0.2333,0.2294,-0.0117,1.0000,4 +141.578,51.18149199,-2.27266003,426.64,34.83,158.35,176.33,176.07,-4.41,8.73,665.64,63.16,0.2686,0.1530,-0.1582,1.0000,4 diff --git a/track_data/flight_20260901_181801_021145.csv b/track_data/flight_20260901_181801_021145.csv new file mode 100644 index 00000000..50209efa --- /dev/null +++ b/track_data/flight_20260901_181801_021145.csv @@ -0,0 +1,285 @@ +# aircraft_title=Extra 300S Paint2 +# recorded_at=2026-09-01T18:18:01 +# laps_s=29.380,33.399,35.389,33.278 +timestamp_s,latitude,longitude,altitude_ft,agl_ft,heading_deg,airspeed_kts,groundspeed_kts,pitch_deg,bank_deg,torque,health_pct,stick_pitch,stick_roll,stick_yaw,stick_throttle,lap +0.343,51.18977506,-2.27596286,983.09,619.54,166.16,183.35,182.13,6.90,0.13,1069.68,100.00,-0.0215,0.0000,0.0000,1.0000,1 +0.812,51.18937902,-2.27580774,961.94,594.21,166.06,186.93,185.37,7.89,0.15,1168.40,100.00,-0.1699,0.0000,0.0000,1.0000,1 +1.328,51.18895120,-2.27564021,936.29,565.12,166.01,190.73,188.64,8.86,0.16,1173.65,100.00,-0.1055,0.0883,0.0000,1.0000,1 +1.796,51.18855698,-2.27548452,910.61,535.91,165.99,194.34,191.76,9.66,0.17,1174.53,100.00,-0.1680,0.1216,0.0000,1.0000,1 +2.390,51.18810465,-2.27530604,878.25,499.97,165.94,198.51,195.04,10.93,0.04,1176.72,100.00,-0.1660,0.1471,0.0000,1.0000,1 +2.921,51.18757388,-2.27509628,836.06,454.68,165.94,203.32,198.92,11.61,-0.93,1176.78,100.00,-0.1328,0.1530,0.0000,1.0000,1 +3.375,51.18716115,-2.27493412,802.16,420.74,165.94,207.06,202.05,12.08,-1.75,1179.13,100.00,-0.1738,0.1471,0.0000,1.0000,1 +3.859,51.18671570,-2.27475471,762.71,382.02,165.91,210.96,205.10,13.20,-2.37,1178.82,100.00,-0.1758,0.1471,0.0000,1.0000,1 +4.359,51.18624271,-2.27457132,718.73,337.11,165.92,215.18,207.91,14.24,-2.99,1181.33,100.00,-0.1562,0.1393,0.0000,1.0000,1 +4.843,51.18578601,-2.27439207,672.43,290.62,165.98,219.05,210.75,14.98,-3.09,1181.12,100.00,-0.0957,0.0138,-0.0156,1.0000,1 +5.328,51.18531840,-2.27420892,623.54,241.48,166.12,223.20,213.89,15.12,-3.10,1183.95,100.00,0.1137,0.0138,0.0000,1.0000,1 +5.843,51.18483631,-2.27402030,571.99,183.78,166.44,226.87,217.88,13.36,-3.07,1184.24,100.00,0.2765,0.0138,-0.0391,1.0000,1 +6.343,51.18433236,-2.27382750,525.94,136.55,166.76,230.42,223.51,10.93,-3.06,1187.25,100.00,0.2137,0.0079,-0.0508,1.0000,1 +6.828,51.18383428,-2.27364053,487.60,98.48,167.14,232.94,227.87,7.65,-3.05,1187.75,100.00,0.2765,0.0000,-0.0293,1.0000,1 +7.312,51.18333770,-2.27345828,460.87,72.07,167.55,234.93,231.71,3.63,-3.07,1189.62,100.00,0.3177,0.0000,-0.0195,1.0000,1 +7.765,51.18285752,-2.27328762,447.35,59.04,167.86,235.91,233.93,0.29,-3.09,1190.42,100.00,0.2255,0.0000,-0.0117,1.0000,1 +8.265,51.18233242,-2.27310469,443.37,53.46,167.89,236.89,234.93,-0.52,-3.09,1190.42,100.00,0.0196,-0.0098,0.0000,1.0000,1 +8.750,51.18180444,-2.27292276,442.60,50.59,167.95,237.69,235.70,-0.58,-1.66,1190.99,100.00,0.0000,-0.2754,-0.0273,1.0000,1 +9.250,51.18128903,-2.27274760,442.24,50.24,168.10,238.38,236.39,-0.67,7.88,1190.62,100.00,0.0000,-0.2285,-0.0195,1.0000,1 +9.765,51.18073445,-2.27256116,442.09,50.09,167.84,239.11,237.08,-0.80,15.34,1190.34,100.00,0.0471,-0.2090,-0.0176,1.0000,1 +10.296,51.18017395,-2.27236471,442.03,50.03,167.22,239.78,237.76,-1.07,24.42,1190.03,100.00,0.2275,-0.2148,0.0000,1.0000,1 +10.781,51.17963954,-2.27215619,443.04,51.14,165.66,240.24,238.24,-2.42,40.23,1189.74,100.00,0.2137,-0.3516,0.0000,1.0000,1 +11.281,51.17910056,-2.27191540,446.81,54.87,162.03,240.39,238.46,-3.94,60.67,1189.49,100.00,0.3275,-0.3379,-0.0020,1.0000,1 +11.765,51.17860549,-2.27164951,452.21,53.97,155.90,240.16,238.32,-4.59,75.44,1189.20,100.00,0.3353,-0.2402,0.0000,1.0000,1 +12.218,51.17813425,-2.27130936,457.80,56.04,149.52,239.88,238.16,-3.76,84.87,1189.12,100.00,0.4137,-0.2168,0.0000,1.0000,1 +12.687,51.17770330,-2.27089945,460.53,58.56,137.78,238.74,237.60,-2.75,88.51,1188.43,100.00,0.5117,0.1079,0.0706,1.0000,1 +13.156,51.17731518,-2.27034527,459.33,57.21,122.73,236.27,235.73,-1.76,86.25,1188.46,100.00,0.5804,0.2765,0.0961,1.0000,1 +13.640,51.17702482,-2.26965625,455.36,53.30,108.37,233.86,233.02,-2.00,80.41,1188.49,100.00,0.5373,0.2726,0.0706,1.0000,1 +14.093,51.17687208,-2.26893874,452.85,50.72,97.89,233.05,232.04,-3.08,72.77,1188.69,100.00,0.5373,0.1647,0.0000,1.0000,1 +14.578,51.17679490,-2.26810156,454.75,44.69,87.80,232.01,230.85,-4.82,70.87,1189.17,100.00,0.3510,0.1902,0.0059,1.0000,1 +15.078,51.17681593,-2.26724021,461.95,59.41,82.20,232.06,230.36,-4.78,64.53,1189.34,100.00,0.3824,0.0667,0.0000,1.0000,1 +15.562,51.17688912,-2.26644626,470.35,75.49,79.07,232.34,230.55,-4.38,61.99,1188.94,100.00,0.1981,0.0000,0.0000,1.0000,1 +16.046,51.17699342,-2.26562869,478.33,83.63,74.09,232.48,230.82,-5.38,62.56,1188.88,100.00,0.3235,0.0000,0.0000,1.0000,1 +16.531,51.17714062,-2.26484312,488.73,86.85,68.82,232.31,230.54,-6.23,64.98,1188.18,100.00,0.2922,-0.2480,0.0000,1.0000,1 +17.015,51.17732941,-2.26407367,500.90,66.81,61.90,232.04,230.29,-6.74,78.09,1188.05,100.00,0.4824,-0.2812,0.0000,1.0000,1 +17.515,51.17757121,-2.26335642,513.24,69.13,54.87,231.38,229.78,-5.69,83.78,1187.23,100.00,0.2451,0.0000,0.0000,1.0000,1 +17.984,51.17785974,-2.26269767,522.08,66.55,45.89,230.96,229.87,-4.90,84.09,1187.00,100.00,0.4490,0.2863,0.1922,1.0000,1 +18.500,51.17822739,-2.26208460,528.74,50.95,34.45,229.79,228.96,-4.49,83.14,1186.34,100.00,0.5412,0.0530,0.0000,1.0000,1 +19.046,51.17872625,-2.26155955,534.51,37.17,21.69,228.57,227.82,-4.56,79.62,1186.11,100.00,0.4765,0.0000,0.0000,1.0000,1 +19.515,51.17918277,-2.26126575,539.53,35.60,15.84,228.42,227.28,-3.57,72.74,1185.92,100.00,0.0000,0.4157,0.0000,1.0000,1 +19.984,51.17966636,-2.26104642,542.70,38.39,14.53,229.38,228.09,-1.82,53.69,1185.89,100.00,0.0000,0.4667,0.0000,1.0000,1 +20.500,51.18017811,-2.26084285,543.35,40.63,12.84,230.39,229.04,-0.83,8.67,1185.75,100.00,0.0000,0.5922,0.0275,1.0000,1 +20.968,51.18067622,-2.26066034,543.43,39.81,12.60,231.27,229.96,-1.67,-42.16,1185.55,100.00,0.2882,0.4961,0.0059,1.0000,1 +21.468,51.18118147,-2.26045328,543.85,37.99,19.57,231.62,230.56,-3.37,-70.25,1185.50,100.00,0.4137,0.1000,0.0216,1.0000,1 +21.921,51.18164392,-2.26019256,546.43,31.44,23.59,231.99,230.68,-2.58,-50.48,1185.48,100.00,0.1765,-0.6582,-0.0840,1.0000,1 +22.406,51.18211502,-2.25986132,550.11,22.28,25.07,232.73,231.26,-1.95,15.72,1185.46,100.00,0.1804,-0.6367,-0.0859,1.0000,1 +22.859,51.18255810,-2.25955335,554.31,16.34,21.88,232.86,231.63,-6.08,48.27,1185.09,100.00,0.4255,-0.3125,-0.0391,1.0000,1 +23.312,51.18302811,-2.25927533,566.73,23.94,13.76,231.74,229.96,-9.35,69.58,1185.08,100.00,0.3647,-0.5098,-0.0664,1.0000,1 +23.765,51.18348984,-2.25910262,584.72,40.67,2.26,230.38,228.88,-8.96,88.64,1184.35,100.00,0.6510,-0.3145,0.0000,1.0000,1 +24.265,51.18401082,-2.25908094,600.75,57.71,338.17,224.01,225.32,-5.42,97.55,1184.50,100.00,0.7353,0.0863,0.0844,1.0000,1 +24.765,51.18449372,-2.25936954,603.55,64.10,319.00,218.81,219.69,-0.58,96.62,1183.28,100.00,0.5117,0.3667,0.1020,1.0000,1 +25.281,51.18488900,-2.25988507,593.17,63.45,307.04,218.86,218.30,1.61,85.04,1183.48,99.97,0.5314,0.1824,0.0000,1.0000,1 +25.812,51.18519999,-2.26055086,578.16,59.11,291.10,217.47,217.37,0.88,77.82,1180.51,99.70,0.2843,0.3255,0.0922,1.0000,1 +26.343,51.18540834,-2.26132765,563.06,54.46,288.70,219.85,218.35,2.87,68.78,1177.18,99.45,0.1726,0.0000,0.0000,1.0000,1 +26.812,51.18556143,-2.26206488,547.00,47.88,285.21,221.93,219.98,3.35,71.33,1175.19,99.22,0.3020,-0.2188,0.0000,1.0000,1 +27.312,51.18569587,-2.26288238,526.98,44.38,277.69,223.57,221.65,2.83,72.48,1171.55,98.96,0.4471,0.3039,0.1255,1.0000,1 +27.796,51.18576178,-2.26363482,510.29,41.60,270.43,224.49,222.83,1.90,62.74,1169.86,98.74,0.1588,0.4275,0.0765,1.0000,1 +28.328,51.18576606,-2.26450027,494.76,37.22,267.82,226.40,224.55,2.73,42.83,1166.03,98.47,0.1451,0.3628,0.0000,1.0000,1 +28.796,51.18574259,-2.26531071,480.88,32.60,266.33,228.23,226.15,2.93,23.06,1164.04,98.24,0.1922,0.3647,0.0039,1.0000,1 +29.328,51.18570384,-2.26619893,467.08,30.82,265.32,229.89,227.91,2.85,-7.24,1160.36,97.98,0.1353,0.3824,0.0373,1.0000,1 +29.843,51.18566513,-2.26703765,454.78,29.62,265.61,230.36,228.34,2.41,-34.61,567.37,98.01,0.1393,0.3804,0.0412,1.0000,1 +30.375,51.18563179,-2.26794156,441.75,26.33,269.31,229.36,227.62,0.51,-45.19,559.61,98.17,0.2863,0.0373,0.0785,1.0000,1 +30.953,51.18563337,-2.26891360,434.30,29.89,273.12,227.26,225.75,-0.64,-37.30,563.43,98.33,0.0765,-0.3203,0.0000,1.0000,1 +31.468,51.18566592,-2.26976166,431.62,39.24,274.29,225.59,224.07,-0.27,-25.46,565.09,98.48,-0.0352,-0.2891,0.0000,1.0000,1 +31.984,51.18571076,-2.27062126,429.01,48.86,275.44,223.93,222.45,-0.20,5.88,566.09,98.63,0.0000,-0.6602,0.0000,1.0000,1 +32.515,51.18575974,-2.27148813,426.17,50.48,274.89,222.15,220.83,-1.24,69.18,567.12,98.78,0.4353,-0.2930,0.0000,1.0000,1 +32.937,51.18578664,-2.27218073,421.87,41.86,258.83,219.59,219.78,-3.51,81.22,1147.94,98.70,0.8784,0.0039,0.0000,1.0000,1 +33.359,51.18572128,-2.27285469,420.21,40.23,236.18,211.09,213.22,-5.33,83.85,1174.30,98.47,0.5667,0.1039,0.0000,1.0000,1 +33.859,51.18547143,-2.27349546,421.73,41.80,215.12,206.67,207.94,-5.16,85.18,1169.51,98.19,0.7235,0.1471,0.0000,1.0000,1 +34.359,51.18508173,-2.27394180,423.69,42.05,194.22,201.91,203.20,-4.73,86.51,1164.90,97.92,0.6098,0.1883,0.0000,1.0000,1 +34.875,51.18464422,-2.27413758,424.31,36.31,177.32,200.00,200.79,-4.13,83.09,1160.98,97.65,0.6294,0.0785,0.0000,1.0000,1 +35.343,51.18418416,-2.27413054,423.92,35.88,168.96,200.40,200.19,-2.93,79.78,1157.24,97.38,0.3902,0.3333,0.0687,1.0000,1 +35.843,51.18374041,-2.27399771,421.99,33.90,165.16,202.09,201.42,-1.38,58.40,1153.49,97.12,0.0059,0.4490,0.0000,1.0000,1 +36.375,51.18327466,-2.27379110,418.36,30.20,163.83,203.71,202.77,0.24,37.28,577.39,97.07,0.0569,0.3275,0.0000,1.0000,1 +36.875,51.18282113,-2.27357319,413.38,25.24,161.53,203.20,202.43,-1.40,31.02,557.40,97.21,0.2079,0.0000,0.0000,1.0000,1 +37.375,51.18236307,-2.27332126,412.20,22.85,159.91,202.42,201.62,-1.79,27.78,555.13,97.36,0.1530,0.2373,0.0000,1.0000,1 +37.843,51.18195119,-2.27307127,412.94,21.06,158.76,201.49,200.64,-1.48,13.75,556.78,97.50,0.0941,0.3647,0.0236,1.0000,1 +38.343,51.18153057,-2.27280483,414.02,22.08,158.07,200.76,199.91,-1.37,-6.25,558.88,97.65,0.1079,0.3667,0.0373,1.0000,1 +38.890,51.18106098,-2.27250899,415.00,23.02,158.30,199.95,199.16,-1.59,-26.67,559.46,97.82,0.2353,0.2745,0.0667,1.0000,2 +39.375,51.18063897,-2.27225563,416.47,24.62,161.41,198.86,198.21,-4.38,-32.69,560.51,97.97,0.3255,0.2314,0.0902,1.0000,2 +39.843,51.18023220,-2.27204862,423.51,32.07,164.47,197.53,196.55,-6.28,-39.96,561.92,98.11,0.2432,0.2333,0.1373,1.0000,2 +40.328,51.17980371,-2.27186535,435.41,43.83,165.53,196.28,195.11,-5.19,-35.26,562.89,98.26,-0.3047,-0.3418,0.0000,1.0000,2 +40.796,51.17940745,-2.27170625,444.02,52.25,165.88,195.32,194.53,-2.82,-9.21,563.67,98.40,0.0000,-0.6230,0.0000,1.0000,2 +41.281,51.17896919,-2.27154047,449.11,55.09,167.06,194.59,193.96,-2.69,45.31,564.82,98.54,0.0824,-0.5332,0.0000,1.0000,2 +41.828,51.17850051,-2.27135050,450.23,48.17,163.76,193.77,193.48,-2.73,73.42,565.87,98.71,0.3451,-0.1797,-0.0410,1.0000,2 +42.312,51.17809052,-2.27115531,447.70,45.56,156.19,192.83,192.79,-2.21,82.95,567.05,98.86,0.4412,0.0000,0.0000,1.0000,2 +42.781,51.17770969,-2.27088858,442.47,40.23,144.71,191.78,192.01,-1.27,85.31,1068.08,98.91,0.6255,0.0745,0.0000,1.0000,2 +43.265,51.17735563,-2.27050045,434.27,32.12,124.75,189.16,190.70,-2.21,80.81,1176.99,98.65,0.7510,0.1922,0.0000,1.0000,2 +43.781,51.17708744,-2.26990272,427.84,25.68,105.66,185.47,186.99,-4.10,74.94,1173.61,98.38,0.3686,0.4745,0.0000,1.0000,2 +44.250,51.17696162,-2.26929440,426.24,24.22,99.68,187.25,187.32,-3.97,67.79,729.26,98.21,0.4098,0.0000,-0.0586,1.0000,2 +44.734,51.17688936,-2.26863400,426.57,21.67,95.64,187.37,187.15,-3.06,67.64,569.03,98.34,0.3333,0.0000,-0.0391,1.0000,2 +45.218,51.17685332,-2.26795823,425.80,17.17,88.95,186.56,186.54,-3.67,68.32,564.03,98.49,0.4157,0.0157,0.0000,1.0000,2 +45.703,51.17686016,-2.26729083,425.25,23.24,85.41,185.91,185.68,-2.52,59.92,566.38,98.65,0.2275,0.3353,0.0000,1.0000,2 +46.156,51.17689350,-2.26669102,423.84,28.76,82.87,186.08,185.76,-2.06,49.84,567.80,98.78,0.3882,0.0628,0.0000,1.0000,2 +46.640,51.17695186,-2.26603160,422.96,28.05,78.27,185.40,185.27,-4.97,41.67,568.15,98.93,0.5157,0.0000,0.0000,1.0000,2 +47.156,51.17704811,-2.26535686,430.21,35.67,72.59,183.80,183.32,-8.90,43.06,570.19,99.08,0.4098,-0.1387,-0.0020,1.0000,2 +47.656,51.17718351,-2.26471660,446.29,46.62,68.36,183.15,181.71,-10.42,48.29,1120.95,99.08,0.3000,-0.3047,-0.0254,1.0000,2 +48.093,51.17732418,-2.26417317,463.63,36.14,65.34,183.69,182.03,-10.55,60.30,1180.54,98.84,0.3784,-0.2637,-0.0117,1.0000,2 +48.546,51.17749161,-2.26360735,481.88,40.41,60.06,184.33,182.89,-10.82,70.17,1176.51,98.57,0.3726,-0.2578,-0.0117,1.0000,2 +49.031,51.17770166,-2.26304603,500.24,53.24,53.99,185.00,183.88,-10.17,77.81,1170.05,98.31,0.4177,-0.1875,-0.0059,1.0000,2 +49.546,51.17794897,-2.26250026,517.05,59.83,42.79,184.71,184.43,-9.57,84.49,605.86,98.22,0.5941,-0.2344,0.0000,1.0000,2 +50.078,51.17827648,-2.26200140,530.59,50.63,29.41,181.02,181.66,-7.49,92.38,563.73,98.37,0.4863,-0.0488,-0.0273,1.0000,2 +50.593,51.17865736,-2.26164812,537.24,41.45,20.33,179.93,180.55,-3.95,94.21,564.04,98.53,0.4216,0.1667,0.0000,1.0000,2 +51.078,51.17901836,-2.26141091,536.23,34.86,17.39,179.66,179.84,-1.45,63.79,562.84,98.68,0.1745,0.5314,0.0000,1.0000,2 +51.609,51.17942468,-2.26120612,531.19,28.17,15.92,180.14,180.02,0.33,20.90,565.32,98.83,0.2902,0.4490,0.0000,1.0000,2 +52.140,51.17986187,-2.26101738,526.74,24.60,15.33,180.50,180.50,-1.92,-16.43,566.91,98.99,0.3529,0.4314,0.0000,1.0000,2 +52.609,51.18024442,-2.26084399,527.71,25.70,18.64,180.46,180.57,-5.42,-39.60,1001.87,99.07,0.4196,0.2451,0.0000,1.0000,2 +53.093,51.18063588,-2.26062366,535.94,31.16,23.40,181.38,181.00,-7.51,-44.28,1176.04,98.82,0.1098,-0.2227,0.0000,1.0000,2 +53.531,51.18097033,-2.26039187,546.96,40.14,23.86,182.83,182.11,-5.86,-21.81,1173.60,98.59,-0.0410,-0.6836,0.0000,1.0000,2 +54.046,51.18136876,-2.26009963,558.72,43.54,25.31,184.72,184.19,-4.47,34.85,1167.77,98.32,0.0824,-0.3242,0.0000,1.0000,2 +54.546,51.18175012,-2.25982241,566.09,40.91,24.73,186.62,186.45,-4.06,63.52,1163.36,98.06,0.2902,-0.4844,-0.0078,1.0000,2 +55.015,51.18213402,-2.25955797,568.89,32.94,17.81,188.09,188.47,-4.66,77.55,1158.78,97.80,0.5216,0.0000,0.0000,1.0000,2 +55.500,51.18253458,-2.25935251,569.91,27.66,10.42,189.47,189.78,-3.68,78.44,1154.59,97.53,0.6431,0.0000,-0.0039,1.0000,2 +55.953,51.18291958,-2.25924290,569.44,25.08,0.54,190.21,190.83,-3.71,79.35,1150.87,97.28,0.5117,0.0000,-0.0059,1.0000,2 +56.375,51.18330454,-2.25922488,568.33,25.28,352.02,191.00,191.53,-3.32,79.08,1147.37,97.04,0.5490,0.1432,0.0000,1.0000,2 +56.875,51.18373699,-2.25932039,566.41,25.57,339.49,191.22,192.10,-3.74,79.74,1143.55,96.78,0.5725,0.0000,-0.0078,1.0000,2 +57.453,51.18421515,-2.25961409,564.56,28.88,326.05,191.49,192.12,-3.29,81.17,1139.41,96.48,0.4980,-0.0449,-0.0117,1.0000,2 +57.953,51.18458018,-2.25999751,561.85,33.27,315.45,192.34,192.83,-2.50,82.28,1135.83,96.22,0.5294,0.0000,-0.0293,1.0000,2 +58.421,51.18490071,-2.26048314,557.37,37.17,306.44,193.30,193.57,-1.33,83.14,1132.07,95.95,0.3961,0.1236,-0.0664,1.0000,2 +58.890,51.18513921,-2.26098894,550.53,37.84,299.88,195.00,194.81,-0.15,77.61,1128.56,95.71,0.3980,0.2569,0.0000,1.0000,2 +59.406,51.18537954,-2.26163795,540.43,35.96,292.49,196.79,196.48,0.15,74.74,1125.14,95.44,0.4275,0.0000,-0.0352,1.0000,2 +59.875,51.18554250,-2.26226026,530.28,35.17,285.15,198.40,197.98,0.22,75.01,1121.55,95.20,0.4216,0.0000,-0.0293,1.0000,2 +60.406,51.18566862,-2.26300794,519.09,39.66,274.47,199.52,199.38,-0.43,75.52,1118.47,94.93,0.3726,0.0745,-0.0137,1.0000,2 +60.937,51.18571554,-2.26382645,507.84,42.02,269.83,201.73,200.94,1.10,65.31,1114.47,94.65,0.1255,0.5314,0.0000,1.0000,2 +61.500,51.18571204,-2.26461930,494.68,38.50,268.16,204.34,203.11,2.88,29.32,1111.25,94.38,0.2608,0.2882,0.0000,1.0000,2 +62.093,51.18568911,-2.26545612,481.12,35.04,266.66,206.86,205.64,2.56,2.00,1107.87,94.12,0.0000,0.4294,0.0000,1.0000,2 +62.687,51.18565727,-2.26636409,467.79,34.52,266.26,209.34,207.94,2.90,-32.13,1104.05,93.82,0.0981,0.4059,0.0000,1.0000,2 +63.296,51.18562952,-2.26730801,451.43,29.08,269.46,211.76,210.12,1.81,-52.74,1100.20,93.51,0.3765,0.2471,0.0000,1.0000,2 +63.937,51.18564486,-2.26830896,438.78,27.35,277.27,213.09,212.01,-0.32,-46.92,1095.28,93.18,0.0039,-0.3438,0.0000,1.0000,2 +64.531,51.18572059,-2.26922517,432.57,32.23,278.37,214.89,213.56,1.13,-20.23,1091.95,92.88,0.0471,-0.5957,-0.0039,1.0000,2 +65.156,51.18581606,-2.27019684,423.64,39.17,279.13,216.77,215.26,0.64,42.85,1087.55,92.58,0.3902,-0.1797,0.0000,1.0000,2 +65.765,51.18589596,-2.27121321,415.96,43.17,268.41,217.16,216.49,-4.02,72.12,1082.79,92.24,0.7118,-0.3438,-0.0078,1.0000,2 +66.296,51.18587096,-2.27204165,416.92,37.99,252.95,214.61,214.41,-4.28,81.70,1079.52,91.98,0.2589,0.3961,0.0000,1.0000,2 +66.796,51.18572821,-2.27281563,418.88,39.04,234.56,212.46,213.31,-5.92,81.04,1075.98,91.73,0.7706,-0.0801,-0.0508,1.0000,2 +67.312,51.18544339,-2.27347238,424.77,45.00,211.57,205.59,206.63,-6.36,88.49,1072.31,91.46,0.6588,-0.1465,-0.0527,1.0000,2 +67.859,51.18503221,-2.27389326,429.46,47.42,190.50,201.48,202.84,-3.69,92.23,1069.01,91.20,0.5373,0.0000,-0.0312,1.0000,2 +68.515,51.18447024,-2.27408430,424.94,36.54,173.35,200.54,200.85,-0.78,86.45,1064.68,90.90,0.6471,0.1588,-0.0078,1.0000,2 +69.046,51.18392144,-2.27400791,414.97,26.61,166.09,201.43,200.58,0.39,65.53,1060.69,90.60,0.2098,0.4569,0.0000,1.0000,2 +69.625,51.18342156,-2.27380386,405.44,17.12,161.67,203.00,202.15,-1.14,38.02,622.95,90.41,0.3647,0.1961,-0.0410,1.0000,2 +70.156,51.18292814,-2.27353678,403.40,15.46,159.56,201.94,201.12,-2.40,9.66,502.79,90.56,0.2039,0.4020,0.0000,1.0000,2 +70.703,51.18245068,-2.27325056,407.73,18.82,159.11,200.75,199.81,-3.00,-21.77,501.28,90.71,0.0726,0.4627,0.0000,1.0000,2 +71.250,51.18199565,-2.27298031,413.18,21.63,160.52,199.23,198.41,-3.56,-33.42,501.10,90.86,0.0039,0.0490,0.0000,1.0000,2 +71.781,51.18152076,-2.27273183,418.57,26.73,161.82,197.85,197.08,-3.03,-34.67,502.67,91.02,0.1824,0.0000,0.0216,1.0000,2 +72.312,51.18105441,-2.27250228,422.48,30.59,163.41,196.72,196.02,-2.76,-33.87,504.29,91.18,0.1922,-0.1895,0.0000,1.0000,3 +72.781,51.18065025,-2.27232118,425.18,33.27,164.56,195.85,195.18,-2.24,-30.48,504.83,91.31,0.1177,-0.2949,0.0000,1.0000,3 +73.281,51.18020736,-2.27213859,427.11,35.18,165.93,194.91,194.29,-2.18,-19.58,505.89,91.46,0.2745,-0.1914,0.0000,1.0000,3 +73.890,51.17969381,-2.27194430,431.17,39.36,167.22,193.73,193.04,-2.92,0.39,507.36,91.63,0.0745,-0.4590,-0.0117,1.0000,3 +74.390,51.17923268,-2.27177759,436.36,44.78,167.45,192.72,192.09,-2.73,31.89,508.63,91.78,0.0785,-0.4609,-0.0137,1.0000,3 +74.859,51.17884972,-2.27163196,439.02,43.19,166.38,191.92,191.47,-2.88,60.75,509.49,91.92,0.1784,-0.4785,-0.0078,1.0000,3 +75.281,51.17846978,-2.27146935,438.93,36.89,161.69,191.03,190.85,-3.21,70.21,510.58,92.05,0.4902,0.2255,0.0000,1.0000,3 +75.750,51.17807729,-2.27125801,437.86,35.83,152.81,189.48,189.67,-3.93,76.14,511.84,92.20,0.5294,0.0000,0.0000,1.0000,3 +76.250,51.17769878,-2.27094650,437.04,35.00,140.12,187.06,187.75,-4.08,83.86,513.07,92.35,0.6392,-0.1035,-0.0059,1.0000,3 +76.781,51.17735572,-2.27049161,434.58,32.51,121.18,182.63,184.21,-3.09,87.27,514.50,92.52,0.6784,0.1510,0.0000,1.0000,3 +77.359,51.17708945,-2.26981048,427.84,25.55,105.48,179.18,179.87,-1.87,77.54,516.07,92.69,0.4490,0.3882,0.0000,1.0000,3 +77.859,51.17697212,-2.26919764,421.19,18.92,98.60,178.79,178.90,-2.12,62.56,517.22,92.83,0.5039,0.3647,0.0000,1.0000,3 +78.296,51.17691178,-2.26860383,417.86,12.52,91.29,177.76,178.16,-5.13,55.70,518.66,92.97,0.4471,0.1804,0.0000,1.0000,3 +78.812,51.17690301,-2.26794413,420.98,16.22,87.60,177.21,177.05,-5.10,47.92,520.05,93.12,0.3765,0.0000,-0.0020,1.0000,3 +79.265,51.17692500,-2.26734675,426.18,26.61,83.42,176.45,176.32,-6.76,48.04,521.17,93.26,0.3177,-0.0293,-0.0098,1.0000,3 +79.750,51.17697512,-2.26671654,435.10,40.47,79.71,175.56,175.17,-7.54,49.28,522.33,93.41,0.3647,0.0000,0.0000,1.0000,3 +80.250,51.17705428,-2.26608692,446.05,51.66,76.22,174.57,174.06,-7.71,51.99,523.38,93.56,0.1059,-0.0332,0.0000,1.0000,3 +80.734,51.17715356,-2.26549575,456.80,60.71,74.73,174.01,173.60,-6.21,54.05,524.29,93.71,0.1902,0.0000,0.0000,1.0000,3 +81.218,51.17726669,-2.26487932,464.67,57.86,71.95,173.48,173.39,-5.73,55.28,525.43,93.86,0.2765,0.0000,0.0000,1.0000,3 +81.718,51.17739478,-2.26429044,470.78,41.60,68.87,173.08,173.06,-5.29,50.08,526.34,94.02,0.2118,0.3784,0.0000,1.0000,3 +82.187,51.17753508,-2.26373848,475.96,36.49,65.48,172.65,172.67,-6.24,43.50,527.51,94.16,0.4314,-0.2207,-0.0410,1.0000,3 +82.671,51.17770105,-2.26318871,484.31,36.84,60.48,171.69,171.61,-8.54,54.69,528.48,94.31,0.4529,-0.3164,-0.0820,1.0000,3 +83.125,51.17788470,-2.26269102,495.77,38.77,51.82,170.00,170.04,-11.35,66.66,529.66,94.45,0.6431,-0.3730,-0.0449,1.0000,3 +83.593,51.17810694,-2.26224043,510.21,41.90,38.65,166.88,167.37,-12.40,84.49,530.36,94.59,0.5863,-0.3008,-0.0469,1.0000,3 +84.046,51.17837083,-2.26187883,523.94,38.73,28.17,164.87,165.42,-9.71,91.05,531.50,94.73,0.4961,-0.1777,-0.0488,1.0000,3 +84.500,51.17866737,-2.26159387,532.57,35.45,20.95,163.83,164.64,-6.59,93.52,531.93,94.87,0.4314,0.0000,-0.0664,1.0000,3 +84.984,51.17900509,-2.26136988,535.27,32.90,17.93,164.01,164.58,-3.38,80.82,533.13,95.02,0.1294,0.4353,0.0000,1.0000,3 +85.468,51.17935236,-2.26119114,532.49,27.72,17.12,164.88,165.14,-0.47,53.34,534.13,95.17,0.0745,0.4667,0.0000,1.0000,3 +86.031,51.17975385,-2.26100626,524.99,20.54,15.52,166.10,166.05,1.56,23.21,535.44,95.33,0.2098,0.5490,0.0000,1.0000,3 +86.593,51.18019184,-2.26082401,516.17,12.69,14.63,167.16,167.24,-1.04,-13.07,537.26,95.50,0.4608,0.4667,0.0000,1.0000,3 +87.109,51.18057632,-2.26065884,515.72,10.96,18.89,166.64,167.15,-7.21,-29.43,538.60,95.65,0.2549,0.0687,-0.0332,1.0000,3 +87.546,51.18089672,-2.26048286,524.75,18.82,19.66,166.42,166.04,-6.48,-32.07,539.89,95.79,0.1020,-0.0781,0.0000,1.0000,3 +88.046,51.18125509,-2.26027206,535.07,23.84,21.07,166.21,166.02,-6.38,-28.60,541.15,95.93,0.3098,-0.3125,0.0000,1.0000,3 +88.531,51.18160838,-2.26004828,545.52,27.86,22.51,165.92,165.58,-6.19,4.65,541.85,96.08,0.0000,-0.6816,-0.0273,1.0000,3 +88.968,51.18191668,-2.25983862,555.11,28.98,23.13,165.86,165.65,-5.56,52.15,625.24,96.21,0.2177,-0.4590,-0.0879,1.0000,3 +89.468,51.18226844,-2.25961478,562.11,27.81,20.82,167.49,167.80,-5.27,72.32,1129.62,96.01,0.2039,-0.2051,-0.1699,1.0000,3 +89.906,51.18258614,-2.25942558,564.63,24.08,17.44,169.51,169.94,-3.96,76.68,1132.82,95.77,0.4098,-0.0664,-0.1406,1.0000,3 +90.390,51.18295822,-2.25925086,563.78,19.32,8.00,171.21,172.08,-3.90,78.10,1128.19,95.49,0.6451,0.0196,-0.0840,1.0000,3 +90.828,51.18330353,-2.25916574,561.90,17.50,356.21,171.64,172.95,-4.32,78.83,1124.19,95.25,0.6059,0.1137,-0.0820,1.0000,3 +91.265,51.18365141,-2.25918453,560.12,17.12,345.23,172.30,173.57,-4.24,79.61,1120.54,95.01,0.6667,0.0706,-0.1660,1.0000,3 +91.796,51.18404279,-2.25934490,558.33,18.48,329.36,171.66,173.29,-4.53,80.83,1116.84,94.73,0.5353,0.0020,-0.1758,1.0000,3 +92.265,51.18438539,-2.25964460,556.71,21.85,318.74,172.67,173.84,-3.94,77.91,1113.37,94.48,0.3882,0.2745,-0.1211,1.0000,3 +92.765,51.18469437,-2.26006372,554.42,27.19,311.44,174.66,175.26,-3.03,74.09,1109.50,94.20,0.5176,0.2177,-0.0547,1.0000,3 +93.250,51.18495980,-2.26052652,551.13,31.83,306.60,176.87,177.16,-1.76,73.57,1105.90,93.95,0.3412,0.0314,-0.1719,1.0000,3 +93.734,51.18520669,-2.26106152,545.36,33.51,301.00,179.38,179.48,-0.92,74.36,1101.99,93.68,0.5039,0.0000,-0.0586,1.0000,3 +94.218,51.18540650,-2.26159445,537.95,32.79,292.38,180.99,181.32,-1.07,76.59,1098.42,93.44,0.4431,0.0000,-0.0488,1.0000,3 +94.750,51.18558246,-2.26226869,528.31,32.73,282.76,182.91,183.14,-0.58,78.47,1094.89,93.16,0.4824,0.0000,-0.0391,1.0000,3 +95.281,51.18568461,-2.26296886,517.38,36.66,273.50,184.69,184.67,0.07,74.66,1090.84,92.89,0.3686,0.4373,0.0000,1.0000,3 +95.828,51.18571714,-2.26371945,505.18,37.63,270.70,187.70,187.03,2.15,48.83,1087.38,92.60,0.1020,0.4451,0.0000,1.0000,3 +96.328,51.18571714,-2.26438781,492.49,33.77,269.35,190.51,189.44,3.39,37.75,1083.80,92.36,0.0961,0.2941,0.0000,1.0000,3 +96.812,51.18570581,-2.26506675,478.62,27.29,267.63,193.10,191.92,3.07,19.79,1081.36,92.12,0.2216,0.3157,0.0000,1.0000,3 +97.343,51.18568102,-2.26584840,464.57,23.12,266.60,195.94,194.78,3.16,-3.87,1077.21,91.85,0.2177,0.2882,0.0000,1.0000,3 +97.875,51.18565514,-2.26659342,452.43,21.88,267.17,198.25,197.23,1.51,-19.52,1074.45,91.60,0.2569,0.3196,0.0000,1.0000,3 +98.468,51.18563577,-2.26748091,443.22,22.88,270.01,200.46,199.70,-0.79,-44.11,1070.08,91.30,0.3039,0.3510,0.0000,1.0000,3 +98.921,51.18564145,-2.26816434,439.62,26.46,273.40,201.79,201.10,-1.41,-51.04,1067.20,91.06,0.1490,-0.3613,-0.0117,1.0000,3 +99.390,51.18566898,-2.26883541,436.92,31.21,274.43,203.29,202.42,0.05,-32.73,1063.61,90.82,0.0000,-0.4434,-0.0176,1.0000,3 +99.921,51.18571416,-2.26962062,431.82,36.79,275.61,205.09,204.05,1.19,1.01,1059.86,90.56,0.0451,-0.5156,-0.0195,1.0000,3 +100.406,51.18575810,-2.27033558,425.57,42.60,276.11,206.70,205.53,1.42,38.56,1056.26,90.32,0.1314,-0.4980,-0.0156,1.0000,3 +100.890,51.18579909,-2.27108657,416.79,41.52,272.41,208.09,207.04,-0.70,54.95,1052.95,90.07,0.4490,-0.4238,-0.0195,1.0000,3 +101.390,51.18580723,-2.27185768,412.00,34.27,261.57,207.74,207.52,-4.67,70.19,1048.90,89.80,0.5117,-0.3438,-0.0410,1.0000,3 +101.906,51.18572523,-2.27264771,414.15,34.27,242.90,205.46,206.24,-6.19,82.15,1045.33,89.51,0.7725,0.0000,0.0000,1.0000,3 +102.390,51.18552929,-2.27329813,418.96,39.17,220.39,198.97,200.81,-6.75,86.11,1041.64,89.24,0.7333,0.0000,0.0000,1.0000,3 +102.859,51.18522130,-2.27375007,423.72,42.18,199.49,194.17,196.14,-5.67,88.80,1038.15,89.00,0.6862,0.0000,0.0000,1.0000,3 +103.359,51.18481075,-2.27401299,425.81,39.48,186.86,192.60,192.99,-3.98,83.13,1034.78,88.72,0.5000,0.0745,-0.0059,1.0000,3 +103.859,51.18437727,-2.27410481,425.40,37.35,171.41,191.63,192.62,-4.10,82.85,1030.80,88.45,0.6000,0.1432,0.0000,1.0000,3 +104.296,51.18397310,-2.27403874,424.30,36.24,164.20,191.59,191.58,-2.78,77.41,1027.41,88.20,0.2039,0.4059,0.0000,1.0000,3 +104.796,51.18356377,-2.27386501,421.41,33.25,163.17,193.49,192.85,-0.25,51.11,1023.73,87.93,0.0706,0.4745,0.0000,1.0000,3 +105.296,51.18313042,-2.27364568,415.57,27.31,161.71,195.46,194.60,1.24,20.42,1019.92,87.67,0.1647,0.3902,0.0000,1.0000,3 +105.796,51.18269457,-2.27340961,409.13,20.99,160.66,197.16,196.39,-1.19,6.67,1016.01,87.38,0.3392,0.1686,0.0000,1.0000,3 +106.250,51.18229994,-2.27318757,408.69,18.86,160.40,198.24,197.48,-2.03,-3.63,1012.49,87.14,0.1451,0.2412,0.0000,1.0000,3 +106.750,51.18187565,-2.27294425,411.19,19.23,160.29,199.45,198.61,-1.60,-15.82,1008.57,86.86,0.0902,0.3157,0.0000,1.0000,3 +107.203,51.18148699,-2.27273242,412.33,20.34,160.57,200.51,199.65,-1.23,-27.62,1005.07,86.62,0.1726,0.2275,0.0000,1.0000,3 +107.734,51.18101622,-2.27248413,412.63,20.70,162.99,201.60,200.81,-2.86,-30.69,1000.71,86.33,0.2843,0.0000,0.0000,1.0000,4 +108.234,51.18056542,-2.27227897,416.25,24.52,164.43,202.45,201.50,-2.89,-25.24,996.81,86.06,0.1490,-0.2695,0.0000,1.0000,4 +108.687,51.18016257,-2.27210128,420.28,28.41,165.14,203.20,202.25,-2.21,-14.07,992.89,85.82,0.1255,-0.3145,0.0000,1.0000,4 +109.187,51.17970319,-2.27191618,423.66,31.75,165.82,204.12,203.14,-1.71,6.24,988.82,85.54,0.1667,-0.3984,0.0000,1.0000,4 +109.640,51.17928202,-2.27174565,426.44,34.77,165.56,204.75,203.82,-3.15,22.94,984.99,85.29,0.1726,-0.5078,0.0000,1.0000,4 +110.109,51.17886693,-2.27156970,430.93,35.66,164.25,205.28,204.35,-3.82,47.70,981.17,85.03,0.2451,-0.4023,0.0000,1.0000,4 +110.609,51.17841171,-2.27133984,435.75,33.82,159.79,205.56,204.86,-4.74,67.51,977.05,84.76,0.3843,-0.1816,0.0000,1.0000,4 +111.046,51.17801183,-2.27110022,439.63,37.76,153.35,205.57,205.03,-4.55,81.70,973.03,84.50,0.4137,-0.2754,0.0000,1.0000,4 +111.484,51.17764346,-2.27080692,441.44,39.42,142.63,205.00,204.99,-2.63,95.91,969.55,84.26,0.7078,0.0000,0.0000,1.0000,4 +112.031,51.17726386,-2.27033306,435.17,32.73,120.13,200.64,201.72,0.01,85.56,965.33,83.97,0.7255,0.4059,0.0000,1.0000,4 +112.515,51.17700986,-2.26969672,425.14,22.73,100.90,196.06,197.16,-2.81,73.59,961.92,83.72,0.5667,0.4392,0.0000,1.0000,4 +113.031,51.17691029,-2.26896017,421.83,19.86,91.62,195.70,195.65,-4.35,62.33,958.95,83.44,0.2824,0.3059,0.0000,1.0000,4 +113.531,51.17689463,-2.26824024,424.68,19.81,88.65,196.61,195.99,-3.88,46.69,955.24,83.17,0.2745,0.0000,-0.0137,1.0000,4 +114.046,51.17691311,-2.26748908,429.66,29.84,86.01,197.32,196.61,-4.09,44.87,951.22,82.88,0.2255,0.1059,0.0000,1.0000,4 +114.562,51.17695158,-2.26678249,435.66,40.89,82.92,197.81,197.11,-5.02,45.36,947.22,82.60,0.2569,0.0000,0.0000,1.0000,4 +115.078,51.17702012,-2.26601292,444.42,49.78,78.66,197.95,197.12,-7.04,47.08,942.66,82.31,0.3020,-0.0684,-0.0117,1.0000,4 +115.531,51.17710429,-2.26538373,455.94,61.09,74.89,197.73,196.57,-8.28,52.68,938.94,82.05,0.2843,-0.4082,-0.0156,1.0000,4 +116.015,51.17722686,-2.26468903,471.89,57.06,69.35,197.31,196.06,-9.27,70.19,934.25,81.77,0.3294,-0.4160,-0.0156,1.0000,4 +116.500,51.17738629,-2.26404118,487.06,51.56,63.82,196.96,195.98,-7.92,86.93,930.30,81.50,0.3706,0.1177,0.0000,1.0000,4 +116.937,51.17755832,-2.26347272,497.12,53.44,58.22,196.76,196.32,-5.82,88.79,925.48,81.25,0.4216,0.2882,0.0000,1.0000,4 +117.421,51.17777716,-2.26290130,503.40,50.48,47.46,195.90,196.13,-6.42,73.22,921.83,81.00,0.6137,0.0196,-0.0273,1.0000,4 +117.921,51.17808036,-2.26236106,511.72,45.12,38.09,194.64,194.52,-7.03,75.94,917.70,80.74,0.4765,0.0000,-0.0352,1.0000,4 +118.437,51.17844255,-2.26191406,521.46,33.54,27.97,193.67,193.64,-6.98,80.77,913.62,80.45,0.3824,0.0000,-0.0273,1.0000,4 +118.890,51.17879764,-2.26160244,529.04,32.26,21.79,193.59,193.51,-5.42,83.91,909.64,80.21,0.4020,0.0000,-0.0234,1.0000,4 +119.375,51.17921390,-2.26133328,533.19,31.85,16.26,193.80,193.77,-3.52,79.53,905.66,79.92,0.0000,0.6628,0.0000,1.0000,4 +119.828,51.17958547,-2.26115438,533.45,31.13,15.22,194.71,194.43,-1.25,42.69,902.02,79.69,0.0000,0.5980,0.0000,1.0000,4 +120.359,51.18004551,-2.26096334,531.61,30.73,13.74,195.84,195.45,-0.91,-7.60,897.96,79.41,0.3294,0.4784,0.0000,1.0000,4 +120.906,51.18052751,-2.26076586,532.41,30.62,16.74,196.29,196.04,-4.57,-48.36,894.06,79.14,0.3647,0.4863,0.0000,1.0000,4 +121.406,51.18097448,-2.26053466,538.71,34.68,24.43,195.88,195.65,-6.63,-57.46,890.16,78.86,0.3216,-0.3652,-0.0039,1.0000,4 +121.859,51.18134018,-2.26027721,548.85,37.46,25.94,195.90,195.09,-5.58,-26.83,886.33,78.60,0.0000,-0.6172,-0.0059,1.0000,4 +122.328,51.18172515,-2.25997078,560.03,40.46,27.00,196.08,195.27,-4.82,21.88,882.57,78.35,0.1275,-0.6836,-0.0195,1.0000,4 +122.781,51.18208782,-2.25968673,568.65,37.82,25.84,196.04,195.71,-5.66,69.13,878.81,78.10,0.5176,0.0294,0.0000,1.0000,4 +123.328,51.18254802,-2.25936656,576.52,34.98,13.73,194.92,195.30,-6.72,77.91,873.88,77.81,0.5510,0.0000,0.0000,1.0000,4 +123.843,51.18298562,-2.25919918,583.61,38.90,2.02,193.57,194.06,-6.04,85.01,869.98,77.54,0.5980,-0.3320,0.0000,1.0000,4 +124.359,51.18344385,-2.25916986,588.07,44.78,346.76,191.45,192.57,-3.70,92.29,866.12,77.28,0.6000,0.2255,0.0667,1.0000,4 +125.000,51.18398457,-2.25937702,583.73,44.15,328.78,189.10,190.06,-0.65,86.34,861.74,76.98,0.4510,0.4333,0.0412,1.0000,4 +125.562,51.18440106,-2.25975826,573.32,40.22,318.66,189.36,189.57,0.10,78.91,857.88,76.70,0.4647,0.0922,0.0000,1.0000,4 +126.234,51.18484485,-2.26039250,557.90,36.42,306.53,189.84,189.82,0.24,75.85,853.82,76.36,0.4588,0.0981,0.0000,1.0000,4 +126.859,51.18517111,-2.26110096,543.31,32.10,298.54,191.01,190.58,0.78,69.19,849.47,76.05,0.4177,0.0981,0.0000,1.0000,4 +127.500,51.18543465,-2.26190272,528.40,27.63,289.06,191.79,191.58,-0.16,69.11,845.72,75.74,0.4490,0.0000,0.0000,1.0000,4 +128.093,51.18560297,-2.26270460,516.84,30.99,280.18,192.33,192.17,-0.69,69.84,842.23,75.44,0.3882,0.0000,0.0000,1.0000,4 +128.718,51.18569593,-2.26359718,506.29,37.30,271.47,192.96,192.77,-0.85,70.69,837.82,75.13,0.3529,0.0000,0.0000,1.0000,4 +129.234,51.18570972,-2.26434451,497.50,38.41,268.71,193.99,193.41,1.08,62.67,834.40,74.86,0.0000,0.3804,0.0000,1.0000,4 +129.859,51.18569083,-2.26518657,483.63,33.96,266.79,195.65,194.57,3.23,26.60,830.40,74.59,0.0000,0.4726,0.0000,1.0000,4 +130.546,51.18564871,-2.26618530,464.84,28.59,265.55,197.52,196.24,3.72,-10.03,825.83,74.23,0.2353,0.3784,0.0000,1.0000,4 +131.156,51.18561210,-2.26706459,450.31,25.76,268.11,198.61,197.80,-0.09,-34.58,822.34,73.94,0.3549,0.2530,0.0000,1.0000,4 +131.640,51.18560204,-2.26781924,445.57,29.09,272.17,198.80,198.32,-2.43,-43.14,818.35,73.66,0.2686,0.1706,0.0647,1.0000,4 +132.109,51.18562082,-2.26851038,445.97,36.70,273.61,199.11,198.45,-1.71,-37.71,815.23,73.41,0.0000,-0.3867,0.0000,1.0000,4 +132.656,51.18565959,-2.26929553,445.56,46.06,274.83,199.57,198.84,-0.51,-14.34,811.23,73.14,-0.0137,-0.3984,0.0000,1.0000,4 +133.125,51.18569947,-2.26998402,443.91,55.29,275.56,199.99,199.22,-0.06,11.03,807.57,72.89,0.0000,-0.4824,0.0000,1.0000,4 +133.609,51.18574008,-2.27069227,440.90,61.23,275.61,200.39,199.63,0.01,47.26,803.85,72.64,0.0608,-0.5117,0.0000,1.0000,4 +134.078,51.18577445,-2.27138976,435.05,59.94,271.84,200.70,199.99,-0.65,78.73,800.24,72.39,0.5451,-0.1895,0.0000,1.0000,4 +134.531,51.18578135,-2.27206408,426.59,46.64,255.76,198.70,199.27,-1.23,82.41,796.53,72.13,0.6431,0.2922,0.0000,1.0000,4 +134.968,51.18570163,-2.27268232,418.94,38.68,239.48,195.07,196.25,-2.65,79.42,793.37,71.89,0.6961,0.1471,0.0000,1.0000,4 +135.421,51.18550408,-2.27325681,413.98,33.89,218.49,189.47,191.72,-4.93,80.55,790.05,71.64,0.7568,0.0706,-0.0117,1.0000,4 +135.890,51.18520745,-2.27366301,413.25,31.56,198.74,184.05,186.23,-5.93,82.21,786.46,71.40,0.6569,0.2137,-0.0391,1.0000,4 +136.406,51.18480112,-2.27391455,414.74,28.34,181.69,180.45,181.95,-5.61,83.70,782.72,71.12,0.5902,0.1118,-0.0781,1.0000,4 +136.859,51.18442900,-2.27395595,415.65,27.65,171.38,179.36,180.06,-4.30,78.35,778.89,70.86,0.3824,0.6157,0.0000,1.0000,4 +137.312,51.18404505,-2.27388183,415.65,27.66,167.07,179.85,179.86,-3.69,56.83,774.89,70.60,0.2059,0.4863,0.0000,1.0000,4 +137.828,51.18362298,-2.27372277,417.00,29.13,164.30,180.58,180.35,-3.56,39.98,770.75,70.32,0.1686,0.2510,-0.0391,1.0000,4 +138.296,51.18324674,-2.27354541,419.69,31.78,163.04,181.29,180.92,-2.48,28.81,766.93,70.06,-0.1699,0.3451,0.0000,1.0000,4 +138.812,51.18283429,-2.27333392,421.47,33.48,161.93,182.11,181.71,-1.42,13.53,762.73,69.79,0.0000,0.2804,0.0000,1.0000,4 +139.265,51.18247431,-2.27314083,421.78,32.88,161.38,182.86,182.44,-0.85,8.34,758.93,69.54,0.0177,0.0000,0.0000,1.0000,4 +139.718,51.18210503,-2.27293671,420.93,29.77,161.09,183.66,183.20,-0.34,7.88,755.17,69.27,0.0118,0.0647,0.0000,1.0000,4 +140.234,51.18170539,-2.27271825,418.66,26.50,160.80,184.54,184.05,0.18,7.95,750.99,68.99,0.0079,0.0608,0.0000,1.0000,4 +140.750,51.18129233,-2.27248239,414.72,22.64,160.50,185.50,184.95,0.69,8.05,746.74,68.70,0.0314,0.0608,0.0000,1.0000,4 diff --git a/track_data/flight_20260901_182822_021306.csv b/track_data/flight_20260901_182822_021306.csv new file mode 100644 index 00000000..773b058a --- /dev/null +++ b/track_data/flight_20260901_182822_021306.csv @@ -0,0 +1,294 @@ +# aircraft_title=Extra 300S Paint2 +# recorded_at=2026-09-01T18:28:22 +# laps_s=29.112,33.068,35.489,35.390 +timestamp_s,latitude,longitude,altitude_ft,agl_ft,heading_deg,airspeed_kts,groundspeed_kts,pitch_deg,bank_deg,torque,health_pct,stick_pitch,stick_roll,stick_yaw,stick_throttle,lap +0.328,51.18972671,-2.27594444,980.94,617.52,166.15,183.75,182.53,6.92,0.13,1113.15,100.00,-0.1406,0.0098,0.0000,1.0000,1 +0.797,51.18932913,-2.27578857,959.36,590.47,166.01,187.37,185.69,8.47,0.15,1169.44,100.00,-0.1855,0.0138,0.0000,1.0000,1 +1.281,51.18891749,-2.27562666,933.55,561.60,165.99,191.08,188.69,9.41,0.16,1173.92,100.00,-0.1387,0.0510,0.0000,1.0000,1 +1.781,51.18849310,-2.27545994,904.40,528.99,165.99,195.02,192.10,10.01,0.17,1174.62,100.00,-0.1621,0.0530,0.0000,1.0000,1 +2.312,51.18810666,-2.27530468,875.35,496.70,165.97,198.68,195.17,10.71,0.17,1176.98,100.00,-0.0938,0.0883,0.0000,1.0000,1 +2.797,51.18759243,-2.27510417,835.98,454.76,165.97,203.21,199.13,11.12,0.18,1176.97,100.00,-0.0664,0.0863,0.0000,1.0000,1 +3.297,51.18715368,-2.27493059,801.10,419.73,165.96,207.09,202.47,11.45,0.19,1179.18,100.00,-0.0723,0.0844,0.0000,1.0000,1 +3.750,51.18672462,-2.27475767,765.02,383.62,165.95,210.72,205.73,11.70,0.19,1178.84,100.00,-0.0781,0.0863,0.0000,1.0000,1 +4.187,51.18632035,-2.27460014,731.36,349.90,165.94,214.15,208.66,11.88,0.19,1180.66,100.00,-0.0703,0.0863,0.0000,1.0000,1 +4.656,51.18587636,-2.27442670,695.01,312.94,165.93,217.50,211.72,12.01,0.20,1181.15,100.00,-0.0293,0.0902,0.0000,1.0000,1 +5.078,51.18546919,-2.27426136,659.18,277.80,165.92,220.78,214.52,12.09,0.21,1180.75,100.00,-0.0098,0.1039,0.0000,1.0000,1 +5.531,51.18504686,-2.27409260,622.81,240.38,165.90,223.76,217.32,12.11,-0.80,1183.83,100.00,0.0000,0.1706,0.0000,1.0000,1 +6.000,51.18457970,-2.27390272,582.16,192.70,165.90,227.25,220.37,12.09,-3.07,1183.12,100.00,0.0000,0.0451,0.0000,1.0000,1 +6.469,51.18410167,-2.27371735,542.68,152.53,166.04,230.25,223.31,11.82,-3.46,1186.59,100.00,0.1314,0.0000,0.0314,1.0000,1 +6.922,51.18364687,-2.27353540,504.55,115.24,166.37,232.90,226.70,9.90,-3.47,1186.01,100.00,0.2804,0.0000,0.0000,1.0000,1 +7.359,51.18319686,-2.27336512,472.97,83.84,166.59,235.27,230.22,8.37,-3.19,1189.28,100.00,0.1804,-0.1016,0.0000,1.0000,1 +7.797,51.18273835,-2.27319428,445.35,56.85,166.78,237.25,232.74,6.88,-3.06,1189.11,100.00,0.2157,0.0000,0.0000,1.0000,1 +8.266,51.18225058,-2.27300692,421.12,29.97,167.09,239.07,235.59,4.27,-3.07,1190.93,100.00,0.2981,-0.0273,0.0000,1.0000,1 +8.750,51.18174474,-2.27282306,406.26,13.97,167.58,239.98,237.66,-0.24,-2.80,1191.27,100.00,0.4314,0.0000,0.0000,1.0000,1 +9.297,51.18117437,-2.27261733,406.37,14.59,167.73,240.38,238.09,-2.76,-2.74,1191.95,100.00,0.0000,-0.0879,0.0000,1.0000,1 +9.766,51.18064001,-2.27243457,413.33,21.62,167.81,240.65,238.26,-2.97,3.71,1191.99,100.00,0.0000,-0.2871,0.0000,1.0000,1 +10.234,51.18015427,-2.27226191,420.77,29.05,167.75,240.80,238.43,-3.21,14.82,1191.44,100.00,0.0118,-0.3105,0.0000,1.0000,1 +10.687,51.17964733,-2.27208367,428.36,36.77,167.34,240.96,238.61,-3.42,31.26,1190.90,100.00,0.0000,-0.3418,0.0000,1.0000,1 +11.156,51.17913617,-2.27188894,435.76,44.53,166.37,241.04,238.87,-3.42,52.50,1190.70,100.00,0.0981,-0.3809,0.0000,1.0000,1 +11.594,51.17867158,-2.27169253,440.74,42.59,162.90,241.14,239.17,-4.02,70.49,1189.54,100.00,0.4647,-0.2461,0.0000,1.0000,1 +12.047,51.17818892,-2.27144572,446.03,44.16,149.89,239.24,238.23,-5.67,84.71,1189.42,100.00,0.5235,-0.5078,0.0000,1.0000,1 +12.500,51.17777065,-2.27106176,451.43,49.63,135.40,236.81,236.12,-3.53,91.60,1189.00,100.00,0.6333,0.3255,0.1177,1.0000,1 +12.984,51.17741441,-2.27050479,452.28,50.25,118.47,233.35,232.92,-2.91,82.66,1189.16,100.00,0.5373,0.2863,0.0510,1.0000,1 +13.469,51.17714153,-2.26976571,451.92,49.92,106.86,231.94,231.02,-3.22,77.89,1189.17,100.00,0.4726,0.3000,0.0000,1.0000,1 +13.922,51.17699201,-2.26903197,453.33,51.45,98.57,231.43,230.21,-3.97,70.10,1189.34,100.00,0.3471,0.2530,0.0000,1.0000,1 +14.375,51.17691700,-2.26822707,458.21,54.29,91.19,231.10,229.70,-5.22,67.50,1189.34,100.00,0.3235,0.1961,0.0000,1.0000,1 +14.875,51.17690620,-2.26739913,466.88,66.47,87.34,231.39,229.65,-4.71,67.22,1189.38,100.00,0.2804,0.0000,0.0000,1.0000,1 +15.328,51.17693169,-2.26666062,474.44,79.74,82.12,231.42,229.83,-5.29,68.02,1188.97,100.00,0.3333,-0.0801,0.0000,1.0000,1 +15.797,51.17700451,-2.26585510,483.84,89.20,76.24,231.30,229.62,-5.76,69.06,1188.58,100.00,0.3216,-0.1426,0.0000,1.0000,1 +16.266,51.17712625,-2.26508829,494.10,97.43,71.26,231.23,229.57,-5.68,74.17,1188.47,100.00,0.2941,-0.2617,0.0000,1.0000,1 +16.687,51.17726594,-2.26441595,502.41,84.40,66.75,231.29,229.75,-5.04,81.68,1187.64,100.00,0.3392,-0.1562,0.0000,1.0000,1 +17.156,51.17746823,-2.26368731,509.31,69.85,59.75,231.17,229.95,-4.12,83.00,1187.66,100.00,0.3667,0.1275,0.0000,1.0000,1 +17.609,51.17770703,-2.26301300,513.28,61.36,53.11,231.22,230.03,-3.27,79.84,1186.90,100.00,0.4275,0.2373,0.0000,1.0000,1 +18.094,51.17799733,-2.26237027,515.80,49.45,40.32,230.08,229.50,-4.41,79.24,1186.92,100.00,0.5804,0.1255,0.0000,1.0000,1 +18.609,51.17841955,-2.26183248,520.94,31.20,24.41,227.28,226.91,-5.94,77.51,1186.60,100.00,0.5686,0.1000,-0.0098,1.0000,1 +19.062,51.17885652,-2.26150234,529.47,30.87,14.58,225.84,224.91,-6.31,76.76,1186.83,100.00,0.1941,0.2922,0.0000,1.0000,1 +19.531,51.17931179,-2.26130068,538.95,38.28,13.02,226.51,225.01,-4.29,43.49,1186.45,100.00,-0.1172,0.5706,0.0000,1.0000,1 +20.094,51.17989104,-2.26109589,550.27,51.73,11.32,227.32,225.75,-3.74,-17.41,1186.47,100.00,-0.0234,0.5706,0.0000,1.0000,1 +20.578,51.18039327,-2.26092428,558.58,61.05,11.65,227.78,226.52,-4.07,-68.00,1185.56,100.00,0.1412,0.5333,0.0000,1.0000,1 +21.016,51.18084110,-2.26076120,562.79,63.40,16.00,228.35,227.27,-3.07,-75.49,1185.48,100.00,0.2902,-0.3457,-0.1250,1.0000,1 +21.469,51.18131401,-2.26054583,564.93,60.75,20.11,229.00,227.88,-2.50,-54.52,1184.94,100.00,0.2765,-0.2598,-0.0820,1.0000,1 +21.906,51.18174253,-2.26029040,568.05,55.59,22.26,229.63,228.36,-2.61,-38.60,1184.79,100.00,0.0059,-0.4355,-0.0879,1.0000,1 +22.391,51.18220611,-2.25997826,572.35,48.39,23.34,230.37,229.05,-1.96,-1.82,1184.61,100.00,0.0000,-0.5508,-0.1055,1.0000,1 +22.891,51.18268751,-2.25965430,576.25,41.05,23.59,231.10,229.85,-2.52,47.99,1184.40,100.00,0.3098,-0.4863,-0.1328,1.0000,1 +23.359,51.18317440,-2.25935169,579.42,38.15,11.95,230.39,230.03,-6.33,76.34,1184.23,100.00,0.7274,0.0000,-0.0117,1.0000,1 +23.812,51.18364447,-2.25920350,587.82,45.36,350.83,224.15,225.14,-8.40,83.96,1183.96,100.00,0.5392,-0.1406,-0.0566,1.0000,1 +24.297,51.18411213,-2.25930462,599.70,59.68,335.66,221.69,221.79,-6.49,91.44,1184.36,100.00,0.6745,-0.2031,-0.0195,1.0000,1 +24.781,51.18457434,-2.25963825,607.06,71.87,314.34,216.78,218.18,-3.05,96.35,1183.80,100.00,0.5196,0.0000,0.0000,1.0000,1 +25.266,51.18493020,-2.26015579,603.64,78.52,304.71,216.76,216.54,0.64,96.04,1182.49,99.86,0.3647,0.3588,0.0000,1.0000,1 +25.750,51.18522126,-2.26079145,591.21,75.51,299.54,218.38,217.07,2.84,83.81,1178.98,99.62,0.3255,0.0000,-0.0254,1.0000,1 +26.203,51.18544770,-2.26142926,574.70,66.74,293.74,220.10,218.30,3.78,81.69,1175.53,99.37,0.4235,-0.0527,0.0000,1.0000,1 +26.703,51.18564596,-2.26213391,553.91,55.28,282.24,221.04,219.24,3.85,84.59,1173.40,99.13,0.5314,-0.1719,0.0000,1.0000,1 +27.187,51.18576086,-2.26290756,530.60,48.21,273.54,221.96,219.40,5.14,79.23,1169.99,98.89,0.3000,0.3549,0.0079,1.0000,1 +27.703,51.18579616,-2.26373928,504.02,36.88,268.03,224.35,221.24,4.94,53.64,1168.46,98.64,0.3726,0.4784,0.0255,1.0000,1 +28.234,51.18577495,-2.26462318,481.60,25.40,264.74,226.24,224.13,3.52,19.06,1164.70,98.37,0.1373,0.4922,0.0000,1.0000,1 +28.719,51.18572789,-2.26540905,467.11,19.88,263.94,228.14,225.94,3.53,-17.73,1163.36,98.13,0.1726,0.3784,0.0000,1.0000,1 +29.219,51.18567974,-2.26623595,452.32,16.67,265.54,229.72,227.58,2.20,-35.60,1159.83,97.89,0.2706,-0.1113,-0.0176,1.0000,1 +29.750,51.18564613,-2.26713373,440.73,16.91,269.71,230.95,229.23,-0.61,-37.58,1157.43,97.65,0.3059,0.0000,0.0000,1.0000,1 +30.281,51.18564735,-2.26800790,438.55,24.09,272.25,231.09,229.47,-1.71,-37.89,601.15,97.55,0.0000,-0.1582,0.0000,1.0000,1 +30.812,51.18567586,-2.26893544,439.64,35.18,273.68,229.86,228.24,-1.30,-26.67,555.60,97.70,-0.0273,-0.4766,0.0000,1.0000,1 +31.312,51.18571414,-2.26976774,440.59,47.73,274.88,228.02,226.47,-1.34,11.61,560.26,97.84,-0.0449,-0.5195,0.0000,1.0000,1 +31.797,51.18575236,-2.27057815,441.82,61.58,274.51,226.07,224.66,-1.90,54.97,560.09,97.98,0.0294,-0.5195,0.0000,1.0000,1 +32.266,51.18578164,-2.27136399,440.53,65.65,269.82,224.09,222.93,-1.85,86.03,561.41,98.13,0.5804,0.1569,0.0000,1.0000,1 +32.734,51.18577518,-2.27212399,435.65,55.57,252.81,219.53,219.90,-1.36,86.66,563.00,98.27,0.6706,0.0726,0.0000,1.0000,1 +33.187,51.18565314,-2.27282450,428.49,48.19,233.99,214.50,214.96,-0.91,85.90,1153.91,98.13,0.6628,0.2333,0.0000,1.0000,1 +33.672,51.18538126,-2.27344045,419.56,39.43,213.40,209.93,211.00,-2.35,78.92,1165.64,97.86,0.7470,0.3235,0.0000,1.0000,1 +34.141,51.18501343,-2.27384306,416.33,33.65,192.25,204.21,206.03,-7.13,74.63,1160.90,97.62,0.6902,-0.1602,-0.0820,1.0000,1 +34.625,51.18456940,-2.27400880,424.19,36.60,176.35,201.00,201.13,-8.74,77.19,1157.07,97.36,0.4588,-0.0430,-0.1016,1.0000,1 +35.094,51.18414505,-2.27398129,435.84,48.28,167.84,201.10,200.32,-8.08,81.21,1153.53,97.09,0.4294,-0.3184,-0.1074,1.0000,1 +35.562,51.18370952,-2.27384155,447.09,59.40,161.77,201.58,200.87,-5.87,95.67,1149.43,96.84,0.1588,0.0000,-0.1074,1.0000,1 +36.000,51.18331995,-2.27365157,452.37,64.46,160.71,202.82,202.13,-3.17,86.62,741.31,96.66,-0.0215,0.4314,0.0000,1.0000,1 +36.453,51.18291160,-2.27342884,452.57,64.49,159.41,203.28,202.57,-0.92,61.90,552.53,96.78,-0.0957,0.4333,0.0000,1.0000,1 +36.937,51.18250950,-2.27317614,448.94,60.05,157.75,202.35,201.56,0.23,33.91,551.19,96.92,-0.0801,0.4451,0.0000,1.0000,1 +37.469,51.18203823,-2.27286670,443.69,52.00,156.46,201.95,201.11,0.61,-1.95,553.63,97.08,0.0000,0.4922,0.0000,1.0000,1 +37.922,51.18164798,-2.27259944,439.05,46.85,156.35,201.52,200.69,0.42,-36.21,553.32,97.22,0.2118,0.4824,0.0000,1.0000,1 +38.391,51.18125802,-2.27233311,433.72,41.64,159.76,200.83,200.14,-1.32,-45.54,554.80,97.36,0.4314,0.1000,0.0000,1.0000,1 +38.859,51.18083085,-2.27210019,431.91,39.96,164.78,199.74,199.21,-3.16,-49.07,556.67,97.50,0.1843,0.0000,0.0000,1.0000,2 +39.344,51.18042379,-2.27192556,433.84,41.90,166.06,198.98,198.32,-2.22,-46.57,557.28,97.64,0.0628,-0.2715,-0.0273,1.0000,2 +39.797,51.18001394,-2.27177408,434.21,42.18,167.22,198.38,197.72,-1.15,-35.17,558.66,97.77,0.0628,-0.3809,-0.1035,1.0000,2 +40.281,51.17957335,-2.27162638,432.87,40.82,168.61,197.90,197.21,-0.77,-8.56,559.76,97.92,0.1334,-0.5254,-0.1641,1.0000,2 +40.781,51.17912117,-2.27148479,431.88,39.13,168.93,197.36,196.68,-1.04,18.96,560.99,98.07,0.1334,-0.2773,-0.1641,1.0000,2 +41.266,51.17870869,-2.27134736,431.08,31.30,168.04,196.83,196.20,-1.46,48.53,562.29,98.21,0.2373,-0.5039,-0.1641,1.0000,2 +41.719,51.17829391,-2.27119346,429.20,27.13,163.19,196.03,195.68,-3.05,63.71,563.45,98.35,0.4333,-0.3770,-0.1211,1.0000,2 +42.156,51.17791921,-2.27100936,428.35,26.35,153.34,194.40,194.61,-4.80,74.81,564.56,98.49,0.6921,-0.0781,-0.0723,1.0000,2 +42.609,51.17755079,-2.27071117,430.26,28.42,135.33,189.52,191.07,-7.48,78.38,851.44,98.61,0.6784,-0.0801,-0.0703,1.0000,2 +43.125,51.17724860,-2.27025477,437.43,35.71,117.56,186.32,187.50,-8.26,81.46,1170.80,98.39,0.5882,0.0000,-0.0566,1.0000,2 +43.750,51.17698278,-2.26946242,448.29,46.61,103.26,185.83,185.99,-6.64,84.12,1169.55,98.07,0.5471,0.0000,-0.0469,1.0000,2 +44.203,51.17688316,-2.26885477,453.98,52.02,93.71,186.42,186.67,-5.26,85.66,1164.70,97.81,0.4686,0.0745,-0.0234,1.0000,2 +44.656,51.17684580,-2.26821808,456.29,49.44,88.63,187.83,187.82,-3.27,79.68,1160.58,97.56,0.1765,0.4431,0.0000,1.0000,2 +45.094,51.17684980,-2.26762142,455.21,51.09,87.14,190.03,189.65,-1.32,62.14,1156.57,97.32,0.2412,0.3157,0.0000,1.0000,2 +45.578,51.17687637,-2.26693251,451.55,56.10,83.60,191.43,191.04,-1.58,48.45,568.60,97.35,0.3157,0.3275,0.0000,1.0000,2 +46.047,51.17692768,-2.26626929,450.06,55.10,78.64,190.06,189.98,-4.75,44.81,556.48,97.50,0.3196,0.0000,-0.0332,1.0000,2 +46.516,51.17700807,-2.26566639,454.91,60.20,74.40,189.25,188.96,-6.93,46.69,558.37,97.63,0.3471,-0.2754,-0.0352,1.0000,2 +47.016,51.17713609,-2.26499014,466.28,66.05,69.72,187.64,186.87,-8.41,57.16,557.64,97.79,0.3314,-0.3770,-0.0371,1.0000,2 +47.469,51.17727392,-2.26440805,479.10,54.70,65.24,186.18,185.45,-8.55,67.02,560.01,97.93,0.2824,-0.2090,-0.0547,1.0000,2 +47.906,51.17743284,-2.26385664,490.36,53.75,61.98,185.24,184.67,-7.52,69.72,559.75,98.07,0.2039,0.0000,-0.0371,1.0000,2 +48.344,51.17761530,-2.26332934,499.83,53.16,58.52,184.31,183.99,-6.53,71.16,560.84,98.21,0.4157,-0.1797,-0.0352,1.0000,2 +48.828,51.17782228,-2.26280188,507.40,53.50,51.10,183.02,183.21,-6.98,73.18,562.14,98.35,0.5020,-0.1641,-0.0391,1.0000,2 +49.312,51.17807474,-2.26230177,514.82,43.96,39.61,181.04,181.59,-7.01,83.58,562.57,98.50,0.6157,0.0000,-0.0137,1.0000,2 +49.797,51.17839679,-2.26186268,520.29,31.39,24.81,177.94,179.34,-5.96,87.22,921.86,98.61,0.4961,0.0177,-0.0430,1.0000,2 +50.297,51.17875807,-2.26156739,522.26,23.59,21.31,179.62,179.82,-3.01,71.45,1168.96,98.38,0.3039,0.4686,0.0000,1.0000,2 +50.781,51.17912779,-2.26133985,520.97,18.40,19.27,182.04,181.97,-1.41,34.97,1167.34,98.13,0.2667,0.4941,0.0000,1.0000,2 +51.297,51.17953912,-2.26111882,520.01,15.68,17.63,184.51,184.41,-2.54,9.27,1162.48,97.87,0.2882,0.0000,-0.1074,1.0000,2 +51.797,51.17995060,-2.26091359,523.52,19.52,17.11,186.70,186.35,-3.11,-4.92,1158.44,97.61,0.1137,0.4412,0.0000,1.0000,2 +52.281,51.18036348,-2.26070839,528.45,23.71,16.98,188.77,188.43,-2.91,-31.34,1154.55,97.36,0.2333,0.4216,0.0000,1.0000,2 +52.750,51.18075998,-2.26050331,531.73,24.96,19.86,190.49,190.33,-4.22,-49.74,1150.49,97.10,0.4216,0.1922,0.0000,1.0000,2 +53.219,51.18113428,-2.26027751,536.30,25.15,22.85,192.06,191.73,-4.05,-44.98,1146.43,96.85,0.0608,-0.4609,-0.0781,1.0000,2 +53.687,51.18151751,-2.26001190,540.91,20.62,23.77,193.81,193.42,-2.66,-16.77,1142.83,96.62,0.2981,-0.5664,-0.0762,1.0000,2 +54.156,51.18191775,-2.25972126,546.61,16.26,24.39,195.12,194.68,-7.35,21.02,1138.50,96.35,0.3588,-0.5625,-0.0645,1.0000,2 +54.625,51.18229331,-2.25947081,560.51,21.93,20.80,195.54,194.34,-9.93,63.07,1134.95,96.11,0.3490,-0.5859,-0.0625,1.0000,2 +55.078,51.18267964,-2.25924523,576.94,32.98,12.90,195.79,195.14,-8.95,92.28,1130.41,95.86,0.5157,0.3569,0.0000,1.0000,2 +55.547,51.18309511,-2.25908789,588.72,42.08,358.65,194.64,195.43,-6.34,91.79,1126.62,95.60,0.6862,0.2353,0.0000,1.0000,2 +56.016,51.18350021,-2.25908493,593.91,49.30,341.48,191.73,193.50,-4.24,92.17,1121.95,95.35,0.5902,0.0000,-0.0352,1.0000,2 +56.500,51.18391887,-2.25928188,592.65,51.99,329.22,191.49,192.38,-1.33,91.86,1118.26,95.08,0.5216,0.2196,0.0000,1.0000,2 +56.969,51.18427089,-2.25959290,585.22,49.23,319.67,192.41,192.66,0.16,80.43,1114.90,94.84,0.5078,0.3843,0.0000,1.0000,2 +57.453,51.18461512,-2.26003405,574.80,46.77,311.60,193.83,193.74,0.33,75.73,1111.41,94.60,0.4373,0.0000,-0.0254,1.0000,2 +57.937,51.18490253,-2.26055979,563.57,44.96,303.48,195.53,195.30,-0.04,70.86,1108.51,94.35,0.4745,0.0000,-0.0371,1.0000,2 +58.406,51.18514299,-2.26111722,554.37,43.52,298.41,197.15,196.82,0.33,70.61,1105.11,94.10,0.3275,-0.0391,-0.0312,1.0000,2 +58.891,51.18535405,-2.26173513,544.02,41.05,291.80,199.04,198.55,0.38,74.75,1101.99,93.85,0.4098,-0.2168,-0.0195,1.0000,2 +59.437,51.18553675,-2.26248272,531.12,40.68,283.45,200.69,200.14,0.82,78.33,1098.68,93.58,0.4020,-0.1836,-0.0195,1.0000,2 +59.953,51.18565009,-2.26321043,517.48,41.96,275.24,202.29,201.50,1.60,78.48,1095.36,93.31,0.3471,0.3804,0.0000,1.0000,2 +60.422,51.18569822,-2.26394024,502.74,38.75,270.69,204.25,202.99,2.56,60.14,1092.14,93.07,0.2471,0.4745,0.0000,1.0000,2 +60.969,51.18570232,-2.26473144,486.79,31.82,268.08,206.66,205.28,2.73,24.61,1089.04,92.81,0.1451,0.4765,0.0000,1.0000,2 +61.547,51.18568200,-2.26557241,472.63,28.01,267.10,208.96,207.58,3.24,-7.08,1085.54,92.55,0.0510,0.4216,0.0000,1.0000,2 +62.156,51.18565667,-2.26652228,457.69,26.18,268.08,211.34,209.94,1.82,-28.70,1082.38,92.26,0.2216,0.3255,0.0000,1.0000,2 +62.766,51.18564621,-2.26741907,446.05,25.35,271.19,213.18,211.91,0.07,-43.00,1078.17,91.97,0.3314,0.1118,0.0000,1.0000,2 +63.406,51.18566913,-2.26845300,437.79,28.06,273.50,214.94,213.66,0.89,-29.50,1074.01,91.64,0.0373,-0.4082,-0.0156,1.0000,2 +64.000,51.18571162,-2.26936068,429.82,31.42,274.73,216.60,215.15,1.61,3.18,1069.48,91.33,0.0922,-0.4395,-0.0156,1.0000,2 +64.656,51.18576398,-2.27041245,419.37,37.74,274.31,218.34,216.84,0.87,39.98,1064.98,91.00,0.2039,-0.3926,-0.0195,1.0000,2 +65.187,51.18579512,-2.27132416,410.61,36.03,269.17,219.35,218.08,-1.50,63.90,1061.20,90.70,0.5039,-0.3672,-0.0215,1.0000,2 +65.687,51.18577644,-2.27213562,406.37,26.53,253.36,217.57,217.65,-4.13,80.35,1057.36,90.44,0.5941,-0.1074,-0.0215,1.0000,2 +66.219,51.18562659,-2.27294370,406.24,26.28,232.77,213.65,214.33,-4.58,84.27,1054.17,90.18,0.7470,0.0000,-0.0020,1.0000,2 +66.766,51.18530010,-2.27362145,407.63,26.97,204.77,205.86,207.96,-4.99,87.10,1050.63,89.91,0.7510,0.0549,-0.0020,1.0000,2 +67.281,51.18488287,-2.27396314,408.31,23.10,186.21,201.87,202.76,-4.63,80.67,1047.53,89.67,0.7274,0.1922,-0.0449,1.0000,2 +67.844,51.18436496,-2.27405148,411.05,23.23,167.04,198.39,198.99,-6.03,80.14,1044.17,89.40,0.6274,0.1549,-0.0488,1.0000,2 +68.391,51.18387067,-2.27388071,416.86,29.06,162.02,198.11,197.43,-4.00,55.61,508.51,89.35,0.0000,0.4922,0.0000,1.0000,2 +68.922,51.18342042,-2.27363871,421.52,33.68,160.25,196.51,195.77,-2.74,18.02,490.36,89.48,-0.0293,0.4902,0.0000,1.0000,2 +69.484,51.18292565,-2.27335504,426.35,38.53,159.45,195.26,194.54,-2.61,-13.27,492.31,89.65,0.0000,0.2902,0.0000,1.0000,2 +69.984,51.18250821,-2.27311425,430.24,41.74,159.85,194.44,193.80,-2.40,-26.85,493.08,89.79,0.0000,0.2765,0.0000,1.0000,2 +70.484,51.18207068,-2.27287160,432.72,41.59,160.81,193.44,192.86,-1.94,-31.71,492.87,89.94,0.1314,0.1490,0.0471,1.0000,2 +70.969,51.18165784,-2.27265634,433.33,41.31,161.91,192.58,192.06,-1.32,-32.51,494.47,90.09,0.1255,-0.1465,0.0000,1.0000,2 +71.453,51.18125997,-2.27246043,432.22,40.15,163.18,192.09,191.57,-1.01,-29.83,495.82,90.22,0.1922,-0.2227,0.0000,1.0000,2 +71.906,51.18085976,-2.27227962,430.65,38.61,164.56,191.51,190.99,-1.03,-21.69,496.37,90.36,0.1353,-0.2832,0.0000,1.0000,3 +72.391,51.18045106,-2.27210598,429.48,37.44,165.46,190.92,190.40,-1.02,-14.11,497.87,90.50,0.1804,-0.2129,0.0000,1.0000,3 +72.875,51.18005203,-2.27194548,429.00,37.01,166.14,190.39,189.87,-1.35,-10.47,499.18,90.64,0.0863,-0.1289,0.0000,1.0000,3 +73.375,51.17962276,-2.27178359,429.27,37.28,166.52,189.79,189.27,-1.17,-8.04,500.23,90.79,0.1530,-0.1992,0.0000,1.0000,3 +73.812,51.17924952,-2.27164499,429.36,37.49,166.93,189.28,188.77,-1.17,5.76,501.49,90.93,0.1608,-0.4570,0.0000,1.0000,3 +74.281,51.17884212,-2.27149186,429.44,32.75,166.54,188.73,188.27,-2.38,34.68,502.61,91.06,0.4059,-0.4277,0.0000,1.0000,3 +74.766,51.17842855,-2.27131948,430.71,28.78,161.56,187.55,187.45,-4.83,66.57,503.77,91.21,0.4980,-0.2109,0.0000,1.0000,3 +75.250,51.17803085,-2.27109580,432.98,31.06,151.68,185.71,186.08,-5.51,77.55,505.06,91.36,0.5078,0.1118,0.0000,1.0000,3 +75.687,51.17769985,-2.27082083,435.03,33.10,143.44,184.09,184.41,-4.88,78.80,506.18,91.50,0.5294,0.2392,0.0000,1.0000,3 +76.172,51.17737117,-2.27043613,436.46,34.49,130.12,181.59,182.57,-5.79,78.72,507.34,91.65,0.7510,0.0000,-0.0137,1.0000,3 +76.656,51.17710747,-2.26995706,439.06,37.20,112.54,176.65,178.39,-7.17,80.90,508.58,91.79,0.6059,0.0647,0.0000,1.0000,3 +77.125,51.17693757,-2.26937948,442.87,40.98,103.10,174.72,175.39,-5.70,82.33,509.52,91.92,0.5235,0.0981,0.0000,1.0000,3 +77.594,51.17684758,-2.26882813,444.67,42.52,94.53,173.49,174.18,-4.47,82.24,510.61,92.06,0.3255,0.2490,0.0000,1.0000,3 +78.062,51.17680712,-2.26820594,443.70,35.20,88.78,173.17,173.51,-2.98,75.58,511.83,92.22,0.3863,0.2922,0.0000,1.0000,3 +78.516,51.17681035,-2.26764760,440.65,33.09,84.75,173.14,173.27,-2.08,63.63,512.93,92.35,0.3157,0.3549,0.0000,1.0000,3 +79.000,51.17684991,-2.26700832,436.81,39.08,80.63,173.21,173.28,-2.42,47.38,514.12,92.50,0.2981,0.3588,0.0000,1.0000,3 +79.516,51.17692024,-2.26637638,435.27,40.29,77.57,173.18,173.19,-3.34,31.13,515.50,92.64,0.3255,0.2530,0.0000,1.0000,3 +80.016,51.17701171,-2.26576578,438.38,43.61,74.52,172.60,172.62,-6.63,29.31,516.72,92.79,0.2412,-0.2598,-0.0039,1.0000,3 +80.531,51.17712637,-2.26513976,448.78,52.94,72.10,171.83,171.27,-7.76,35.45,517.95,92.93,0.2765,-0.3262,-0.0059,1.0000,3 +81.000,51.17724934,-2.26456191,461.07,49.49,68.82,170.83,170.20,-9.36,47.77,519.23,93.08,0.3726,-0.2324,-0.0117,1.0000,3 +81.453,51.17738437,-2.26404506,475.18,44.69,64.66,169.72,168.98,-10.35,58.24,519.88,93.23,0.3138,-0.3066,-0.0117,1.0000,3 +81.922,51.17754462,-2.26352443,489.60,49.64,60.77,168.73,168.08,-9.82,67.38,521.09,93.37,0.2824,-0.3262,-0.0059,1.0000,3 +82.391,51.17772916,-2.26301340,502.61,51.60,55.21,167.76,167.63,-9.14,76.33,521.54,93.52,0.5098,-0.1875,-0.0137,1.0000,3 +82.859,51.17793359,-2.26254451,512.67,54.44,46.80,166.42,166.88,-8.35,84.31,522.68,93.66,0.4608,-0.2441,-0.0137,1.0000,3 +83.375,51.17819693,-2.26208622,519.85,42.55,37.33,165.33,166.21,-5.96,87.52,523.47,93.82,0.6510,0.2412,0.0000,1.0000,3 +83.812,51.17846339,-2.26174557,521.71,28.87,25.28,163.40,164.93,-5.21,80.62,524.56,93.96,0.4137,0.3843,0.0000,1.0000,3 +84.281,51.17877086,-2.26148641,521.47,20.70,21.64,163.60,164.22,-3.55,71.52,525.54,94.10,0.4431,0.0177,-0.0430,1.0000,3 +84.750,51.17910649,-2.26127175,519.48,14.40,15.96,163.73,164.39,-3.20,63.17,526.78,94.25,0.1079,0.5863,0.0000,1.0000,3 +85.281,51.17948418,-2.26110569,516.65,11.01,14.62,164.67,164.86,-0.78,22.98,528.13,94.40,0.1804,0.5451,0.0000,1.0000,3 +85.797,51.17987011,-2.26095572,514.15,10.22,13.57,165.34,165.53,-2.47,-10.12,529.52,94.55,0.5117,0.3745,0.0000,1.0000,3 +86.297,51.18024728,-2.26080670,516.76,13.82,16.03,165.51,165.67,-6.05,-35.60,877.46,94.66,0.3628,0.4314,0.0000,1.0000,3 +86.812,51.18064021,-2.26062125,525.59,20.50,19.38,167.26,167.19,-6.76,-49.60,1113.40,94.42,0.2020,-0.2520,0.0000,1.0000,3 +87.297,51.18097966,-2.26041956,533.94,27.25,20.52,169.13,169.04,-5.29,-35.67,1111.42,94.17,0.2353,-0.4434,0.0000,1.0000,3 +87.766,51.18133274,-2.26019574,541.29,28.52,22.07,171.20,171.05,-4.90,-5.60,1107.00,93.91,0.2333,-0.5312,0.0000,1.0000,3 +88.234,51.18167989,-2.25997124,549.45,28.16,22.38,173.03,172.79,-5.41,26.66,1102.84,93.66,0.2333,-0.4629,0.0000,1.0000,3 +88.781,51.18208410,-2.25971295,559.27,28.24,20.45,174.96,174.86,-6.07,44.83,1098.04,93.37,0.2608,-0.0781,0.0000,1.0000,3 +89.250,51.18245309,-2.25951183,567.40,30.12,18.00,176.61,176.56,-6.01,57.40,1093.92,93.10,0.2177,-0.4551,-0.0176,1.0000,3 +89.734,51.18281906,-2.25933064,573.92,31.96,14.59,178.28,178.54,-5.42,81.90,1089.60,92.85,0.3745,-0.3887,-0.0156,1.0000,3 +90.203,51.18319622,-2.25918286,576.42,31.72,1.90,178.67,179.97,-4.33,88.48,1085.78,92.59,0.6020,0.2589,0.0687,1.0000,3 +90.672,51.18358563,-2.25914768,574.51,30.75,345.66,177.29,179.24,-2.69,88.26,1081.43,92.32,0.5529,0.3529,0.0000,1.0000,3 +91.156,51.18398480,-2.25927657,568.58,27.78,335.94,178.40,179.08,-1.90,75.09,1077.73,92.06,0.6725,0.0353,-0.0059,1.0000,3 +91.656,51.18434592,-2.25953919,563.64,26.62,317.34,176.45,178.81,-6.15,74.36,1074.42,91.81,0.7353,-0.1836,-0.0137,1.0000,3 +92.109,51.18464409,-2.25994008,565.00,35.01,304.55,175.02,176.53,-6.46,80.49,1071.05,91.56,0.3706,-0.1816,-0.0273,1.0000,3 +92.609,51.18488777,-2.26046563,567.23,47.15,303.47,177.61,177.92,-2.96,83.07,1067.62,91.30,0.0745,-0.0762,-0.0645,1.0000,3 +93.094,51.18510349,-2.26098477,564.19,51.70,301.99,179.98,180.07,-0.61,86.04,1063.92,91.03,0.2039,-0.2129,-0.0293,1.0000,3 +93.594,51.18532190,-2.26155975,554.81,49.37,297.74,182.51,182.20,1.72,89.18,1060.19,90.78,0.3314,0.3431,0.0000,1.0000,3 +94.094,51.18552082,-2.26214013,539.62,41.49,289.03,184.64,183.99,2.77,78.60,1056.35,90.52,0.5980,0.2490,0.0000,1.0000,3 +94.609,51.18566985,-2.26280649,521.18,36.79,278.44,186.08,185.54,1.66,70.25,1053.48,90.26,0.5725,0.1510,0.0000,1.0000,3 +95.125,51.18573749,-2.26351128,505.67,34.82,271.22,187.90,187.41,1.02,59.78,1049.64,90.00,0.4255,0.2628,0.0000,1.0000,3 +95.641,51.18574412,-2.26423808,493.99,33.68,266.95,190.15,189.67,0.64,41.33,1047.00,89.73,0.0000,0.5039,0.0000,1.0000,3 +96.141,51.18571685,-2.26492977,485.45,32.61,265.70,192.47,191.77,2.07,11.87,1043.36,89.49,0.0000,0.4431,0.0000,1.0000,3 +96.625,51.18568171,-2.26562519,476.43,32.16,264.91,194.69,193.80,2.56,-16.83,1040.52,89.25,0.0000,0.4451,0.0000,1.0000,3 +97.125,51.18564738,-2.26632909,465.45,31.15,265.02,196.81,195.73,2.91,-46.46,1036.75,89.00,0.0745,0.4431,0.0000,1.0000,3 +97.625,51.18561823,-2.26706310,450.87,25.91,269.21,198.95,197.67,2.09,-55.27,1033.81,88.74,0.4059,-0.1621,0.0000,1.0000,3 +98.141,51.18561845,-2.26781321,438.03,21.04,274.91,200.48,199.63,0.62,-43.39,1029.77,88.47,0.1784,-0.5039,-0.0117,1.0000,3 +98.703,51.18566298,-2.26863452,429.34,21.11,276.00,202.56,201.50,1.66,-1.59,1026.66,88.19,0.1922,-0.5176,-0.0352,1.0000,3 +99.234,51.18571335,-2.26940501,421.31,22.93,276.23,204.28,203.15,1.36,32.30,1022.74,87.93,0.1137,-0.0371,-0.0664,1.0000,3 +99.734,51.18575765,-2.27018117,412.22,26.75,274.19,205.87,204.69,0.41,36.78,1019.55,87.68,0.4235,0.0000,-0.0527,1.0000,3 +100.234,51.18578266,-2.27092872,406.87,30.42,268.76,206.45,205.80,-3.92,51.21,1015.70,87.42,0.3980,-0.4395,-0.0918,1.0000,3 +100.719,51.18576540,-2.27167770,409.77,33.96,260.99,206.36,205.77,-6.01,69.14,1012.37,87.16,0.5686,-0.1445,-0.0469,1.0000,3 +101.187,51.18569334,-2.27236106,416.21,36.41,250.19,205.50,205.13,-5.83,87.41,1008.57,86.90,0.6569,-0.0684,-0.0215,1.0000,3 +101.656,51.18554709,-2.27302685,420.33,40.41,230.65,201.76,202.99,-4.20,89.41,1004.48,86.64,0.7137,0.2471,0.0000,1.0000,3 +102.125,51.18527877,-2.27358644,420.14,39.18,210.65,196.98,198.61,-4.20,82.81,1000.97,86.38,0.6510,0.3059,0.0000,1.0000,3 +102.594,51.18493184,-2.27394271,420.10,36.41,191.21,193.04,194.83,-5.82,81.72,997.72,86.15,0.7431,0.0000,-0.0254,1.0000,3 +103.094,51.18449954,-2.27410113,422.94,35.00,174.34,189.40,190.45,-5.76,83.59,994.07,85.87,0.5137,0.0000,-0.0566,1.0000,3 +103.578,51.18407077,-2.27405507,425.35,37.38,167.47,190.07,190.00,-3.67,78.95,990.54,85.60,0.2902,0.3510,0.0000,1.0000,3 +104.062,51.18365531,-2.27391686,425.05,37.02,165.93,191.59,191.08,-1.45,52.43,986.77,85.33,0.1020,0.4627,0.0000,1.0000,3 +104.578,51.18321907,-2.27373206,422.40,34.29,164.12,193.22,192.59,-0.93,27.89,982.94,85.07,0.3118,0.0000,-0.0352,1.0000,3 +105.078,51.18278315,-2.27352521,421.61,33.69,161.92,194.29,193.75,-3.41,28.44,979.04,84.80,0.2432,-0.2988,-0.0371,1.0000,3 +105.609,51.18233537,-2.27328078,426.08,36.83,160.55,195.31,194.59,-3.09,33.73,975.10,84.52,-0.1484,0.2569,0.0000,1.0000,3 +106.078,51.18192641,-2.27303846,429.73,37.94,159.39,196.31,195.58,-2.27,24.15,971.23,84.25,-0.1172,0.3177,0.0000,1.0000,3 +106.562,51.18152132,-2.27279133,432.27,40.34,158.41,197.29,196.54,-1.69,8.62,966.96,83.99,-0.1113,0.3784,0.0000,1.0000,3 +107.031,51.18111438,-2.27252699,434.00,42.08,157.85,198.27,197.53,-1.37,-12.39,963.00,83.72,0.0000,0.3902,0.0000,1.0000,3 +107.469,51.18074697,-2.27229472,434.66,42.66,157.92,199.15,198.41,-1.13,-32.23,959.43,83.48,0.1118,0.3882,0.0000,1.0000,4 +107.953,51.18033147,-2.27204455,433.15,41.05,159.51,200.15,199.46,-1.34,-45.94,955.44,83.21,0.3608,0.0000,0.0098,1.0000,4 +108.422,51.17993393,-2.27181910,431.60,39.61,164.18,200.74,200.16,-3.05,-38.35,951.83,82.97,0.2059,-0.6230,0.0000,1.0000,4 +108.906,51.17948863,-2.27161999,434.76,42.91,165.16,201.49,200.61,-2.38,-5.21,948.11,82.70,0.0000,-0.3672,0.0000,1.0000,4 +109.375,51.17907665,-2.27144726,438.60,45.53,165.41,202.09,201.22,-2.31,9.75,944.31,82.44,0.0471,-0.3340,-0.0156,1.0000,4 +109.859,51.17863656,-2.27125974,442.06,41.06,165.26,202.70,201.88,-2.47,42.58,940.25,82.17,0.1961,-0.5762,-0.0273,1.0000,4 +110.328,51.17821785,-2.27106669,443.37,41.37,162.54,203.18,202.55,-2.93,68.69,936.32,81.91,0.3216,-0.3965,-0.0879,1.0000,4 +110.812,51.17778859,-2.27083637,442.29,40.23,152.23,202.85,202.76,-3.25,81.81,577.71,81.70,0.6804,0.0000,-0.0820,1.0000,4 +111.328,51.17738729,-2.27048375,440.43,38.38,125.30,193.60,196.66,-5.29,85.18,429.80,81.83,0.8117,-0.0449,-0.0781,1.0000,4 +111.859,51.17708375,-2.26987636,439.64,37.61,104.20,185.42,187.17,-4.76,82.54,428.69,81.99,0.5353,0.2804,-0.0176,1.0000,4 +112.375,51.17695972,-2.26919287,438.77,36.74,98.23,183.92,184.00,-3.23,66.32,427.34,82.14,0.2059,0.4157,0.0000,1.0000,4 +112.859,51.17690082,-2.26855336,437.83,30.79,93.60,182.81,182.81,-3.38,61.47,429.19,82.29,0.3216,-0.2598,-0.0742,1.0000,4 +113.328,51.17687786,-2.26791792,437.40,30.09,88.87,182.00,182.02,-3.54,64.79,431.22,82.44,0.3392,0.0118,-0.0449,1.0000,4 +113.781,51.17688855,-2.26729287,436.93,35.90,85.10,181.12,181.08,-2.89,60.57,431.33,82.58,0.1981,0.3686,0.0000,1.0000,4 +114.281,51.17692610,-2.26665890,435.70,40.66,81.45,180.43,180.38,-3.05,49.68,432.97,82.73,0.3451,0.1412,0.0000,1.0000,4 +114.781,51.17699508,-2.26599145,436.57,41.78,76.55,179.27,179.31,-5.35,43.69,434.41,82.88,0.3275,0.3138,0.0000,1.0000,4 +115.281,51.17709889,-2.26535529,443.63,47.91,72.05,177.69,177.43,-8.06,41.15,435.24,83.03,0.2196,-0.1973,-0.0293,1.0000,4 +115.781,51.17722906,-2.26474347,456.13,49.78,69.58,176.30,175.57,-8.05,44.28,436.42,83.18,0.1941,-0.3613,-0.0234,1.0000,4 +116.250,51.17736913,-2.26418263,469.32,35.68,65.80,174.80,174.03,-9.35,50.19,437.71,83.33,0.2353,-0.0332,-0.0645,1.0000,4 +116.719,51.17752803,-2.26364081,483.31,41.40,62.80,173.34,172.70,-8.97,68.37,438.21,83.47,0.2765,-0.4238,-0.0488,1.0000,4 +117.172,51.17769687,-2.26313455,494.37,45.52,56.62,171.94,171.90,-8.54,79.52,439.39,83.62,0.5823,0.0000,-0.0293,1.0000,4 +117.672,51.17791669,-2.26260994,504.21,46.23,44.24,169.20,170.06,-8.48,80.94,440.23,83.77,0.5804,0.0000,-0.0195,1.0000,4 +118.141,51.17817199,-2.26219449,511.66,37.81,32.21,166.48,167.74,-7.96,83.06,441.09,83.91,0.5235,0.1157,-0.0410,1.0000,4 +118.656,51.17848827,-2.26185559,517.23,28.15,23.86,165.05,165.91,-5.87,84.52,442.35,84.06,0.4392,-0.0547,-0.1543,1.0000,4 +119.109,51.17881765,-2.26160450,518.72,21.37,20.26,164.78,165.33,-3.37,76.69,443.27,84.21,0.0196,0.4235,0.0000,1.0000,4 +119.578,51.17916194,-2.26140854,515.96,15.72,19.21,165.21,165.44,-0.63,51.68,444.57,84.36,0.1039,0.5765,0.0000,1.0000,4 +120.094,51.17953087,-2.26121312,510.32,9.00,17.05,166.51,166.58,-0.55,4.15,871.19,84.41,0.3118,0.5922,0.0000,1.0000,4 +120.609,51.17990937,-2.26103005,509.65,8.82,18.42,168.14,168.47,-6.48,-20.05,966.76,84.17,0.2000,0.5000,0.0000,1.0000,4 +121.125,51.18029586,-2.26082269,518.83,16.84,19.16,169.71,169.50,-5.83,-49.89,965.11,83.89,0.3569,0.0236,-0.0449,1.0000,4 +121.578,51.18063131,-2.26062309,525.85,20.68,22.73,170.84,170.86,-6.04,-55.96,961.53,83.64,0.3216,0.0961,0.0000,1.0000,4 +122.047,51.18097365,-2.26038665,532.39,23.03,25.01,172.11,172.14,-4.97,-49.66,957.09,83.37,0.1196,-0.5078,-0.1445,1.0000,4 +122.578,51.18133546,-2.26010504,537.59,20.34,25.95,173.74,173.76,-2.84,-15.66,953.16,83.10,0.1922,-0.5371,-0.1582,1.0000,4 +123.094,51.18170773,-2.25980265,541.70,14.53,26.75,175.29,175.28,-4.51,17.97,948.88,82.85,0.3294,-0.6152,-0.1680,1.0000,4 +123.609,51.18207788,-2.25951534,549.08,12.28,22.29,176.03,176.12,-7.90,60.25,945.20,82.60,0.5294,0.0000,-0.0254,1.0000,4 +124.172,51.18252008,-2.25925169,561.70,17.47,7.52,174.97,175.54,-10.51,81.57,940.63,82.31,0.7588,-0.0586,-0.0430,1.0000,4 +124.687,51.18291768,-2.25916581,573.88,28.49,353.31,172.51,173.61,-7.57,95.82,937.11,82.06,0.1314,0.1118,-0.1484,1.0000,4 +125.187,51.18333134,-2.25921435,579.22,36.27,348.42,174.17,174.83,-3.52,92.14,932.61,81.80,0.3275,0.6157,0.0000,1.0000,4 +125.734,51.18375968,-2.25934265,577.34,36.89,340.14,175.31,175.98,-3.33,72.14,928.94,81.54,0.6314,-0.5000,-0.1660,1.0000,4 +126.281,51.18418681,-2.25959705,574.81,38.70,326.15,174.82,176.04,-3.06,86.63,924.50,81.26,0.4216,0.0334,-0.0898,1.0000,4 +126.859,51.18457017,-2.25997746,568.43,39.31,317.73,176.20,176.66,-0.48,81.74,920.74,80.98,0.4137,0.4627,0.0000,1.0000,4 +127.375,51.18489627,-2.26043673,558.02,37.17,308.24,177.33,177.68,-0.17,76.62,917.00,80.71,0.4588,0.0000,-0.0723,1.0000,4 +127.969,51.18519756,-2.26104853,545.04,33.00,297.68,178.50,178.69,0.14,76.90,913.29,80.41,0.4569,0.0000,-0.1406,1.0000,4 +128.531,51.18541515,-2.26169770,531.34,27.54,290.20,180.22,179.94,1.35,74.17,909.60,80.15,0.3667,0.2137,-0.0566,1.0000,4 +129.156,51.18558847,-2.26246111,513.83,22.55,282.48,182.37,181.80,1.58,65.36,906.18,79.85,0.4255,0.3020,-0.0352,1.0000,4 +129.719,51.18568797,-2.26322063,498.75,23.22,273.85,183.70,183.51,-0.50,56.95,901.91,79.56,0.4078,0.3451,-0.0059,1.0000,4 +130.375,51.18571803,-2.26408112,489.94,28.28,269.38,185.29,185.07,-0.53,41.08,898.37,79.25,0.0981,0.2824,-0.0566,1.0000,4 +130.906,51.18570514,-2.26485419,484.18,30.62,268.11,187.05,186.60,0.96,33.90,894.46,78.96,-0.1836,0.3726,-0.1113,1.0000,4 +131.453,51.18568126,-2.26560948,475.70,31.27,266.71,188.82,188.16,2.11,7.22,890.48,78.68,-0.1055,0.4686,-0.0723,1.0000,4 +131.937,51.18565452,-2.26629453,466.69,31.90,265.97,190.41,189.60,2.46,-26.61,887.07,78.43,0.0902,0.4980,0.0000,1.0000,4 +132.469,51.18563009,-2.26704097,455.28,30.16,269.46,191.81,191.13,0.44,-50.61,883.33,78.17,0.3314,0.3549,0.0000,1.0000,4 +132.984,51.18563182,-2.26776819,446.62,29.14,276.33,192.65,192.27,-1.17,-59.06,880.06,77.90,0.4235,0.0000,-0.0469,1.0000,4 +133.469,51.18568059,-2.26847984,441.87,31.87,279.36,193.47,192.94,-0.52,-50.54,876.25,77.64,0.0000,-0.4551,-0.1660,1.0000,4 +133.969,51.18575473,-2.26915386,436.90,35.10,280.44,194.63,193.89,1.02,-10.30,872.85,77.39,0.1647,-0.6094,-0.1680,1.0000,4 +134.484,51.18584221,-2.26988019,430.54,40.10,280.68,195.73,194.99,0.13,48.15,868.96,77.12,0.2628,-0.6016,-0.1602,1.0000,4 +134.984,51.18591725,-2.27059363,423.56,44.29,272.56,196.10,195.81,-2.07,72.60,865.31,76.85,0.6372,0.0000,-0.0625,1.0000,4 +135.469,51.18593626,-2.27133093,418.23,46.20,261.12,194.95,195.05,-3.07,73.88,861.34,76.57,0.5078,0.0687,-0.0371,1.0000,4 +135.953,51.18587223,-2.27200296,415.80,37.16,250.16,194.15,194.37,-4.02,74.69,857.75,76.30,0.5431,0.0000,-0.0391,1.0000,4 +136.422,51.18573293,-2.27262908,415.76,35.79,239.61,193.05,193.28,-4.70,76.08,854.08,76.03,0.5490,0.0000,-0.0586,1.0000,4 +136.969,51.18548977,-2.27328599,418.19,38.34,220.17,189.76,191.18,-7.09,80.46,849.88,75.74,0.7510,-0.2520,-0.0508,1.0000,4 +137.469,51.18517241,-2.27372341,423.58,42.22,198.34,183.59,185.68,-7.05,87.75,846.22,75.47,0.6961,-0.1758,-0.0723,1.0000,4 +137.953,51.18477376,-2.27396673,427.32,40.80,183.43,180.77,181.85,-4.38,90.67,841.79,75.18,0.5882,0.0588,-0.0605,1.0000,4 +138.453,51.18437469,-2.27402226,425.39,37.22,172.76,180.33,180.82,-1.67,84.87,837.64,74.92,0.3980,0.3706,-0.0020,1.0000,4 +138.906,51.18398967,-2.27396395,419.18,30.92,168.01,181.41,181.18,-0.62,68.31,833.91,74.66,0.4569,0.2765,-0.0488,1.0000,4 +139.391,51.18358942,-2.27382848,412.14,23.94,163.53,182.41,182.09,-0.79,47.37,829.98,74.39,0.2608,0.4294,0.0000,1.0000,4 +139.906,51.18317018,-2.27361832,407.37,19.30,160.53,183.61,183.28,-2.19,26.25,825.95,74.11,0.2510,0.4824,0.0000,1.0000,4 +140.375,51.18278776,-2.27339586,408.29,20.35,159.29,184.29,183.85,-3.40,4.67,822.50,73.84,0.1902,0.2647,0.0000,1.0000,4 +140.859,51.18241722,-2.27316585,413.09,24.07,158.98,184.91,184.30,-3.00,0.22,818.54,73.57,0.1686,0.1079,-0.0938,1.0000,4 +141.312,51.18204976,-2.27295071,417.83,26.69,158.95,185.43,184.85,-2.94,-0.16,814.80,73.32,0.1490,0.0392,-0.0820,1.0000,4 +141.797,51.18166114,-2.27270895,422.41,30.55,158.93,185.98,185.43,-2.53,-0.17,810.78,73.05,0.1236,0.0353,-0.0664,1.0000,4 +142.281,51.18128117,-2.27247250,426.16,34.21,158.91,186.59,186.05,-2.11,-0.17,806.47,72.78,0.1510,0.0373,-0.0117,1.0000,4 diff --git a/track_data/flight_20260901_183125_021169.csv b/track_data/flight_20260901_183125_021169.csv new file mode 100644 index 00000000..88f52417 --- /dev/null +++ b/track_data/flight_20260901_183125_021169.csv @@ -0,0 +1,290 @@ +# aircraft_title=Extra 300S Paint2 +# recorded_at=2026-09-01T18:31:25 +# laps_s=28.928,34.410,34.330,34.021 +timestamp_s,latitude,longitude,altitude_ft,agl_ft,heading_deg,airspeed_kts,groundspeed_kts,pitch_deg,bank_deg,torque,health_pct,stick_pitch,stick_roll,stick_yaw,stick_throttle,lap +0.344,51.18977110,-2.27596085,982.91,619.39,166.16,183.40,182.17,6.91,0.13,1075.82,100.00,0.0000,0.0000,-0.0801,1.0000,1 +0.829,51.18935247,-2.27579753,960.57,591.64,166.06,187.16,185.57,7.82,0.15,1169.11,100.00,-0.0195,0.0000,0.0000,1.0000,1 +1.313,51.18896304,-2.27564437,937.47,566.28,166.02,190.58,188.67,8.57,0.16,1173.65,100.00,-0.1719,0.0000,-0.0156,1.0000,1 +1.797,51.18855090,-2.27547991,910.91,535.98,165.99,194.36,191.84,9.45,0.17,1174.55,100.00,-0.1289,0.0000,-0.0234,1.0000,1 +2.344,51.18812198,-2.27531284,881.49,503.18,165.96,198.21,195.09,10.61,0.18,1176.53,100.00,-0.1875,0.0000,-0.0488,1.0000,1 +2.829,51.18764067,-2.27512263,844.76,463.46,165.93,202.53,198.33,11.54,0.18,1176.70,100.00,-0.1035,0.0000,-0.0566,1.0000,1 +3.297,51.18722419,-2.27495816,810.31,428.88,165.94,206.35,201.46,11.90,0.19,1178.63,100.00,-0.1406,0.0000,-0.0566,1.0000,1 +3.782,51.18678059,-2.27478276,772.53,391.09,165.93,210.13,204.77,12.25,0.19,1178.81,100.00,-0.1406,0.0000,-0.0586,1.0000,1 +4.250,51.18634070,-2.27460473,733.46,351.96,165.92,214.03,208.03,12.55,0.20,1180.41,100.00,-0.1426,0.0000,-0.0566,1.0000,1 +4.719,51.18589157,-2.27442885,694.72,312.47,165.88,217.50,210.98,13.30,0.20,1181.20,100.00,-0.1797,0.0000,-0.0605,1.0000,1 +5.188,51.18545387,-2.27425513,652.69,271.06,165.87,221.27,213.53,13.87,0.21,1181.04,100.00,0.0000,0.0530,0.0000,1.0000,1 +5.672,51.18498326,-2.27406644,607.06,222.02,165.88,224.89,216.73,13.93,0.21,1183.91,100.00,0.0706,0.0079,-0.0703,1.0000,1 +6.172,51.18449959,-2.27387385,559.60,169.99,165.90,228.72,220.03,13.45,0.22,1183.12,100.00,0.2118,0.0628,-0.0625,1.0000,1 +6.672,51.18399940,-2.27367307,511.49,122.77,166.02,231.89,224.50,10.83,0.21,1187.26,100.00,0.2686,0.0373,-0.0977,1.0000,1 +7.157,51.18349821,-2.27347223,473.66,84.43,166.06,234.66,229.31,8.40,-0.35,1186.55,100.00,0.1412,0.0098,-0.1094,1.0000,1 +7.625,51.18302524,-2.27328378,445.91,56.52,166.15,236.58,232.49,5.55,-0.44,1190.29,100.00,0.3039,0.1157,-0.0410,1.0000,1 +8.094,51.18252926,-2.27308737,425.27,35.98,166.20,238.14,235.30,2.94,-0.44,1189.74,100.00,0.2333,0.0863,-0.0117,1.0000,1 +8.579,51.18200962,-2.27288171,413.51,21.51,166.24,239.21,236.93,0.76,-0.45,1191.88,100.00,0.1883,0.0000,-0.0762,1.0000,1 +9.079,51.18147856,-2.27267294,408.54,16.47,166.26,240.04,237.88,-0.74,-0.45,1191.46,100.00,0.1471,0.0000,-0.0215,1.0000,1 +9.563,51.18096080,-2.27247486,408.33,16.36,166.13,240.65,238.44,-0.99,2.19,1191.94,100.00,0.1412,-0.1504,-0.1641,1.0000,1 +10.016,51.18047039,-2.27227497,409.72,17.82,166.04,241.11,238.89,-1.97,5.97,1191.62,100.00,0.1628,-0.2520,-0.1465,1.0000,1 +10.516,51.17995563,-2.27207219,413.90,22.09,165.71,241.44,239.15,-2.23,20.87,1191.30,100.00,0.1137,-0.3867,-0.1797,1.0000,1 +11.000,51.17941212,-2.27184526,418.65,26.89,164.94,241.77,239.51,-2.31,42.59,1191.01,100.00,0.1667,-0.3359,-0.1816,1.0000,1 +11.469,51.17891679,-2.27162217,421.91,27.33,162.52,241.95,239.84,-2.81,65.31,1190.38,100.00,0.3059,-0.2344,-0.1758,1.0000,1 +11.891,51.17846498,-2.27138470,424.21,22.33,158.18,242.02,239.99,-2.66,76.65,1190.14,100.00,0.2294,-0.2285,-0.1816,1.0000,1 +12.344,51.17800838,-2.27109410,425.10,23.09,151.45,241.91,240.11,-2.43,80.98,1189.87,100.00,0.4510,0.0000,-0.0684,1.0000,1 +12.782,51.17759401,-2.27072600,424.76,22.76,138.51,240.27,239.16,-3.20,82.27,1189.64,100.00,0.6118,0.0902,-0.0664,1.0000,1 +13.266,51.17719222,-2.27015262,425.66,23.80,118.65,235.71,235.50,-4.37,82.19,1189.75,100.00,0.5706,0.2314,-0.0352,1.0000,1 +13.719,51.17693539,-2.26945936,428.96,27.12,106.84,233.55,232.50,-4.48,77.91,1190.05,100.00,0.4314,0.4883,0.0000,1.0000,1 +14.172,51.17678647,-2.26872503,434.54,32.72,97.04,232.54,231.27,-6.01,70.47,1190.32,100.00,0.5235,0.0000,-0.1797,1.0000,1 +14.657,51.17672678,-2.26790907,446.58,32.88,85.35,230.51,228.72,-8.40,71.30,1190.02,100.00,0.4431,-0.0234,-0.1855,1.0000,1 +15.094,51.17675900,-2.26717813,463.03,57.78,79.83,229.92,227.29,-8.03,75.49,1190.19,100.00,0.1196,-0.3320,-0.1973,1.0000,1 +15.547,51.17684289,-2.26642527,479.45,84.97,78.33,230.02,227.76,-6.14,84.85,1188.98,100.00,0.1530,-0.2441,-0.1855,1.0000,1 +16.000,51.17693888,-2.26566622,490.97,96.48,74.41,230.23,228.48,-4.71,87.67,1188.89,100.00,0.3235,0.1157,-0.0039,1.0000,1 +16.469,51.17706100,-2.26497083,497.64,95.83,69.05,230.34,229.00,-3.26,84.23,1187.83,100.00,0.3510,0.3529,0.0000,1.0000,1 +16.938,51.17724859,-2.26418202,500.22,68.39,61.44,230.30,229.21,-2.92,76.35,1187.47,100.00,0.3255,0.3726,0.0000,1.0000,1 +17.375,51.17746124,-2.26354260,501.45,57.76,56.43,230.69,229.41,-2.70,70.96,1187.39,100.00,0.3784,-0.1602,-0.1992,1.0000,1 +17.891,51.17775759,-2.26284671,503.98,49.98,45.62,230.11,229.20,-4.84,72.17,1187.23,100.00,0.4883,-0.2070,-0.1680,1.0000,1 +18.375,51.17812206,-2.26224960,511.06,39.36,34.08,228.72,227.84,-6.39,75.71,1187.25,100.00,0.5098,-0.1367,-0.1680,1.0000,1 +18.860,51.17854488,-2.26179276,522.38,30.93,22.94,227.33,226.14,-6.44,81.54,1187.16,100.00,0.4333,-0.0977,-0.1914,1.0000,1 +19.297,51.17896693,-2.26149611,532.34,34.16,17.92,227.23,225.86,-4.81,82.12,1187.33,100.00,0.0588,0.1471,-0.1426,1.0000,1 +19.766,51.17942465,-2.26125482,539.68,38.65,16.61,227.95,226.58,-2.80,54.86,1186.28,100.00,-0.1875,0.5098,-0.0176,1.0000,1 +20.250,51.17991490,-2.26102357,542.86,41.87,15.59,228.93,227.62,0.42,-6.62,1186.29,100.00,0.0000,0.6529,0.0000,1.0000,1 +20.719,51.18039137,-2.26080354,539.99,38.43,17.23,229.85,228.65,-1.75,-52.89,1185.64,100.00,0.3628,0.2647,-0.0352,1.0000,1 +21.172,51.18085472,-2.26055920,538.94,34.36,22.26,230.30,229.18,-2.95,-45.50,1185.58,100.00,0.1393,-0.7012,-0.2285,1.0000,1 +21.657,51.18133567,-2.26023943,542.94,31.84,23.16,231.10,229.65,-3.23,21.68,1185.71,100.00,0.2392,-0.6270,-0.2266,1.0000,1 +22.125,51.18179778,-2.25993660,551.25,30.69,18.70,230.91,229.41,-7.15,55.00,1185.69,100.00,0.2549,-0.4707,-0.2305,1.0000,1 +22.594,51.18227053,-2.25969223,566.29,37.68,14.43,230.73,228.62,-6.97,67.91,1185.14,100.00,0.1334,-0.4863,-0.2324,1.0000,1 +23.047,51.18272540,-2.25951169,580.15,43.04,11.41,230.83,229.03,-5.84,87.83,1185.06,100.00,0.2667,-0.3164,-0.1855,1.0000,1 +23.500,51.18318935,-2.25936228,590.01,49.12,4.78,230.60,229.52,-4.05,91.60,1184.11,100.00,0.4196,0.2843,0.0000,1.0000,1 +23.938,51.18367653,-2.25929697,593.98,52.52,353.61,229.73,229.30,-2.52,90.93,1183.47,100.00,0.6451,0.0000,-0.0723,1.0000,1 +24.407,51.18414324,-2.25938537,592.24,52.60,331.63,224.62,226.18,-1.48,90.26,1183.58,100.00,0.5373,0.0000,-0.1484,1.0000,1 +24.875,51.18459465,-2.25972954,584.74,51.18,314.95,222.22,222.77,-0.09,86.70,1183.65,100.00,0.4980,0.2726,-0.0039,1.0000,1 +25.375,51.18495719,-2.26026488,574.14,50.32,303.50,221.46,220.80,0.78,82.80,1182.80,99.90,0.5549,0.1922,-0.0684,1.0000,1 +25.875,51.18524837,-2.26096128,560.46,47.07,296.99,222.68,221.30,2.06,78.81,1179.49,99.60,0.3784,-0.1035,-0.1250,1.0000,1 +26.407,51.18547855,-2.26168216,545.02,40.87,286.31,223.64,222.34,1.56,78.48,1175.32,99.29,0.5137,0.0000,-0.0391,1.0000,1 +26.875,51.18563217,-2.26248059,529.79,38.78,279.28,224.56,222.94,2.13,75.48,1172.46,99.00,0.4569,0.2745,0.0000,1.0000,1 +27.391,51.18571259,-2.26327393,514.39,39.63,273.63,226.05,224.22,2.13,63.99,1168.32,98.71,0.3412,0.0000,-0.0762,1.0000,1 +27.875,51.18574155,-2.26411195,500.32,38.84,267.56,227.13,225.65,0.71,58.83,1165.56,98.41,0.1745,0.2373,0.0000,1.0000,1 +28.360,51.18571944,-2.26492871,490.07,37.24,265.92,228.75,226.95,2.02,39.19,1161.26,98.12,0.0000,0.5373,0.0000,1.0000,1 +28.875,51.18567659,-2.26576693,478.88,36.61,264.40,230.28,228.35,2.55,-4.10,1158.07,97.82,0.0000,0.5392,0.0000,1.0000,1 +29.407,51.18562789,-2.26663108,466.86,37.09,264.51,231.71,229.64,2.02,-50.63,1110.86,97.52,0.2843,0.3941,0.0000,1.0000,1 +29.891,51.18559284,-2.26749185,455.14,35.26,274.12,230.69,229.47,-1.45,-55.31,557.63,97.62,0.3628,-0.3652,0.0000,1.0000,1 +30.407,51.18562148,-2.26830829,453.41,42.44,276.24,229.25,227.72,-1.24,-24.70,556.42,97.78,-0.0723,-0.4961,-0.0059,1.0000,1 +30.875,51.18568021,-2.26914969,453.59,52.15,276.89,227.33,225.80,0.93,8.02,560.10,97.95,-0.2461,-0.4414,-0.0488,1.0000,1 +31.360,51.18573876,-2.26995279,448.00,58.73,277.42,225.91,224.24,1.40,46.43,561.61,98.11,0.1726,-0.5195,-0.0625,1.0000,1 +31.813,51.18579032,-2.27069221,438.41,58.89,274.34,225.07,223.37,0.87,75.65,1085.02,98.12,0.4392,0.0981,-0.0605,1.0000,1 +32.266,51.18581720,-2.27143858,427.09,52.29,263.38,224.19,223.11,-0.51,71.54,1163.81,97.84,0.5059,0.2981,-0.0117,1.0000,1 +32.704,51.18577052,-2.27215987,420.07,40.11,252.27,223.25,222.53,-2.74,71.37,1159.70,97.56,0.6176,-0.1602,-0.1699,1.0000,1 +33.141,51.18563624,-2.27283397,419.61,39.75,234.98,219.83,220.39,-6.61,76.66,1154.54,97.27,0.7549,0.0000,-0.0488,1.0000,1 +33.594,51.18538325,-2.27342873,427.47,48.14,211.52,212.66,213.92,-9.63,82.98,1150.14,96.99,0.7431,-0.2734,-0.1074,1.0000,1 +34.079,51.18501470,-2.27382559,440.98,58.64,191.85,207.39,207.86,-7.98,92.41,1146.26,96.69,0.6510,-0.2578,-0.1621,1.0000,1 +34.563,51.18454633,-2.27399970,450.37,62.42,176.96,204.75,205.09,-3.64,96.19,676.85,96.48,0.3275,0.4196,0.0000,1.0000,1 +35.047,51.18408343,-2.27399098,449.73,61.59,172.19,204.83,204.26,-0.90,90.36,549.30,96.64,0.4020,0.1177,-0.1504,1.0000,1 +35.500,51.18367019,-2.27390782,443.50,55.18,167.32,203.54,202.69,0.80,77.35,550.20,96.80,0.2118,0.3020,-0.0957,1.0000,1 +35.938,51.18326341,-2.27376365,433.59,45.18,164.03,203.33,202.22,1.56,63.04,552.87,96.97,0.2314,0.4118,0.0000,1.0000,1 +36.407,51.18286394,-2.27358038,422.91,34.46,161.14,202.97,201.83,1.39,42.73,552.54,97.13,0.2373,0.5549,0.0000,1.0000,1 +36.891,51.18241548,-2.27332798,413.83,24.53,158.87,202.42,201.53,-0.33,9.91,555.07,97.30,0.2706,0.4039,0.0000,1.0000,1 +37.360,51.18201670,-2.27308174,411.84,20.33,158.90,201.65,200.91,-2.74,-14.96,556.60,97.47,0.2157,0.3902,0.0000,1.0000,1 +37.844,51.18160252,-2.27283293,415.28,23.44,159.53,200.68,199.82,-2.85,-40.56,557.89,97.63,0.0000,0.2726,0.0000,1.0000,1 +38.344,51.18115199,-2.27258244,418.07,26.12,161.00,199.74,199.00,-2.15,-45.57,559.59,97.82,0.1941,-0.0410,0.0000,1.0000,1 +38.797,51.18076048,-2.27237636,418.30,26.28,162.68,199.11,198.41,-1.62,-43.86,560.70,97.98,0.1765,-0.2109,0.0000,1.0000,2 +39.250,51.18035157,-2.27219228,417.80,25.83,164.90,198.49,197.79,-2.05,-13.85,561.98,98.14,0.1000,-0.4219,-0.0039,1.0000,2 +39.704,51.17996477,-2.27202059,419.68,27.78,165.33,197.85,197.10,-2.26,-1.70,563.40,98.31,0.1843,-0.0117,-0.0117,1.0000,2 +40.157,51.17955098,-2.27185045,423.11,31.37,165.37,197.06,196.27,-3.04,0.21,564.66,98.46,0.0647,-0.1191,-0.0234,1.0000,2 +40.657,51.17911682,-2.27166803,428.47,36.69,165.52,196.21,195.44,-2.86,27.36,566.11,98.64,0.0334,-0.3750,-0.1211,1.0000,2 +41.110,51.17871963,-2.27149535,432.02,33.73,164.37,195.46,194.89,-3.34,60.70,567.32,98.81,0.3275,-0.4395,-0.1484,1.0000,2 +41.563,51.17833736,-2.27131223,433.08,31.07,160.07,194.68,194.35,-3.24,73.14,568.63,98.97,0.3765,-0.2266,-0.1855,1.0000,2 +42.000,51.17797027,-2.27109643,432.23,30.18,151.12,194.37,194.39,-3.60,77.48,1124.09,98.93,0.6961,0.0000,-0.1445,1.0000,2 +42.469,51.17759397,-2.27076277,431.65,29.65,137.43,192.89,193.39,-4.17,81.08,1177.74,98.64,0.4784,0.1196,-0.0664,1.0000,2 +42.969,51.17727482,-2.27029802,431.37,29.34,123.16,192.56,193.19,-4.23,80.34,1172.42,98.31,0.6686,0.3647,0.0000,1.0000,2 +43.469,51.17702167,-2.26971443,431.93,30.03,104.43,189.41,191.07,-6.20,79.17,797.32,98.06,0.5294,0.2706,-0.0703,1.0000,2 +43.954,51.17689428,-2.26905619,435.70,33.85,95.96,188.55,188.64,-5.48,76.12,566.87,98.20,0.3549,-0.1758,-0.1855,1.0000,2 +44.407,51.17684931,-2.26841725,439.46,31.24,89.01,187.03,187.04,-4.82,77.13,563.12,98.37,0.3706,0.1393,-0.1191,1.0000,2 +44.875,51.17685050,-2.26779440,441.49,31.87,84.93,187.23,187.08,-3.38,74.36,1080.06,98.40,0.1745,0.4157,0.0000,1.0000,2 +45.360,51.17688733,-2.26713127,440.92,40.43,81.18,188.79,188.52,-2.49,62.11,1169.94,98.10,0.2333,0.4902,0.0000,1.0000,2 +45.860,51.17695827,-2.26643799,439.56,44.55,77.22,190.68,190.34,-2.77,45.37,1164.83,97.77,0.2255,0.4569,0.0000,1.0000,2 +46.375,51.17706319,-2.26572933,440.92,45.70,74.43,192.61,192.12,-4.22,30.78,1159.50,97.47,0.3726,0.2589,-0.0273,1.0000,2 +46.875,51.17718823,-2.26504989,447.98,46.70,71.78,193.86,192.98,-6.32,34.94,1154.58,97.15,0.1079,-0.3965,-0.2051,1.0000,2 +47.329,51.17732401,-2.26441930,459.24,38.87,69.94,195.26,194.18,-5.92,47.02,1150.01,96.86,0.3177,-0.1992,-0.1855,1.0000,2 +47.782,51.17746409,-2.26383239,469.30,31.25,66.38,195.61,194.70,-7.12,56.06,589.86,96.80,0.3451,-0.2773,-0.1836,1.0000,2 +48.235,51.17763698,-2.26322304,481.41,35.65,59.23,193.93,193.25,-9.05,67.46,555.26,96.97,0.5431,-0.2305,-0.1523,1.0000,2 +48.704,51.17784941,-2.26266063,496.31,43.20,46.61,190.48,190.18,-10.80,79.89,551.73,97.14,0.5588,-0.4062,-0.1152,1.0000,2 +49.204,51.17813854,-2.26215857,512.86,45.63,31.75,185.91,186.07,-10.08,87.13,553.30,97.31,0.5412,-0.2168,-0.0898,1.0000,2 +49.657,51.17846919,-2.26181007,525.52,39.98,25.81,184.24,184.11,-7.25,90.84,555.75,97.48,0.4843,0.0000,-0.1699,1.0000,2 +50.110,51.17880093,-2.26154043,532.95,34.11,18.55,183.21,183.51,-4.73,92.33,633.34,97.64,0.3824,0.1098,-0.1172,1.0000,2 +50.547,51.17915649,-2.26132931,534.64,32.24,16.88,184.45,184.48,-2.22,74.64,1146.88,97.43,-0.0234,0.4902,-0.0176,1.0000,2 +51.032,51.17954452,-2.26114309,531.86,28.32,15.94,186.63,186.44,0.24,41.23,1152.44,97.13,0.0000,0.5647,0.0000,1.0000,2 +51.625,51.18004051,-2.26092910,524.61,22.31,14.44,189.77,189.35,-0.06,-13.91,1146.15,96.77,0.3647,0.4333,-0.0332,1.0000,2 +52.094,51.18044528,-2.26075059,521.79,19.25,16.87,191.61,191.43,-3.19,-32.30,1141.69,96.49,0.4157,0.2471,0.0000,1.0000,2 +52.594,51.18087810,-2.26053215,526.08,20.97,20.59,193.02,192.65,-5.45,-39.84,1136.97,96.18,0.1255,-0.1367,-0.0918,1.0000,2 +53.079,51.18126727,-2.26029054,534.50,23.62,21.40,194.54,193.91,-4.19,-26.08,1132.62,95.88,0.0000,-0.5117,-0.1699,1.0000,2 +53.547,51.18167469,-2.26002884,541.71,21.72,22.27,196.10,195.48,-3.20,3.16,1128.16,95.58,0.0941,-0.5645,-0.1855,1.0000,2 +54.032,51.18206600,-2.25977012,547.87,18.05,21.82,197.46,196.97,-4.41,41.65,1123.26,95.30,0.2667,-0.5879,-0.1738,1.0000,2 +54.485,51.18245266,-2.25953248,554.61,16.55,17.31,197.86,197.51,-6.44,59.58,580.25,95.21,0.3628,0.0000,-0.1680,1.0000,2 +54.969,51.18288219,-2.25933837,564.04,22.04,10.81,196.23,195.91,-7.27,70.82,539.85,95.38,0.3490,-0.3867,-0.1816,1.0000,2 +55.422,51.18329155,-2.25921556,573.94,30.63,1.57,194.22,194.29,-7.20,82.17,536.34,95.55,0.6118,-0.2461,-0.0879,1.0000,2 +55.875,51.18368351,-2.25919390,581.73,39.23,347.42,190.11,191.21,-6.82,86.70,538.35,95.72,0.6235,-0.1367,-0.0742,1.0000,2 +56.375,51.18410577,-2.25932625,587.51,47.53,331.80,186.12,187.68,-5.14,89.94,540.40,95.90,0.5686,-0.2246,-0.0703,1.0000,2 +56.844,51.18448964,-2.25963560,587.96,52.81,318.20,183.30,184.59,-2.43,91.91,540.42,96.08,0.5627,0.0804,0.0000,1.0000,2 +57.329,51.18479799,-2.26003947,582.00,54.47,308.45,181.92,182.60,0.35,91.52,542.11,96.24,0.5314,0.2863,0.0000,1.0000,2 +57.782,51.18504554,-2.26050504,570.63,50.47,301.26,181.74,181.48,2.42,85.43,543.64,96.42,0.3569,0.1902,-0.0332,1.0000,2 +58.266,51.18525792,-2.26105389,553.19,40.76,295.29,182.25,181.24,3.59,74.02,545.15,96.59,0.5353,0.1216,-0.0508,1.0000,2 +58.750,51.18542833,-2.26161158,534.82,29.61,286.89,182.27,181.39,2.74,68.26,546.89,96.75,0.4255,0.2373,0.0000,1.0000,2 +59.204,51.18555077,-2.26223688,517.51,21.34,281.61,183.19,182.23,2.84,64.96,929.46,96.86,0.3451,0.2353,0.0000,1.0000,2 +59.704,51.18563157,-2.26289243,500.59,18.33,275.18,185.51,184.64,1.86,59.18,1143.42,96.60,0.3353,0.4431,0.0000,1.0000,2 +60.266,51.18567110,-2.26361200,485.62,17.10,270.88,188.26,187.57,0.97,38.92,1142.02,96.28,0.2981,0.3431,0.0000,1.0000,2 +60.735,51.18567380,-2.26429624,477.15,17.66,268.81,190.57,190.02,0.45,22.83,737.45,96.05,0.0687,0.4294,0.0000,1.0000,2 +61.250,51.18565993,-2.26500936,471.08,19.34,267.83,191.21,190.65,1.02,0.44,550.67,96.19,0.0196,0.3451,0.0000,1.0000,2 +61.797,51.18564299,-2.26572240,464.76,22.45,267.60,191.20,190.57,1.23,-18.73,545.14,96.36,0.1079,0.3196,0.0000,1.0000,2 +62.485,51.18562626,-2.26667768,454.57,25.80,268.52,191.13,190.42,1.64,-36.25,547.64,96.58,0.1941,0.2824,0.0000,1.0000,2 +63.032,51.18562331,-2.26746681,443.79,23.49,271.33,191.12,190.36,0.96,-43.10,549.85,96.77,0.3216,0.0275,0.0000,1.0000,2 +63.672,51.18564735,-2.26836506,433.54,22.45,274.73,192.05,191.36,0.68,-40.33,1066.35,96.84,0.1196,-0.3789,-0.0117,1.0000,2 +64.313,51.18570179,-2.26926135,423.38,23.10,276.15,194.87,193.84,1.81,-4.19,1147.64,96.47,0.2569,-0.3262,-0.0352,1.0000,2 +64.922,51.18576474,-2.27015186,413.79,28.44,276.13,197.64,196.77,-0.09,26.64,1140.58,96.09,0.2471,-0.4238,-0.0742,1.0000,2 +65.469,51.18580972,-2.27094032,409.06,32.97,273.44,199.66,198.93,-1.58,59.83,1135.89,95.77,0.3372,-0.3965,-0.0742,1.0000,2 +66.000,51.18582768,-2.27178012,404.54,27.54,263.78,200.98,200.66,-3.22,71.96,1130.72,95.43,0.5255,0.0000,-0.0449,1.0000,2 +66.547,51.18576982,-2.27258024,403.09,23.10,250.11,200.68,200.81,-4.94,75.83,737.87,95.17,0.6745,0.0000,-0.0488,1.0000,2 +67.016,51.18562746,-2.27322475,405.94,26.18,230.61,195.87,197.30,-7.66,80.06,541.92,95.30,0.7294,-0.1367,-0.0625,1.0000,2 +67.563,51.18533901,-2.27380582,414.63,34.84,206.83,187.25,189.25,-8.85,84.88,539.78,95.47,0.7216,-0.1406,-0.0410,1.0000,2 +68.110,51.18493521,-2.27416086,424.43,44.42,185.39,180.45,182.26,-7.60,88.60,542.31,95.66,0.6686,0.0000,-0.0449,1.0000,2 +68.704,51.18443911,-2.27425073,430.07,42.12,168.64,176.41,177.50,-4.37,90.61,544.16,95.85,0.4216,0.1490,-0.0156,1.0000,2 +69.266,51.18395433,-2.27412558,427.50,39.22,165.88,178.96,178.80,-0.70,68.62,1132.71,95.58,0.1902,0.4745,0.0000,1.0000,2 +69.766,51.18356708,-2.27396058,420.70,32.41,162.66,181.57,181.11,-0.34,47.55,1130.40,95.27,0.3686,0.3157,0.0000,1.0000,2 +70.282,51.18314441,-2.27374410,414.59,26.43,160.53,182.70,182.30,-0.52,20.58,549.38,95.36,0.3628,0.2157,-0.0137,1.0000,2 +70.782,51.18274444,-2.27351453,412.66,24.69,159.31,182.36,182.01,-2.18,6.28,539.42,95.54,0.1569,0.2392,0.0000,1.0000,2 +71.250,51.18237420,-2.27328413,414.36,25.14,158.93,181.93,181.51,-1.80,-1.35,542.24,95.70,0.0647,0.1843,0.0000,1.0000,2 +71.688,51.18204243,-2.27307741,415.66,24.32,158.82,182.09,181.66,-1.37,-7.39,543.50,95.86,0.0059,0.2863,0.0000,1.0000,2 +72.157,51.18166428,-2.27285636,416.07,24.06,158.94,181.83,181.43,-1.07,-17.98,544.23,96.04,0.1451,0.2765,0.0000,1.0000,2 +72.625,51.18130891,-2.27264251,415.36,23.32,160.01,181.79,181.45,-1.81,-23.40,546.69,96.20,0.3196,0.1196,0.0000,1.0000,2 +73.188,51.18085765,-2.27239531,416.86,24.99,162.22,181.46,181.12,-3.61,-24.58,547.80,96.40,0.2412,0.1510,0.0000,1.0000,3 +73.657,51.18047609,-2.27220776,421.67,29.77,163.62,180.97,180.52,-4.26,-25.55,548.77,96.57,0.1706,0.0569,0.0000,1.0000,3 +74.141,51.18009652,-2.27203546,427.83,36.05,164.41,180.55,180.08,-3.66,-26.34,550.48,96.75,0.0392,0.0000,0.0000,1.0000,3 +74.625,51.17970172,-2.27187400,432.72,40.87,165.18,180.31,179.93,-2.77,-23.00,551.44,96.92,0.0373,-0.3496,0.0000,1.0000,3 +75.141,51.17928393,-2.27171049,435.91,44.07,166.12,180.15,179.81,-1.86,-4.60,552.85,97.10,0.1039,-0.3945,0.0000,1.0000,3 +75.610,51.17891112,-2.27156743,437.39,42.27,166.62,180.12,179.80,-1.68,22.60,554.25,97.27,0.2314,-0.6133,-0.0156,1.0000,3 +76.079,51.17852584,-2.27141101,438.10,36.37,164.08,179.85,179.77,-3.95,53.86,555.55,97.44,0.4647,-0.2109,-0.0605,1.0000,3 +76.547,51.17814171,-2.27122359,439.81,37.85,157.03,178.87,179.15,-5.47,69.01,556.94,97.62,0.3902,-0.0586,-0.0664,1.0000,3 +77.000,51.17780170,-2.27099181,442.17,40.21,148.78,177.96,178.43,-5.45,80.25,558.43,97.78,0.5647,-0.4277,-0.0391,1.0000,3 +77.469,51.17747428,-2.27067943,443.18,41.15,133.86,175.44,176.91,-4.56,88.43,559.75,97.95,0.8039,0.0549,-0.0059,1.0000,3 +78.000,51.17718295,-2.27020051,439.91,37.71,113.49,170.53,172.46,-2.52,88.16,561.34,98.13,0.6628,0.2726,0.0000,1.0000,3 +78.500,51.17699935,-2.26963940,432.31,30.03,103.49,171.07,171.57,-1.32,73.03,1148.92,98.02,0.4039,0.4784,0.0000,1.0000,3 +78.969,51.17690274,-2.26903981,424.95,22.69,97.51,173.44,173.57,-2.31,56.00,1165.85,97.72,0.5274,0.0216,-0.0801,1.0000,3 +79.469,51.17685437,-2.26844266,422.58,14.60,88.69,174.51,175.13,-6.68,56.02,1161.43,97.41,0.3922,-0.0273,-0.1172,1.0000,3 +79.954,51.17686068,-2.26780361,428.08,20.21,85.01,176.55,176.30,-6.04,57.83,1156.30,97.10,0.2745,0.0000,-0.0586,1.0000,3 +80.407,51.17689759,-2.26719302,434.27,33.80,82.81,178.55,178.28,-4.67,53.85,1151.59,96.80,0.1393,0.2628,0.0000,1.0000,3 +80.891,51.17695534,-2.26657421,438.52,43.64,80.67,180.84,180.50,-3.92,47.09,1146.85,96.49,0.3412,0.1373,0.0000,1.0000,3 +81.375,51.17702698,-2.26594642,442.56,47.78,76.19,182.47,182.30,-6.27,43.76,1141.85,96.17,0.2882,0.3235,0.0000,1.0000,3 +81.875,51.17713588,-2.26526517,451.84,55.74,73.27,184.18,183.49,-6.79,41.28,1136.87,95.85,0.2177,-0.3574,-0.0410,1.0000,3 +82.344,51.17726201,-2.26465727,463.24,47.20,69.96,185.46,184.67,-8.25,43.69,1131.60,95.53,0.3118,-0.2441,-0.0371,1.0000,3 +82.813,51.17740125,-2.26406377,476.99,43.21,66.89,186.45,185.32,-8.79,58.22,1127.03,95.23,0.2706,-0.3984,-0.1094,1.0000,3 +83.250,51.17755647,-2.26350963,490.82,47.70,63.05,187.33,186.48,-8.55,70.58,1121.87,94.93,0.3471,-0.1641,-0.0625,1.0000,3 +83.735,51.17774501,-2.26292478,503.58,53.07,54.70,187.16,186.83,-8.85,79.29,550.59,94.94,0.6294,-0.4062,-0.0605,1.0000,3 +84.235,51.17799221,-2.26238065,515.67,50.72,38.65,182.56,183.51,-8.58,87.30,535.67,95.11,0.5765,-0.2559,-0.0781,1.0000,3 +84.688,51.17828140,-2.26197392,524.13,42.09,28.00,180.21,181.03,-6.13,91.44,537.70,95.28,0.5922,0.0000,-0.0703,1.0000,3 +85.188,51.17864138,-2.26165318,527.01,30.63,19.36,178.70,179.34,-3.35,84.22,536.61,95.46,0.2569,0.4647,0.0000,1.0000,3 +85.688,51.17901980,-2.26142496,524.77,23.58,18.09,178.81,178.87,-0.87,51.47,538.40,95.64,0.1922,0.5373,0.0000,1.0000,3 +86.235,51.17945413,-2.26121138,520.03,17.60,15.89,180.93,180.78,-0.65,10.26,1115.59,95.50,0.2784,0.5412,0.0000,1.0000,3 +86.750,51.17986631,-2.26102651,518.43,16.74,16.34,183.01,182.94,-3.58,-23.51,1125.54,95.20,0.4706,0.2726,-0.0293,1.0000,3 +87.282,51.18029417,-2.26081242,523.78,21.57,18.25,184.74,184.38,-4.36,-35.01,1120.19,94.88,0.0726,0.1294,0.0000,1.0000,3 +87.766,51.18070791,-2.26058711,529.99,24.75,19.60,186.57,186.31,-4.02,-42.69,1115.30,94.57,0.3922,0.0000,-0.0312,1.0000,3 +88.266,51.18110502,-2.26034466,534.91,25.86,21.33,188.35,188.06,-3.10,-24.15,1110.56,94.26,0.1098,-0.6074,-0.1602,1.0000,3 +88.766,51.18151479,-2.26008005,539.89,21.66,22.69,190.08,189.73,-4.60,7.48,1105.44,93.95,0.3138,-0.4551,-0.1250,1.0000,3 +89.266,51.18191643,-2.25981769,548.58,21.15,21.75,191.38,190.85,-5.39,47.73,1100.88,93.65,0.2020,-0.5645,-0.1855,1.0000,3 +89.750,51.18231173,-2.25957628,556.61,20.59,18.95,192.64,192.31,-5.14,66.51,1095.83,93.34,0.3726,-0.0254,-0.1836,1.0000,3 +90.219,51.18272290,-2.25937219,562.62,20.89,12.30,193.57,193.55,-5.31,73.85,1091.34,93.03,0.4020,-0.2793,-0.1797,1.0000,3 +90.672,51.18311675,-2.25923323,567.53,23.72,2.73,193.11,193.49,-5.72,78.59,535.63,93.04,0.6490,-0.1270,-0.1387,1.0000,3 +91.141,51.18354195,-2.25920197,572.46,29.67,348.31,188.92,190.00,-6.22,84.06,518.32,93.22,0.6314,-0.2402,-0.0977,1.0000,3 +91.641,51.18396054,-2.25933739,576.53,36.69,330.39,184.69,186.46,-5.11,88.52,520.78,93.40,0.6706,-0.0723,-0.0566,1.0000,3 +92.094,51.18431165,-2.25961710,576.56,40.98,320.90,182.98,183.82,-2.66,89.51,1022.62,93.41,0.3902,0.2255,-0.0059,1.0000,3 +92.594,51.18464231,-2.26002338,571.52,43.09,313.34,184.46,184.74,-0.27,85.85,1092.63,93.12,0.4255,0.2922,0.0000,1.0000,3 +93.079,51.18492998,-2.26050152,561.33,41.08,307.23,186.44,186.15,1.48,83.44,1088.48,92.80,0.3902,0.1393,-0.0195,1.0000,3 +93.579,51.18520041,-2.26104690,546.13,33.96,299.97,188.71,188.01,2.34,74.62,1084.26,92.49,0.4098,0.0902,-0.0703,1.0000,3 +94.110,51.18541948,-2.26166630,529.10,24.77,290.61,190.54,190.00,1.44,69.00,1079.52,92.18,0.4843,0.3706,0.0000,1.0000,3 +94.657,51.18559317,-2.26240379,513.96,21.42,280.59,192.05,191.85,-0.48,66.86,1076.14,91.86,0.4941,0.0000,-0.0898,1.0000,3 +95.188,51.18567809,-2.26314756,504.62,27.98,272.19,193.29,193.27,-1.43,65.35,1071.47,91.55,0.2530,0.3235,0.0000,1.0000,3 +95.735,51.18569854,-2.26392364,497.55,33.40,270.61,195.75,195.14,0.89,48.92,1067.74,91.23,0.0000,0.5176,0.0000,1.0000,3 +96.329,51.18569509,-2.26476496,486.69,32.11,268.84,198.41,197.46,2.53,9.32,1062.69,90.88,-0.0508,0.4529,0.0000,1.0000,3 +96.844,51.18568383,-2.26553666,475.53,30.16,268.34,200.67,199.56,3.06,-6.24,1058.73,90.58,0.0000,0.1628,0.0000,1.0000,3 +97.391,51.18567265,-2.26632598,462.48,28.16,268.50,203.05,201.64,3.53,-14.42,1053.95,90.27,0.1236,0.4333,0.0000,1.0000,3 +97.938,51.18566449,-2.26715235,447.38,23.17,269.27,205.33,203.78,3.25,-27.40,1050.27,89.95,0.1863,0.2726,0.0000,1.0000,3 +98.469,51.18566597,-2.26796171,433.20,17.60,272.13,207.24,205.97,0.85,-29.97,1046.02,89.64,0.1510,0.0000,-0.0293,1.0000,3 +99.016,51.18568874,-2.26877568,425.30,18.86,273.79,208.77,207.68,0.47,-21.88,1042.25,89.34,0.0000,-0.5391,-0.0195,1.0000,3 +99.547,51.18572744,-2.26960301,419.50,24.35,274.78,210.35,209.13,0.75,17.85,1038.16,89.03,0.1941,-0.5098,-0.0273,1.0000,3 +100.016,51.18576186,-2.27035080,414.19,31.47,273.46,211.43,210.32,-0.69,50.86,1034.23,88.75,0.3255,-0.4316,-0.0508,1.0000,3 +100.532,51.18577985,-2.27113485,409.15,33.81,268.06,212.24,211.28,-1.93,62.97,1029.82,88.44,0.3235,-0.2168,-0.0762,1.0000,3 +101.079,51.18575063,-2.27199221,406.76,27.37,257.57,211.95,211.52,-4.52,70.60,1024.87,88.09,0.4941,-0.2754,-0.1680,1.0000,3 +101.547,51.18565064,-2.27271556,409.35,29.48,242.96,209.98,210.12,-5.68,81.82,538.89,87.94,0.7921,0.0000,-0.0703,1.0000,3 +102.047,51.18543914,-2.27337305,414.30,34.51,215.81,199.68,202.27,-6.92,87.43,483.19,88.10,0.7706,0.0000,-0.0430,1.0000,3 +102.532,51.18509959,-2.27380732,418.77,36.08,193.93,191.26,193.38,-5.10,90.28,853.26,88.19,0.5471,0.1667,-0.0254,1.0000,3 +103.016,51.18468001,-2.27400971,418.71,30.63,185.16,191.25,191.35,-2.81,80.10,1023.23,87.90,0.6059,0.3157,0.0000,1.0000,3 +103.500,51.18426337,-2.27407165,416.46,28.39,173.38,190.71,191.05,-3.53,75.68,1020.22,87.60,0.4020,0.3314,0.0000,1.0000,3 +103.985,51.18382001,-2.27400311,415.17,27.15,168.12,191.73,191.45,-2.84,67.20,1015.16,87.28,0.3020,0.2863,-0.0117,1.0000,3 +104.454,51.18341644,-2.27386868,414.05,25.98,164.57,193.01,192.56,-2.39,59.70,1010.64,86.98,0.2549,0.1255,-0.0117,1.0000,3 +104.922,51.18302204,-2.27368214,412.46,24.38,162.79,194.45,193.83,-1.02,48.01,1006.17,86.67,0.0941,0.4569,0.0000,1.0000,3 +105.485,51.18254217,-2.27343389,408.22,19.92,161.19,196.33,195.50,0.48,22.77,1000.99,86.32,0.1941,0.3471,0.0000,1.0000,3 +105.969,51.18212527,-2.27320167,403.91,12.78,160.12,197.80,197.00,-0.28,1.41,996.68,86.01,0.2020,0.2157,0.0000,1.0000,3 +106.469,51.18170050,-2.27296189,402.05,10.06,160.25,199.04,198.24,-1.89,-9.22,992.10,85.70,0.2235,0.2432,0.0000,1.0000,3 +106.954,51.18126139,-2.27271483,404.00,12.10,160.57,200.08,199.20,-1.99,-18.53,987.56,85.37,0.1294,0.2941,0.0000,1.0000,3 +107.438,51.18084678,-2.27248530,406.05,14.08,161.19,201.03,200.16,-1.97,-30.20,983.09,85.07,0.2765,0.2432,0.0000,1.0000,4 +107.922,51.18041205,-2.27226296,407.74,15.78,163.15,201.85,201.02,-2.67,-37.08,978.33,84.75,0.1784,-0.1895,0.0000,1.0000,4 +108.391,51.17999116,-2.27207106,410.42,18.54,164.83,202.62,201.72,-2.54,-29.14,973.96,84.45,0.0647,-0.4102,-0.0234,1.0000,4 +108.891,51.17954120,-2.27188717,413.49,21.59,165.96,203.44,202.49,-2.25,0.93,969.31,84.14,0.3039,-0.1797,-0.0605,1.0000,4 +109.375,51.17910961,-2.27171320,418.14,26.63,165.58,203.85,202.83,-4.84,32.81,964.88,83.84,0.2451,-0.6641,-0.0664,1.0000,4 +109.860,51.17866274,-2.27151517,426.85,28.71,162.59,203.92,202.98,-5.87,73.03,959.83,83.52,0.2275,-0.6152,-0.0996,1.0000,4 +110.375,51.17821648,-2.27127911,433.36,31.57,154.57,203.84,203.39,-4.70,87.51,955.29,83.20,0.5235,0.1412,-0.0195,1.0000,4 +110.844,51.17781338,-2.27097951,435.27,33.25,145.55,203.16,202.91,-2.72,88.27,950.07,82.88,0.4608,0.1451,-0.0137,1.0000,4 +111.297,51.17744647,-2.27058474,432.24,30.05,131.30,201.47,202.00,-1.59,86.25,466.67,82.81,0.7235,0.3098,0.0000,1.0000,4 +111.829,51.17713424,-2.27002436,426.31,24.17,110.04,194.03,195.73,-3.96,78.86,602.53,82.97,0.6569,0.0687,-0.0371,1.0000,4 +112.360,51.17695351,-2.26931936,424.68,22.73,98.48,192.44,192.59,-4.51,67.18,945.12,82.69,0.4314,0.3608,0.0000,1.0000,4 +112.813,51.17688760,-2.26867148,427.02,24.60,93.13,192.81,192.52,-4.87,64.40,945.17,82.41,0.3569,0.0392,-0.0312,1.0000,4 +113.282,51.17686576,-2.26800854,431.13,25.69,88.42,193.24,192.86,-4.90,64.17,940.41,82.09,0.0118,0.2177,0.0000,1.0000,4 +113.750,51.17687758,-2.26735841,435.10,33.76,85.17,193.87,193.42,-4.21,61.69,935.74,81.78,0.1843,0.4275,0.0000,1.0000,4 +114.219,51.17691723,-2.26668003,438.59,43.73,81.05,194.37,193.93,-4.53,57.77,931.23,81.47,0.2961,0.0000,-0.0273,1.0000,4 +114.688,51.17698847,-2.26600820,443.38,48.59,76.51,194.69,194.20,-5.37,54.35,926.50,81.16,0.4177,0.1490,0.0000,1.0000,4 +115.172,51.17709405,-2.26533794,450.69,55.39,71.80,194.78,194.16,-6.78,52.13,921.78,80.85,0.3196,0.0039,0.0000,1.0000,4 +115.657,51.17723603,-2.26468599,461.79,48.53,68.18,194.75,193.83,-7.30,52.53,917.16,80.53,0.3471,0.0000,-0.0215,1.0000,4 +116.157,51.17740579,-2.26403245,475.27,43.57,63.39,194.50,193.45,-8.49,56.27,911.92,80.20,0.1922,-0.0195,-0.0488,1.0000,4 +116.625,51.17760371,-2.26342471,489.79,48.49,60.67,194.00,192.96,-7.94,61.89,476.62,80.05,0.4686,-0.2656,-0.0273,1.0000,4 +117.125,51.17781849,-2.26284020,504.28,50.03,50.24,190.18,189.71,-10.66,72.74,411.70,80.20,0.5353,-0.3789,-0.0195,1.0000,4 +117.704,51.17814747,-2.26222611,524.52,55.90,35.89,186.79,186.61,-9.86,86.76,871.83,80.21,0.5255,0.0000,-0.0020,1.0000,4 +118.219,51.17850014,-2.26180199,538.20,51.04,25.63,185.74,185.91,-6.88,93.84,905.27,79.90,0.4039,0.0373,0.0000,1.0000,4 +118.797,51.17894170,-2.26145135,545.09,44.46,19.66,186.23,186.42,-3.48,87.15,900.15,79.53,0.2333,0.3549,0.0000,1.0000,4 +119.282,51.17933732,-2.26121427,544.23,40.21,17.08,187.39,187.38,-1.42,75.40,895.23,79.21,0.3079,0.1530,0.0000,1.0000,4 +119.782,51.17974889,-2.26102192,539.06,35.13,13.60,188.62,188.41,-0.06,57.69,890.72,78.88,0.0000,0.6784,0.0000,1.0000,4 +120.313,51.18020506,-2.26085235,531.49,29.13,11.71,190.19,189.84,0.33,-1.75,886.02,78.55,0.2490,0.6333,0.0000,1.0000,4 +120.829,51.18064768,-2.26069999,528.12,25.13,14.88,190.81,190.86,-4.60,-40.95,881.96,78.25,0.4177,0.4784,0.0000,1.0000,4 +121.266,51.18103216,-2.26053074,532.67,28.87,20.10,190.74,190.56,-5.85,-58.91,877.93,77.95,0.2628,-0.3809,-0.0195,1.0000,4 +121.750,51.18142282,-2.26030059,540.60,29.44,23.12,190.98,190.60,-5.37,-47.85,873.41,77.65,0.1647,-0.5781,0.0000,1.0000,4 +122.235,51.18182339,-2.26002105,549.13,30.02,24.63,191.36,190.82,-4.63,-11.33,868.86,77.34,0.1883,-0.5645,-0.0098,1.0000,4 +122.750,51.18221031,-2.25973408,558.58,27.01,25.11,191.56,191.02,-5.04,34.10,864.61,77.04,0.2255,-0.4570,-0.0059,1.0000,4 +123.250,51.18263738,-2.25943366,568.05,28.62,20.19,191.43,191.30,-7.26,70.51,859.62,76.75,0.5510,-0.5000,-0.0215,1.0000,4 +123.750,51.18304310,-2.25921380,577.24,32.95,0.97,187.15,188.93,-9.61,82.34,460.37,76.58,0.7510,0.0000,-0.0312,1.0000,4 +124.250,51.18346508,-2.25917798,587.77,44.92,342.52,180.41,182.15,-7.59,92.98,380.67,76.73,0.7372,0.0000,-0.0020,1.0000,4 +124.797,51.18389736,-2.25937418,593.01,53.55,327.14,176.92,178.48,-4.19,88.26,833.12,76.66,0.2549,0.3804,0.0000,1.0000,4 +125.422,51.18431373,-2.25976743,590.37,58.05,319.99,178.44,179.01,-1.51,84.37,852.73,76.31,0.3902,0.1020,0.0000,1.0000,4 +126.000,51.18468020,-2.26025706,581.05,57.35,312.55,179.79,179.84,0.79,84.33,847.49,75.97,0.3490,0.0667,-0.0273,1.0000,4 +126.532,51.18500959,-2.26081352,566.07,50.85,306.10,181.53,181.01,2.72,79.26,842.69,75.64,0.4059,0.4490,0.0000,1.0000,4 +127.079,51.18528548,-2.26141004,546.77,39.33,297.03,182.93,182.19,2.73,73.46,839.02,75.32,0.4588,0.0530,-0.0078,1.0000,4 +127.657,51.18549736,-2.26207997,526.49,28.07,286.11,183.98,183.41,1.78,71.80,834.83,75.01,0.4941,0.1490,0.0000,1.0000,4 +128.219,51.18563703,-2.26281980,508.86,25.18,277.05,184.91,184.49,0.79,63.27,830.82,74.65,0.4902,0.2628,0.0000,1.0000,4 +128.875,51.18569851,-2.26371000,494.30,27.24,270.90,186.30,185.98,-0.00,47.79,825.42,74.28,0.1824,0.1216,0.0000,1.0000,4 +129.469,51.18570047,-2.26449365,485.44,28.12,269.07,187.86,187.33,1.39,36.35,821.39,73.94,0.0255,0.3098,0.0000,1.0000,4 +130.032,51.18568367,-2.26529937,474.59,26.40,267.53,189.49,188.72,2.54,11.73,816.70,73.59,0.0000,0.4529,0.0000,1.0000,4 +130.610,51.18565767,-2.26613183,462.02,24.90,266.70,191.12,190.20,3.07,-19.37,811.84,73.25,0.1432,0.2745,0.0000,1.0000,4 +131.219,51.18563515,-2.26698980,447.57,21.99,269.16,192.62,191.78,0.96,-35.88,807.09,72.88,0.3412,0.2804,0.0000,1.0000,4 +131.750,51.18563520,-2.26774678,439.34,21.71,273.03,193.38,192.83,-0.89,-42.31,802.27,72.56,0.3000,0.0157,0.0000,1.0000,4 +132.235,51.18566086,-2.26843055,435.46,25.22,274.46,194.06,193.43,-0.09,-31.15,798.68,72.27,0.0000,-0.3711,-0.0273,1.0000,4 +132.719,51.18570096,-2.26912747,431.12,29.10,275.40,194.85,194.09,0.90,-13.91,794.42,71.98,0.0000,-0.3594,-0.0273,1.0000,4 +133.250,51.18575097,-2.26989362,424.72,34.29,276.21,195.73,194.88,1.45,16.57,790.05,71.66,0.1393,-0.4609,-0.0117,1.0000,4 +133.735,51.18579443,-2.27058413,417.83,37.49,274.68,196.35,195.59,-0.66,42.49,785.77,71.37,0.3882,-0.2812,-0.0078,1.0000,4 +134.204,51.18582090,-2.27128526,413.76,39.72,269.11,196.20,195.87,-3.23,67.29,781.86,71.07,0.3412,-0.6016,-0.0098,1.0000,4 +134.688,51.18580820,-2.27197482,412.12,33.06,255.15,194.64,195.33,-4.65,80.55,777.26,70.75,0.7314,0.0000,0.0000,1.0000,4 +135.157,51.18570673,-2.27261699,411.76,31.77,238.79,190.92,191.80,-4.66,83.12,772.88,70.43,0.7686,0.0294,-0.0039,1.0000,4 +135.657,51.18548265,-2.27322738,411.78,31.79,216.65,185.37,187.64,-5.29,83.45,767.99,70.10,0.6804,0.3177,0.0000,1.0000,4 +136.141,51.18516997,-2.27363780,412.34,30.17,199.66,181.29,182.82,-5.31,83.02,763.13,69.79,0.5569,0.1765,0.0000,1.0000,4 +136.657,51.18477594,-2.27388182,412.87,25.92,185.88,179.43,180.44,-4.79,82.09,758.31,69.45,0.5314,-0.1621,-0.0371,1.0000,4 +137.157,51.18436808,-2.27396367,412.30,24.24,176.15,178.88,179.37,-3.57,79.88,753.47,69.12,0.4922,0.4824,0.0000,1.0000,4 +137.641,51.18396738,-2.27392628,410.29,22.21,168.07,178.77,179.02,-3.52,67.34,748.71,68.81,0.3726,0.2863,-0.0195,1.0000,4 +138.110,51.18359582,-2.27380640,409.54,21.55,164.95,179.39,179.25,-2.98,43.13,744.43,68.50,0.1745,0.4373,0.0000,1.0000,4 +138.625,51.18316918,-2.27361799,410.21,22.22,163.61,180.31,179.96,-1.69,28.82,739.52,68.18,0.1432,0.3549,0.0000,1.0000,4 +139.079,51.18279527,-2.27343352,409.99,21.96,162.54,181.07,180.69,-1.10,18.33,735.01,67.87,0.2255,0.2451,0.0000,1.0000,4 +139.594,51.18239656,-2.27322456,409.34,19.98,161.62,181.87,181.47,-1.64,4.58,730.13,67.54,0.2961,0.2235,-0.0117,1.0000,4 +140.079,51.18200043,-2.27301380,410.98,19.55,161.43,182.24,181.83,-3.62,1.46,725.63,67.23,0.1334,0.1039,-0.0195,1.0000,4 +140.563,51.18162319,-2.27280683,416.36,24.55,161.24,182.59,182.02,-3.02,1.18,720.90,66.90,0.0000,0.0726,0.0000,1.0000,4 +141.047,51.18123302,-2.27260290,421.12,29.19,161.15,182.90,182.40,-2.51,1.19,716.04,66.59,0.0000,0.0530,0.0000,1.0000,4 diff --git a/track_data/flight_20260901_220251_021128.csv b/track_data/flight_20260901_220251_021128.csv new file mode 100644 index 00000000..20c1a155 --- /dev/null +++ b/track_data/flight_20260901_220251_021128.csv @@ -0,0 +1,325 @@ +# aircraft_title=Extra 300S Paint2 +# recorded_at=2026-09-01T22:02:51 +# laps_s=29.223,34.723,34.611,32.723 +timestamp_s,latitude,longitude,altitude_ft,agl_ft,heading_deg,airspeed_kts,groundspeed_kts,pitch_deg,bank_deg,torque,health_pct,stick_pitch,stick_roll,stick_yaw,stick_throttle,lap +0.000,51.18983522,-2.27598698,986.46,622.95,166.16,182.81,181.60,6.85,0.13,1078.43,100.00,-0.0234,0.0000,0.0000,1.0000,1 +0.444,51.18947880,-2.27585047,967.65,600.15,166.07,186.00,184.52,7.71,0.14,1167.50,100.00,-0.1777,0.0000,0.0000,1.0000,1 +0.778,51.18918039,-2.27573022,950.36,580.33,166.00,188.63,186.83,8.89,0.16,1171.53,100.00,-0.2031,0.0098,0.0000,1.0000,1 +1.167,51.18884545,-2.27560194,929.36,556.31,165.92,191.76,189.05,10.51,0.17,1174.28,100.00,-0.1699,0.0294,-0.0078,1.0000,1 +1.611,51.18849193,-2.27545994,902.03,526.47,165.92,195.14,191.41,11.59,0.18,1174.65,100.00,-0.0996,0.0294,0.0000,1.0000,1 +2.000,51.18815962,-2.27533162,874.24,496.06,165.95,198.50,194.00,12.04,0.18,1176.99,100.00,-0.0430,0.0314,0.0000,1.0000,1 +2.445,51.18774602,-2.27516400,838.02,456.27,165.94,202.50,197.39,12.59,0.18,1177.03,100.00,-0.0312,0.0667,0.0000,1.0000,1 +2.889,51.18738563,-2.27501896,805.28,423.79,165.93,206.04,200.26,12.98,0.19,1178.21,100.00,0.0000,0.0687,0.0000,1.0000,1 +3.278,51.18701844,-2.27487959,772.16,390.69,165.92,209.28,202.96,13.32,0.19,1179.79,100.00,0.0000,0.1549,0.0000,1.0000,1 +3.667,51.18664663,-2.27472811,736.64,355.16,165.91,212.57,205.86,13.63,-0.37,1179.17,100.00,0.1118,0.1745,0.0000,1.0000,1 +4.111,51.18627920,-2.27457933,700.07,318.41,165.88,216.02,208.65,13.88,-2.04,1182.33,100.00,0.1588,0.0000,0.0000,1.0000,1 +4.445,51.18590261,-2.27443180,664.88,282.32,165.99,219.04,211.35,13.42,-2.43,1182.07,100.00,0.2177,0.0000,0.0000,1.0000,1 +4.889,51.18551074,-2.27427829,626.81,246.08,166.19,222.23,215.14,12.32,-2.47,1182.83,100.00,0.1314,-0.0938,0.0000,1.0000,1 +5.333,51.18509129,-2.27411324,589.30,207.16,166.21,225.23,218.33,12.32,-2.40,1185.22,100.00,0.2863,-0.1133,0.0000,1.0000,1 +5.722,51.18466268,-2.27394748,552.91,163.40,166.49,227.91,221.55,10.22,-2.16,1184.55,100.00,0.1804,-0.1816,0.0000,1.0000,1 +6.111,51.18429237,-2.27380483,525.80,136.62,166.60,230.24,224.92,8.93,-1.22,1188.00,100.00,0.2451,-0.0742,0.0000,1.0000,1 +6.500,51.18388653,-2.27365043,499.03,110.03,166.70,232.17,227.88,7.21,-0.95,1187.82,100.00,0.1883,-0.1602,0.0000,1.0000,1 +6.889,51.18350848,-2.27350357,478.44,89.58,166.74,233.94,230.20,5.81,-0.30,1187.78,100.00,0.2510,-0.0664,0.0000,1.0000,1 +7.278,51.18309184,-2.27334597,460.45,71.78,166.80,235.37,232.46,3.87,0.88,1190.17,100.00,0.2373,-0.0195,0.0000,1.0000,1 +7.667,51.18267331,-2.27319096,447.74,59.21,166.74,236.48,234.11,2.42,1.47,1189.76,100.00,0.1765,0.0000,0.0000,1.0000,1 +8.111,51.18223565,-2.27302487,438.48,47.74,166.66,237.59,235.37,0.84,1.54,1190.98,100.00,0.1726,-0.0059,0.0000,1.0000,1 +8.500,51.18182082,-2.27286554,433.91,41.79,166.58,238.29,236.25,0.11,1.96,1190.88,100.00,0.0000,-0.2090,0.0000,1.0000,1 +8.889,51.18138173,-2.27269835,431.28,39.19,166.51,239.02,236.93,0.11,6.07,1190.58,100.00,0.0059,-0.2363,0.0000,1.0000,1 +9.278,51.18097686,-2.27253879,429.05,36.96,166.40,239.65,237.54,0.04,11.42,1190.84,100.00,0.1471,-0.2031,0.0000,1.0000,1 +9.667,51.18055051,-2.27237355,427.02,34.94,166.10,240.24,238.12,-0.24,18.45,1190.58,100.00,0.2020,-0.2734,-0.0039,1.0000,1 +10.056,51.18012215,-2.27219943,425.37,33.30,165.52,240.78,238.67,-0.79,31.03,1190.57,100.00,0.1726,-0.2930,-0.0117,1.0000,1 +10.445,51.17970426,-2.27201881,424.17,32.12,164.26,241.24,239.15,-1.36,43.19,1190.41,100.00,0.2235,-0.2461,-0.0137,1.0000,1 +10.834,51.17929172,-2.27182444,423.64,31.90,162.08,241.55,239.51,-2.06,52.25,1190.22,100.00,0.2628,-0.1738,-0.0156,1.0000,1 +11.278,51.17885671,-2.27159537,424.29,28.16,158.97,241.76,239.75,-2.75,57.86,1190.04,100.00,0.2059,-0.2852,-0.0254,1.0000,1 +11.667,51.17843536,-2.27133107,425.97,24.02,156.10,241.99,239.95,-2.71,67.66,1189.90,100.00,0.3118,-0.2715,-0.0273,1.0000,1 +12.056,51.17805389,-2.27105122,427.12,25.16,151.48,242.01,240.09,-2.90,76.59,1189.80,100.00,0.6471,-0.0977,-0.0273,1.0000,1 +12.445,51.17765586,-2.27069559,428.28,26.41,138.61,240.27,239.29,-4.35,80.00,1189.72,100.00,0.6157,-0.0098,-0.0273,1.0000,1 +12.889,51.17731994,-2.27024024,431.67,29.89,123.95,237.00,236.61,-5.52,81.86,1189.80,100.00,0.5392,-0.0410,-0.0352,1.0000,1 +13.278,51.17705886,-2.26968783,437.38,35.73,110.60,234.21,233.39,-5.57,83.58,1189.70,100.00,0.5117,0.0000,-0.0391,1.0000,1 +13.667,51.17688226,-2.26903875,443.51,41.88,99.97,232.47,231.49,-5.12,84.92,1189.96,100.00,0.4686,0.0000,0.0000,1.0000,1 +14.112,51.17679518,-2.26834831,448.82,39.43,90.15,231.36,230.35,-4.28,86.13,1189.75,100.00,0.3745,0.1941,0.0000,1.0000,1 +14.500,51.17678104,-2.26767042,451.89,40.74,83.54,230.96,229.69,-3.07,82.77,1189.67,100.00,0.3333,0.3471,0.0000,1.0000,1 +14.889,51.17682184,-2.26701029,452.88,55.75,79.06,231.39,229.94,-2.41,71.90,1189.76,100.00,0.1314,0.4726,0.0000,1.0000,1 +15.278,51.17689889,-2.26634496,453.28,58.30,76.63,231.98,230.37,-1.75,50.07,1189.33,100.00,0.3294,0.2647,0.0000,1.0000,1 +15.723,51.17700432,-2.26565332,454.50,59.65,73.06,232.41,230.88,-4.69,38.49,1189.28,100.00,0.3333,-0.3203,-0.0977,1.0000,1 +16.112,51.17713819,-2.26495134,463.62,62.91,69.37,232.03,229.88,-7.61,47.54,1189.16,100.00,0.3196,-0.3730,-0.0977,1.0000,1 +16.556,51.17728627,-2.26434648,477.77,62.13,65.48,231.67,229.01,-8.89,65.48,1189.12,100.00,0.1510,-0.4980,-0.1465,1.0000,1 +16.889,51.17745598,-2.26375462,493.84,53.96,62.38,231.44,228.88,-8.13,90.09,1189.16,100.00,0.4922,0.0000,-0.0156,1.0000,1 +17.278,51.17763510,-2.26320544,505.75,58.99,54.35,230.61,229.11,-6.13,95.68,1187.53,100.00,0.4902,0.2804,0.0000,1.0000,1 +17.667,51.17787178,-2.26264933,513.92,57.41,44.39,229.45,228.63,-3.82,93.67,1187.44,100.00,0.5412,0.1883,0.0000,1.0000,1 +18.112,51.17819179,-2.26211670,516.00,37.03,31.27,227.91,227.56,-2.03,89.04,1186.95,100.00,0.5804,0.1726,-0.0449,1.0000,1 +18.501,51.17854147,-2.26175471,513.65,19.92,19.04,226.01,225.95,-1.71,83.42,1186.62,100.00,0.3000,0.4647,0.0000,1.0000,1 +18.889,51.17890699,-2.26152427,510.27,11.03,15.39,226.57,225.45,-0.59,66.10,1186.82,100.00,0.1216,0.4922,0.0000,1.0000,1 +19.278,51.17931071,-2.26134241,506.80,7.03,13.15,226.92,225.50,1.42,30.58,1187.25,100.00,0.2432,0.5314,0.0000,1.0000,1 +19.723,51.17975621,-2.26116806,502.69,4.10,12.51,227.88,226.57,-2.40,-2.62,1187.34,100.00,0.4412,0.0118,-0.1953,1.0000,1 +20.223,51.18023492,-2.26098597,509.94,12.69,13.70,227.90,225.90,-6.93,-13.76,1187.58,100.00,0.0588,0.4686,0.0000,1.0000,1 +20.612,51.18068348,-2.26081058,526.02,27.64,13.86,228.04,225.60,-6.78,-39.18,1187.67,100.00,0.2216,0.3255,0.0000,1.0000,1 +21.056,51.18112336,-2.26062390,541.31,41.82,15.88,228.00,225.95,-7.11,-58.22,1187.40,100.00,0.3235,0.1765,-0.0059,1.0000,1 +21.501,51.18152944,-2.26042629,554.81,50.20,19.58,228.00,226.02,-7.13,-62.11,1186.02,100.00,-0.0664,-0.4023,-0.1602,1.0000,1 +21.890,51.18191584,-2.26021243,568.02,52.80,20.07,228.21,226.21,-5.55,-44.75,1186.02,100.00,-0.3320,-0.4941,-0.1582,1.0000,1 +22.278,51.18232690,-2.25996399,578.40,52.73,19.43,228.52,227.13,-1.38,-13.63,1184.61,100.00,0.0000,-0.5996,-0.1641,1.0000,1 +22.723,51.18273519,-2.25972279,581.07,46.96,20.82,229.34,228.12,-0.78,33.22,1184.38,100.00,0.2922,-0.6387,-0.1699,1.0000,1 +23.112,51.18311557,-2.25949804,580.19,40.97,16.99,229.82,228.88,-2.98,77.78,1184.33,100.00,0.8549,0.1843,-0.0664,1.0000,1 +23.501,51.18351753,-2.25932144,579.14,37.83,355.47,225.04,227.03,-4.38,86.45,1183.91,100.00,0.8157,0.4941,0.0000,1.0000,1 +23.890,51.18392988,-2.25934380,579.24,39.30,332.22,216.80,219.87,-5.64,82.95,1184.23,100.00,0.3177,-0.1016,-0.1660,1.0000,1 +24.279,51.18431131,-2.25959974,581.78,45.92,326.62,216.60,216.28,-3.69,83.06,1184.86,100.00,0.5549,-0.3359,-0.0742,1.0000,1 +24.667,51.18463635,-2.25993132,583.02,53.73,317.17,216.77,216.72,-2.76,90.47,1185.23,100.00,0.5549,0.0000,-0.0586,1.0000,1 +25.112,51.18496447,-2.26037972,580.41,58.96,305.39,215.96,216.09,-0.23,95.25,1183.21,99.83,0.5000,0.0000,-0.0762,1.0000,1 +25.501,51.18521840,-2.26091362,571.42,57.35,296.63,216.26,215.55,2.55,94.89,1180.01,99.61,0.4255,0.1373,-0.0977,1.0000,1 +25.945,51.18541109,-2.26147522,556.46,49.34,290.38,217.63,215.78,4.50,83.17,1177.16,99.40,0.4569,0.2412,-0.1504,1.0000,1 +26.390,51.18557805,-2.26217601,535.32,37.53,281.70,219.08,216.93,3.81,72.27,1175.23,99.17,0.3882,0.3608,-0.0195,1.0000,1 +26.834,51.18567437,-2.26286337,516.98,34.30,273.36,219.86,218.28,1.94,65.09,1172.12,98.95,0.2784,0.3138,-0.0566,1.0000,1 +27.279,51.18570637,-2.26359051,503.53,34.34,269.45,221.32,219.73,2.07,52.73,1170.38,98.73,0.0157,0.5157,0.0000,1.0000,1 +27.723,51.18569902,-2.26434421,490.76,31.76,268.07,223.23,221.37,3.27,24.75,1167.56,98.49,0.1726,0.3079,-0.0820,1.0000,1 +28.167,51.18568107,-2.26507024,478.61,27.53,267.12,224.95,222.99,2.96,9.14,1164.72,98.28,0.1726,0.2569,-0.1328,1.0000,1 +28.612,51.18566089,-2.26573198,468.54,25.94,266.76,226.31,224.45,2.64,-2.27,1162.62,98.08,0.2079,0.3020,0.0000,1.0000,1 +29.001,51.18563726,-2.26644795,458.89,26.47,267.05,227.47,225.68,1.45,-16.80,671.65,97.96,0.2392,0.3941,0.0000,1.0000,1 +29.501,51.18561586,-2.26724126,451.17,28.84,268.67,227.24,225.71,-0.30,-41.22,558.75,98.09,0.3353,0.3039,0.0000,1.0000,1 +29.945,51.18561093,-2.26798408,447.49,32.77,273.10,225.96,224.73,-2.13,-56.91,561.62,98.22,0.3098,0.0745,-0.0195,1.0000,1 +30.390,51.18563469,-2.26867487,447.29,39.94,276.98,223.94,222.68,-2.31,-56.81,563.16,98.34,0.0000,-0.4785,-0.1562,1.0000,1 +30.778,51.18568476,-2.26932418,448.10,48.78,278.06,222.68,221.35,-1.25,-29.09,564.83,98.46,0.0000,-0.6543,-0.1543,1.0000,1 +31.223,51.18574975,-2.27002548,448.45,60.81,279.31,221.21,219.91,-1.01,29.19,565.06,98.59,0.0883,-0.3945,-0.1680,1.0000,1 +31.723,51.18582253,-2.27080678,447.18,69.87,278.02,219.59,218.48,-1.42,73.33,566.35,98.73,0.5804,0.0000,-0.0703,1.0000,1 +32.112,51.18587439,-2.27147954,443.11,69.58,266.23,217.30,217.03,-1.87,84.80,567.55,98.86,0.6216,0.0000,-0.1172,1.0000,1 +32.501,51.18586389,-2.27211294,437.76,57.79,250.37,212.21,212.82,-1.51,86.55,952.26,98.93,0.6314,0.1804,-0.0762,1.0000,1 +32.945,51.18576011,-2.27268141,431.49,51.17,235.94,209.23,209.82,-0.92,85.90,1172.02,98.75,0.6667,0.2216,-0.0508,1.0000,1 +33.334,51.18556618,-2.27319650,424.00,43.68,220.00,206.12,206.88,-1.11,83.28,1175.29,98.52,0.6510,0.4196,0.0000,1.0000,1 +33.723,51.18529274,-2.27361499,417.04,35.94,204.67,203.76,204.72,-2.91,77.11,1171.23,98.30,0.7529,0.0000,-0.1543,1.0000,1 +34.112,51.18494660,-2.27388849,414.26,30.30,186.85,199.28,201.04,-5.83,77.12,1167.55,98.08,0.3412,0.0000,-0.1680,1.0000,1 +34.501,51.18458879,-2.27399544,416.31,28.41,182.03,200.38,200.00,-4.29,77.98,1164.72,97.85,0.5529,0.0412,-0.0605,1.0000,1 +34.890,51.18423675,-2.27402259,418.05,30.12,174.43,200.88,200.66,-4.30,78.94,1161.68,97.64,0.5078,0.0687,-0.0547,1.0000,1 +35.278,51.18386706,-2.27397665,419.53,31.59,166.62,201.17,200.99,-4.09,79.91,1076.57,97.43,0.2804,0.0549,-0.1152,1.0000,1 +35.723,51.18349953,-2.27385029,420.23,32.22,162.82,201.71,201.15,-2.60,78.51,569.00,97.48,0.1020,0.5137,-0.0742,1.0000,1 +36.112,51.18314354,-2.27367869,418.63,30.50,161.53,201.74,200.98,-0.73,57.29,560.96,97.60,0.1334,0.4647,-0.0645,1.0000,1 +36.501,51.18279438,-2.27348739,415.04,26.84,160.03,201.10,200.22,0.20,30.28,558.08,97.73,0.2333,0.4549,-0.0664,1.0000,1 +36.890,51.18242329,-2.27326759,411.38,22.08,158.74,200.67,199.88,-1.00,2.58,560.87,97.85,0.2255,0.4745,-0.0430,1.0000,1 +37.390,51.18201686,-2.27301793,411.18,19.73,159.17,199.93,199.17,-2.71,-28.21,560.76,97.99,0.2765,0.3726,-0.0352,1.0000,1 +37.778,51.18166719,-2.27281905,413.76,22.00,161.68,198.95,198.25,-4.32,-42.80,562.02,98.12,0.3255,-0.0742,-0.1113,1.0000,1 +38.223,51.18131320,-2.27263476,419.07,27.35,164.99,197.90,197.09,-5.45,-43.31,563.23,98.24,0.0510,-0.2891,-0.1035,1.0000,1 +38.612,51.18098012,-2.27249527,426.23,34.57,165.98,196.94,195.96,-4.78,-38.62,563.77,98.36,-0.0020,0.0000,0.0000,1.0000,2 +39.001,51.18060561,-2.27235450,433.20,41.44,166.83,196.15,195.29,-3.88,-32.25,565.08,98.48,0.0000,-0.3711,-0.0938,1.0000,2 +39.390,51.18025099,-2.27223140,438.66,46.86,167.75,195.39,194.64,-3.10,-16.11,565.55,98.61,0.0138,-0.4531,-0.0938,1.0000,2 +39.834,51.17990454,-2.27211706,443.01,51.21,168.48,194.84,194.11,-2.66,6.47,566.61,98.73,0.1393,-0.4590,-0.0879,1.0000,2 +40.223,51.17951691,-2.27198947,446.89,55.02,168.66,194.22,193.57,-2.52,32.64,567.42,98.86,0.2039,-0.3809,-0.0723,1.0000,2 +40.667,51.17913334,-2.27185638,449.20,57.32,166.85,193.55,193.13,-3.39,50.75,568.32,98.99,0.3353,-0.2715,-0.0684,1.0000,2 +41.112,51.17877619,-2.27171230,451.04,55.08,162.76,193.52,193.22,-4.49,59.13,1140.43,98.92,0.2922,-0.2734,-0.0684,1.0000,2 +41.501,51.17845032,-2.27154427,453.65,51.71,159.28,194.27,193.92,-4.43,68.47,1177.71,98.71,0.3941,-0.3691,-0.0625,1.0000,2 +41.890,51.17811075,-2.27134022,455.80,53.86,153.78,195.20,194.99,-4.13,83.18,1174.09,98.48,0.6647,-0.0625,-0.0273,1.0000,2 +42.278,51.17779823,-2.27109926,455.99,53.94,141.98,195.00,195.60,-3.10,89.87,1170.08,98.26,0.5529,0.1569,0.0000,1.0000,2 +42.667,51.17750709,-2.27077011,452.99,50.65,128.87,194.20,194.92,-1.30,88.99,1166.30,98.04,0.6784,0.2726,0.0000,1.0000,2 +43.112,51.17724698,-2.27030313,445.95,43.62,115.00,192.80,193.61,-1.10,81.09,1162.84,97.82,0.5392,0.2530,0.0000,1.0000,2 +43.501,51.17707912,-2.26978405,438.96,36.70,106.31,193.68,193.70,-1.51,73.75,1160.16,97.59,0.4412,0.1843,0.0000,1.0000,2 +43.890,51.17696547,-2.26923326,433.51,31.43,98.90,194.47,194.39,-2.17,70.51,1157.25,97.37,0.2922,0.2804,0.0000,1.0000,2 +44.278,51.17690361,-2.26866894,430.07,26.00,94.85,195.99,195.61,-1.75,64.96,1153.98,97.15,0.3059,0.2079,0.0000,1.0000,2 +44.667,51.17687364,-2.26811596,426.95,21.88,91.45,197.65,197.08,-1.71,61.07,1151.25,96.93,0.4000,0.0000,-0.0566,1.0000,2 +45.056,51.17686597,-2.26753821,424.42,22.51,86.66,198.67,198.23,-2.68,60.80,638.06,96.84,0.3000,0.2039,0.0000,1.0000,2 +45.501,51.17688785,-2.26693518,423.42,28.39,82.36,198.86,198.41,-3.22,58.48,555.09,96.96,0.3667,0.3020,0.0000,1.0000,2 +45.945,51.17694019,-2.26630271,424.58,29.65,76.56,196.99,196.74,-5.76,53.04,553.07,97.09,0.3647,0.2255,0.0000,1.0000,2 +46.334,51.17702634,-2.26573599,430.65,36.01,72.39,195.79,195.10,-7.12,49.30,556.02,97.22,0.2451,0.2882,0.0000,1.0000,2 +46.723,51.17713255,-2.26522286,440.00,44.58,69.93,194.49,193.40,-7.51,45.19,554.69,97.34,0.2432,-0.2383,-0.1641,1.0000,2 +47.112,51.17725760,-2.26467975,451.86,44.20,67.42,193.33,192.10,-7.98,49.13,557.20,97.46,0.1784,-0.3184,-0.1680,1.0000,2 +47.501,51.17739662,-2.26417648,463.68,33.75,65.50,192.06,190.90,-7.57,58.11,556.37,97.59,0.2863,-0.2070,-0.1484,1.0000,2 +47.890,51.17754082,-2.26368881,473.96,33.66,62.28,191.75,190.84,-7.68,63.73,1110.73,97.53,0.4431,0.0000,-0.0820,1.0000,2 +48.278,51.17770325,-2.26319048,484.49,36.05,56.74,191.78,191.06,-8.68,66.27,1157.30,97.31,0.4529,-0.2559,-0.0781,1.0000,2 +48.723,51.17790538,-2.26269917,496.85,39.74,50.12,191.72,190.94,-9.25,73.30,1152.76,97.08,0.4902,-0.2305,-0.0781,1.0000,2 +49.112,51.17814296,-2.26224053,509.30,37.88,42.67,191.89,191.29,-8.99,81.73,1148.48,96.85,0.5431,-0.1895,-0.0664,1.0000,2 +49.556,51.17839979,-2.26185046,520.59,31.73,33.27,191.61,191.55,-7.81,90.34,1144.09,96.62,0.5784,-0.1758,-0.0684,1.0000,2 +49.945,51.17871954,-2.26150210,528.57,28.13,20.74,191.05,191.80,-4.89,96.06,1139.98,96.39,0.5137,0.3412,0.0000,1.0000,2 +50.390,51.17905167,-2.26126368,530.10,23.93,12.61,190.88,191.37,-2.36,83.26,1136.87,96.17,0.1941,0.6451,0.0000,1.0000,2 +50.778,51.17940130,-2.26112096,527.33,21.12,11.44,192.81,192.54,-0.85,43.59,601.01,96.11,0.2784,0.5725,0.0000,1.0000,2 +51.223,51.17979189,-2.26099849,524.97,21.45,9.61,193.01,192.83,-2.47,3.94,548.22,96.23,0.3431,0.5412,0.0000,1.0000,2 +51.667,51.18018348,-2.26088925,527.95,27.00,10.96,191.83,191.55,-5.63,-35.65,543.62,96.36,0.5863,0.3902,0.0000,1.0000,2 +52.112,51.18056344,-2.26076127,536.56,35.93,18.43,189.87,189.45,-9.19,-61.34,546.67,96.49,0.3882,0.4020,0.0000,1.0000,2 +52.501,51.18088130,-2.26060441,548.57,46.52,22.84,188.37,187.52,-8.47,-74.53,546.03,96.61,0.0000,-0.3711,-0.0215,1.0000,2 +52.890,51.18119657,-2.26039418,560.22,54.99,23.33,187.37,186.77,-6.63,-66.83,547.29,96.74,0.0000,-0.3340,-0.0234,1.0000,2 +53.278,51.18151202,-2.26017150,568.48,53.28,24.14,186.84,186.51,-4.84,-48.84,548.15,96.86,0.0000,-0.6797,-0.0762,1.0000,2 +53.723,51.18183411,-2.25993185,574.37,52.04,25.40,186.21,185.99,-3.31,-2.05,548.59,96.99,0.1686,-0.6211,-0.1309,1.0000,2 +54.112,51.18215746,-2.25968587,579.15,46.51,25.61,185.80,185.72,-4.14,47.20,549.99,97.12,0.4569,-0.4414,-0.1504,1.0000,2 +54.556,51.18249667,-2.25944177,583.10,43.26,19.37,184.66,185.11,-6.07,73.85,730.84,97.24,0.5274,-0.0762,-0.1094,1.0000,2 +55.001,51.18285587,-2.25924652,587.02,42.89,9.32,184.31,185.11,-6.05,81.55,1141.55,97.05,0.5294,-0.3457,-0.0762,1.0000,2 +55.390,51.18317310,-2.25915074,589.70,44.41,0.53,184.53,185.40,-4.48,90.71,1146.19,96.83,0.6725,0.3863,0.0000,1.0000,2 +55.778,51.18351582,-2.25912950,589.18,45.16,346.48,183.61,185.27,-2.87,88.53,1141.98,96.61,0.6490,0.0000,-0.0449,1.0000,2 +56.223,51.18386993,-2.25923714,585.01,43.58,331.15,181.95,183.75,-1.71,88.16,1138.12,96.37,0.5431,0.2569,0.0000,1.0000,2 +56.667,51.18419358,-2.25948767,577.68,39.14,321.33,182.55,183.24,-0.03,84.21,1134.88,96.14,0.4000,0.4196,0.0000,1.0000,2 +57.112,51.18451521,-2.25987075,566.37,35.21,314.46,184.71,184.59,0.93,73.94,1131.53,95.90,0.5020,0.0000,-0.1191,1.0000,2 +57.501,51.18477255,-2.26026837,555.75,32.10,306.99,186.16,186.05,0.35,71.87,593.11,95.83,0.4294,0.0000,-0.1426,1.0000,2 +57.945,51.18500772,-2.26076499,545.16,29.46,299.64,185.95,185.94,0.03,71.66,545.53,95.96,0.3255,0.3039,-0.0332,1.0000,2 +58.390,51.18519528,-2.26127413,535.42,26.94,294.05,185.72,185.58,0.12,66.46,540.97,96.10,0.3784,0.2373,-0.0195,1.0000,2 +58.834,51.18534654,-2.26181623,526.66,25.04,288.52,185.31,185.26,-0.67,62.53,543.80,96.23,0.3569,0.0745,-0.0723,1.0000,2 +59.279,51.18546895,-2.26238144,519.56,27.19,283.95,185.31,185.29,-0.87,62.54,545.20,96.36,0.2589,0.0000,-0.1387,1.0000,2 +59.723,51.18555865,-2.26295530,513.11,32.82,280.48,185.18,185.00,-0.58,63.06,545.71,96.49,0.3079,0.0000,-0.1367,1.0000,2 +60.223,51.18563274,-2.26365466,505.12,37.07,274.77,185.05,184.98,-0.89,63.74,547.96,96.64,0.3275,-0.1348,-0.1484,1.0000,2 +60.667,51.18566384,-2.26426678,498.59,38.67,270.56,185.05,184.90,-0.69,64.31,548.45,96.78,0.0000,0.4784,-0.0410,1.0000,2 +61.112,51.18566682,-2.26485619,491.79,38.14,268.60,185.20,184.83,0.91,43.98,549.92,96.91,0.0000,0.5706,0.0000,1.0000,2 +61.556,51.18565154,-2.26550587,483.05,37.59,267.10,185.79,185.16,2.29,1.96,551.37,97.05,0.1236,0.5529,0.0000,1.0000,2 +62.056,51.18562983,-2.26616773,473.77,37.41,266.71,186.16,185.49,2.02,-40.87,552.50,97.19,0.3275,0.5157,0.0000,1.0000,2 +62.612,51.18561291,-2.26692534,460.88,34.67,272.73,186.39,185.73,1.20,-74.44,553.87,97.35,0.4216,-0.2168,-0.1191,1.0000,2 +63.112,51.18563435,-2.26763179,447.56,28.39,281.84,186.00,185.38,2.02,-67.36,555.77,97.49,0.3138,-0.5352,-0.1777,1.0000,2 +63.612,51.18571366,-2.26830538,434.89,22.49,284.96,186.39,185.55,1.78,-29.71,556.76,97.64,0.1706,-0.6035,-0.1816,1.0000,2 +64.223,51.18584629,-2.26907262,423.49,20.38,285.40,186.86,186.13,1.13,23.86,558.85,97.80,0.4000,-0.3809,-0.1680,1.0000,2 +64.723,51.18595715,-2.26975423,417.21,24.92,281.46,186.56,186.29,-2.85,46.95,560.37,97.95,0.4529,-0.4238,-0.1680,1.0000,2 +65.279,51.18604378,-2.27052579,418.38,38.74,272.57,184.97,185.03,-6.37,67.41,562.01,98.11,0.4647,-0.4258,-0.1660,1.0000,2 +65.779,51.18606152,-2.27121617,423.83,52.38,262.08,184.02,184.29,-5.80,84.92,1109.33,98.11,0.5294,-0.0254,-0.0625,1.0000,2 +66.279,51.18601676,-2.27184915,426.38,50.87,252.41,184.26,184.60,-3.75,86.89,1168.28,97.89,0.5274,0.0177,-0.0312,1.0000,2 +66.779,51.18589975,-2.27249167,424.54,44.39,241.31,185.19,185.59,-2.19,86.59,1164.00,97.63,0.5784,0.3059,0.0000,1.0000,2 +67.223,51.18572409,-2.27304490,419.30,39.19,228.79,185.24,185.80,-1.86,80.79,1159.85,97.40,0.6647,0.0000,-0.1328,1.0000,2 +67.723,51.18544109,-2.27357474,412.84,32.68,209.40,183.68,185.26,-4.36,77.04,1155.84,97.13,0.6333,-0.1016,-0.1641,1.0000,2 +68.279,51.18506309,-2.27393229,411.49,29.59,191.54,181.30,182.71,-6.16,77.62,1152.37,96.87,0.5569,-0.0020,-0.1660,1.0000,2 +68.723,51.18468752,-2.27407601,414.20,26.70,179.57,181.33,182.02,-6.05,79.43,1149.10,96.63,0.6020,0.0000,-0.1621,1.0000,2 +69.168,51.18430410,-2.27408647,417.72,29.86,167.72,180.75,181.46,-5.88,79.83,573.76,96.63,0.3686,0.4196,-0.0684,1.0000,2 +69.612,51.18394675,-2.27398548,420.67,32.78,163.40,179.74,179.81,-4.75,64.43,553.25,96.75,0.0000,0.5412,-0.0352,1.0000,2 +70.001,51.18362664,-2.27383292,423.24,35.39,162.67,180.02,179.71,-2.57,33.13,550.91,96.88,0.0000,0.3647,-0.0547,1.0000,2 +70.501,51.18325708,-2.27364272,424.88,36.90,161.51,179.68,179.34,-1.70,13.31,552.43,97.02,0.0451,0.0922,0.0000,1.0000,2 +70.890,51.18293272,-2.27346319,425.60,37.61,160.99,179.99,179.65,-1.10,7.43,554.78,97.14,0.0451,0.2098,0.0000,1.0000,2 +71.390,51.18254532,-2.27325171,425.32,36.85,160.66,179.94,179.58,-0.64,2.07,554.34,97.29,0.1804,0.2177,0.0000,1.0000,2 +71.834,51.18220419,-2.27306436,424.19,33.68,160.51,180.08,179.73,-0.54,-4.18,556.32,97.42,0.1000,0.2981,0.0000,1.0000,2 +72.223,51.18189042,-2.27288917,422.73,30.61,160.42,180.28,179.90,-0.09,-14.22,556.61,97.55,0.1863,0.3333,0.0000,1.0000,2 +72.668,51.18154090,-2.27269291,419.86,27.73,161.20,180.41,180.06,-0.76,-26.38,557.96,97.68,0.3588,0.1961,0.0000,1.0000,2 +73.112,51.18117140,-2.27250417,417.95,25.95,163.83,180.36,180.16,-2.91,-30.92,559.51,97.82,0.3059,0.0314,0.0000,1.0000,3 +73.557,51.18083023,-2.27234892,419.82,27.96,166.11,180.03,179.77,-4.30,-32.52,560.18,97.95,0.1981,-0.0059,-0.0352,1.0000,3 +74.001,51.18048121,-2.27221720,424.70,32.96,167.58,179.68,179.25,-4.38,-31.89,561.54,98.08,0.0079,-0.4180,-0.1465,1.0000,3 +74.501,51.18007971,-2.27208733,430.74,38.92,168.36,179.46,179.03,-3.16,-17.39,562.53,98.23,0.1196,-0.3926,-0.1484,1.0000,3 +74.946,51.17972264,-2.27197877,434.63,42.76,169.07,179.32,178.95,-2.35,1.41,563.42,98.36,0.1079,-0.4766,-0.1484,1.0000,3 +75.390,51.17935532,-2.27186845,437.39,45.68,169.29,179.28,178.96,-2.54,28.30,564.51,98.49,0.2039,-0.3789,-0.1152,1.0000,3 +75.835,51.17900091,-2.27175391,439.22,46.02,167.97,179.10,178.93,-3.15,51.50,565.44,98.61,0.3000,-0.3125,-0.1055,1.0000,3 +76.223,51.17867972,-2.27163540,439.89,41.67,164.68,178.92,178.92,-3.75,62.70,566.42,98.74,0.4216,-0.3652,-0.0703,1.0000,3 +76.612,51.17835170,-2.27148816,440.07,38.08,158.55,178.40,178.66,-4.42,72.11,567.43,98.87,0.5176,-0.0762,-0.0625,1.0000,3 +77.001,51.17805462,-2.27130179,440.13,38.13,151.15,177.60,178.13,-4.66,77.47,568.48,99.00,0.5588,-0.2246,-0.0566,1.0000,3 +77.390,51.17776867,-2.27106121,439.73,37.69,141.97,177.01,177.69,-3.96,84.03,1085.21,99.01,0.6961,0.0726,-0.0098,1.0000,3 +77.835,51.17749561,-2.27073344,437.55,35.40,128.09,176.41,177.78,-3.33,85.12,1178.87,98.79,0.6980,0.3196,0.0000,1.0000,3 +78.279,51.17724124,-2.27025871,433.24,31.10,112.29,175.21,176.72,-3.83,77.89,1176.95,98.53,0.6118,0.2392,-0.0293,1.0000,3 +78.724,51.17708478,-2.26974222,430.65,28.58,102.44,175.61,176.48,-4.36,73.42,1172.79,98.29,0.4471,0.1706,-0.0371,1.0000,3 +79.168,51.17699489,-2.26916345,429.77,27.75,96.88,177.58,177.79,-3.73,67.55,1169.20,98.04,0.1667,0.3569,0.0000,1.0000,3 +79.612,51.17694988,-2.26861513,429.00,23.44,95.84,180.06,179.83,-1.54,54.62,1165.52,97.80,0.2216,0.1706,-0.0508,1.0000,3 +80.001,51.17692030,-2.26807855,426.35,23.45,93.94,182.38,182.02,-0.93,51.27,1162.15,97.57,0.2941,0.0687,-0.0195,1.0000,3 +80.446,51.17690060,-2.26751643,422.56,21.63,91.09,184.58,184.19,-1.37,51.24,898.58,97.36,0.6686,0.0392,-0.0195,1.0000,3 +80.890,51.17689992,-2.26690294,420.08,25.13,82.72,184.80,185.16,-6.83,52.15,563.94,97.45,0.3882,0.0000,-0.0254,1.0000,3 +81.335,51.17694356,-2.26632624,426.88,32.37,75.45,181.87,181.73,-10.20,53.53,557.50,97.58,0.2275,-0.0664,-0.0801,1.0000,3 +81.724,51.17702233,-2.26582546,439.09,44.37,73.35,181.46,180.27,-9.20,55.53,560.04,97.71,0.2784,0.0000,-0.0547,1.0000,3 +82.113,51.17712709,-2.26529331,452.09,56.33,71.07,180.22,179.27,-8.55,57.79,558.62,97.84,0.2824,-0.0430,-0.0332,1.0000,3 +82.557,51.17725242,-2.26473886,464.72,54.43,67.99,179.48,178.74,-8.22,59.68,561.47,97.98,0.1451,-0.0273,-0.0254,1.0000,3 +83.001,51.17738345,-2.26423767,475.12,45.67,66.42,178.71,178.18,-6.81,60.51,560.31,98.12,0.1157,-0.0371,-0.0156,1.0000,3 +83.390,51.17752344,-2.26374626,482.84,42.47,64.74,179.15,178.84,-5.77,61.63,1118.59,98.06,0.4569,0.0000,-0.0098,1.0000,3 +83.779,51.17767104,-2.26325753,488.93,41.84,59.19,180.10,180.10,-6.87,63.39,1165.10,97.84,0.4569,-0.1680,-0.0234,1.0000,3 +84.279,51.17787851,-2.26271891,496.99,40.85,49.23,180.48,180.74,-9.68,67.02,1160.68,97.59,0.6157,-0.3184,-0.0332,1.0000,3 +84.724,51.17812414,-2.26225577,509.97,37.48,35.31,178.84,179.10,-12.17,77.77,1156.56,97.34,0.5255,-0.1055,-0.0566,1.0000,3 +85.168,51.17839613,-2.26192411,524.79,43.44,26.79,178.70,178.33,-10.61,85.92,1151.84,97.11,0.4451,-0.1230,-0.0645,1.0000,3 +85.613,51.17871426,-2.26164736,538.00,43.26,20.83,179.62,179.39,-8.32,89.44,1148.31,96.87,0.0941,0.3529,-0.0098,1.0000,3 +86.001,51.17903522,-2.26143592,546.65,47.49,19.77,181.23,181.12,-5.75,80.93,1144.09,96.63,0.0000,0.3882,-0.0059,1.0000,3 +86.446,51.17935643,-2.26125568,551.30,49.16,19.13,183.09,183.05,-3.37,64.78,1140.15,96.41,0.0118,0.3138,0.0000,1.0000,3 +86.890,51.17971850,-2.26106284,552.50,49.41,18.08,184.64,184.65,-1.60,51.77,564.10,96.44,0.1745,0.1373,0.0000,1.0000,3 +87.335,51.18010017,-2.26087208,550.31,47.15,15.40,184.17,184.24,-1.82,46.03,546.21,96.58,0.0000,0.8686,0.0000,1.0000,3 +87.835,51.18050302,-2.26070653,548.35,44.68,13.62,184.50,184.46,-1.03,-2.88,548.92,96.72,0.4020,0.7333,0.0000,1.0000,3 +88.279,51.18088763,-2.26055657,547.99,43.66,17.68,183.71,183.98,-4.62,-64.76,547.43,96.86,0.5392,0.1255,-0.0527,1.0000,3 +88.724,51.18122908,-2.26037395,549.96,41.79,27.27,182.01,182.54,-4.29,-70.95,550.14,97.00,0.0902,-0.9180,-0.1641,1.0000,3 +89.168,51.18158257,-2.26010531,552.82,35.28,27.60,182.07,182.04,-2.41,-14.28,550.30,97.14,0.3765,-0.6367,-0.1523,1.0000,3 +89.613,51.18190915,-2.25982944,555.49,29.19,27.44,181.54,181.59,-4.93,41.91,551.49,97.28,0.4235,-0.4961,-0.1309,1.0000,3 +90.001,51.18220602,-2.25959015,560.25,27.01,22.39,180.65,180.90,-7.08,64.48,552.96,97.41,0.4431,-0.4160,-0.1543,1.0000,3 +90.446,51.18254856,-2.25937598,566.98,25.97,15.27,180.29,180.65,-7.08,76.78,1113.46,97.34,0.4196,-0.1504,-0.1367,1.0000,3 +90.890,51.18289037,-2.25922143,572.72,28.23,8.21,180.99,181.46,-6.13,82.44,1151.12,97.12,0.6157,-0.1719,-0.0508,1.0000,3 +91.279,51.18323088,-2.25913583,576.30,31.02,358.12,181.14,182.14,-5.08,88.29,1147.22,96.89,0.6431,0.0000,-0.0293,1.0000,3 +91.668,51.18356733,-2.25913611,576.93,33.04,345.68,180.87,182.28,-3.21,90.69,1143.24,96.66,0.4588,0.2706,-0.0293,1.0000,3 +92.168,51.18394590,-2.25925644,572.98,31.72,337.14,181.88,182.50,-0.86,86.80,1139.26,96.40,0.5392,0.3412,0.0000,1.0000,3 +92.557,51.18429086,-2.25947185,564.79,26.20,326.85,183.09,183.67,-0.31,81.15,1135.68,96.17,0.5765,0.0000,-0.0625,1.0000,3 +93.001,51.18460792,-2.25979815,554.88,22.53,313.07,183.22,184.19,-0.90,80.59,1132.32,95.94,0.3275,0.1471,-0.0566,1.0000,3 +93.501,51.18489241,-2.26023682,545.16,20.94,307.05,184.94,184.85,0.68,75.71,1129.45,95.68,0.4706,0.3882,0.0000,1.0000,3 +93.946,51.18513106,-2.26072762,534.17,17.63,300.02,187.11,186.94,0.04,66.86,1125.96,95.45,0.4627,0.0549,-0.1602,1.0000,3 +94.390,51.18533786,-2.26129406,524.71,15.85,292.44,188.81,188.78,-0.98,66.37,1123.38,95.19,0.4333,0.0020,-0.1699,1.0000,3 +94.890,51.18550497,-2.26194315,517.84,17.36,285.27,190.60,190.61,-1.66,67.05,1119.50,94.93,0.3667,-0.1074,-0.1719,1.0000,3 +95.335,51.18560603,-2.26252257,513.41,23.53,279.59,192.14,192.05,-1.85,67.75,1116.80,94.70,0.4000,0.0000,-0.1582,1.0000,3 +95.779,51.18567526,-2.26317508,509.58,33.35,273.48,193.61,193.53,-2.12,68.54,1113.20,94.46,0.2686,-0.2617,-0.1699,1.0000,3 +96.224,51.18570180,-2.26381375,506.14,40.53,269.90,195.30,195.02,-1.10,73.98,1109.95,94.23,0.1922,0.0000,-0.1641,1.0000,3 +96.724,51.18570106,-2.26444509,500.58,42.61,267.85,197.24,196.65,0.62,73.83,1106.20,93.97,-0.0664,0.5059,-0.0430,1.0000,3 +97.113,51.18568467,-2.26507293,491.48,40.24,266.70,199.42,198.35,2.69,49.09,1103.02,93.74,0.0000,0.5314,-0.0156,1.0000,3 +97.557,51.18565480,-2.26576592,478.36,35.98,265.27,201.85,200.43,4.09,10.76,1099.81,93.50,0.2118,0.5667,0.0000,1.0000,3 +98.057,51.18561844,-2.26645684,464.54,32.04,265.34,204.10,202.61,2.16,-33.09,1096.29,93.27,0.6549,0.2667,-0.0352,1.0000,3 +98.501,51.18559253,-2.26718215,455.34,32.36,276.18,204.07,204.26,-5.20,-50.46,1093.79,93.03,0.1726,0.0824,0.0000,1.0000,3 +99.001,51.18562678,-2.26786309,461.25,45.39,278.45,204.57,203.51,-4.80,-52.21,1090.33,92.79,-0.2891,-0.3203,-0.0684,1.0000,3 +99.446,51.18569223,-2.26852970,468.00,57.95,278.05,205.57,204.65,-2.29,-37.60,1087.24,92.56,-0.1914,-0.5859,-0.1055,1.0000,3 +99.946,51.18576580,-2.26925896,469.89,69.89,279.27,206.99,206.06,0.50,10.09,1083.69,92.33,-0.0527,-0.6855,-0.1348,1.0000,3 +100.390,51.18583766,-2.26995184,466.08,76.84,280.69,208.33,207.35,0.60,72.46,1079.82,92.10,0.3922,0.1137,-0.0273,1.0000,3 +100.835,51.18590255,-2.27058233,457.40,77.66,275.67,209.49,208.42,1.44,94.87,1076.37,91.87,0.5196,0.6078,0.0000,1.0000,3 +101.279,51.18595065,-2.27129637,442.20,70.06,265.06,210.18,208.87,1.96,70.27,1073.39,91.62,0.5471,-0.3027,-0.1621,1.0000,3 +101.724,51.18592037,-2.27197348,429.60,51.16,253.34,209.61,209.16,-1.12,72.71,1070.10,91.38,0.6961,-0.2520,-0.1230,1.0000,3 +102.168,51.18580242,-2.27264516,423.61,43.58,234.45,205.90,207.22,-4.87,77.37,1067.06,91.13,0.6118,0.0000,-0.0508,1.0000,3 +102.613,51.18558620,-2.27317068,424.11,44.20,221.27,203.76,204.25,-5.45,79.86,1064.63,90.90,0.6725,0.0000,-0.0625,1.0000,3 +103.001,51.18529925,-2.27359300,427.35,47.19,204.70,200.97,201.85,-6.42,82.26,1061.47,90.66,0.5627,-0.2227,-0.0566,1.0000,3 +103.446,51.18493388,-2.27389838,432.20,48.65,192.66,199.11,199.40,-5.33,87.22,1058.05,90.42,0.5353,0.0020,-0.0391,1.0000,3 +103.890,51.18457129,-2.27404654,434.68,46.69,183.75,199.03,199.10,-3.48,89.01,1054.90,90.19,0.3902,0.3980,0.0000,1.0000,3 +104.335,51.18417011,-2.27410708,433.57,45.46,175.58,199.36,199.20,-2.04,85.17,1051.17,89.94,0.6314,0.0883,-0.0410,1.0000,3 +104.779,51.18373154,-2.27405459,429.14,40.84,165.90,199.17,198.91,-1.11,77.35,1047.48,89.68,0.1510,0.4902,0.0000,1.0000,3 +105.224,51.18335941,-2.27391465,423.27,35.00,164.87,201.01,200.01,0.83,53.74,1044.17,89.44,0.3039,0.3039,0.0000,1.0000,3 +105.613,51.18296904,-2.27373824,415.95,27.57,162.05,202.45,201.48,0.09,38.24,1041.02,89.20,0.3529,0.1765,0.0000,1.0000,3 +106.113,51.18252790,-2.27350072,410.92,22.69,159.62,203.80,202.93,-1.45,23.71,1036.96,88.93,0.2804,0.2314,0.0000,1.0000,3 +106.557,51.18212856,-2.27325887,411.27,20.46,158.40,204.78,203.85,-2.61,11.17,1033.89,88.68,0.0902,0.4549,0.0000,1.0000,3 +107.001,51.18176610,-2.27302289,414.50,22.73,157.76,205.63,204.57,-2.21,-12.85,1030.47,88.45,0.1588,0.4765,0.0000,1.0000,3 +107.390,51.18139676,-2.27279102,417.48,25.59,157.87,206.37,205.39,-2.39,-40.55,1027.06,88.22,0.3549,0.1961,-0.0332,1.0000,3 +107.890,51.18096496,-2.27253087,419.77,27.89,161.96,207.04,206.20,-3.91,-51.88,1023.14,87.95,0.1824,-0.3438,-0.0625,1.0000,4 +108.335,51.18058494,-2.27234138,423.61,31.83,164.27,207.53,206.51,-3.60,-43.27,1019.56,87.73,0.1236,-0.5000,-0.0625,1.0000,4 +108.779,51.18016695,-2.27215403,428.47,36.64,165.41,208.27,207.18,-2.56,-12.58,1015.80,87.48,0.1961,-0.5332,-0.0664,1.0000,4 +109.224,51.17974970,-2.27198698,432.93,41.12,166.06,208.86,207.78,-3.10,20.81,1012.16,87.23,0.1922,-0.1895,-0.0684,1.0000,4 +109.724,51.17930401,-2.27180076,438.39,46.78,165.10,209.39,208.29,-3.36,32.03,1008.15,86.98,0.1236,-0.4551,-0.1191,1.0000,4 +110.113,51.17889308,-2.27161598,443.16,48.49,164.39,209.89,208.91,-2.95,55.54,1004.66,86.74,0.2235,-0.4141,-0.1191,1.0000,4 +110.613,51.17846931,-2.27141337,444.89,42.90,160.57,210.36,209.60,-3.19,78.88,1000.56,86.48,0.6471,-0.0215,-0.0273,1.0000,4 +111.057,51.17807371,-2.27118153,444.14,42.08,146.99,209.07,209.36,-3.17,86.08,996.95,86.23,0.4412,0.1451,-0.0273,1.0000,4 +111.501,51.17770495,-2.27082219,441.36,39.11,138.80,208.31,207.85,-1.28,86.56,993.32,85.98,0.6921,0.3079,0.0000,1.0000,4 +111.890,51.17739154,-2.27039270,435.40,33.14,125.65,207.54,207.69,-1.91,75.66,989.94,85.73,0.7608,0.0373,-0.0762,1.0000,4 +112.501,51.17707749,-2.26966642,433.73,31.96,103.65,201.61,202.68,-7.52,72.56,986.19,85.43,0.5647,0.0000,-0.1074,1.0000,4 +112.946,51.17696554,-2.26902144,442.05,40.44,94.57,200.61,200.15,-7.93,73.95,983.04,85.18,0.0000,0.2745,-0.0508,1.0000,4 +113.390,51.17692561,-2.26840577,452.09,46.58,92.87,200.96,199.97,-5.90,69.39,979.26,84.93,0.0000,0.2726,-0.0195,1.0000,4 +113.835,51.17690760,-2.26775498,459.80,53.99,91.76,201.55,200.73,-4.18,64.78,975.55,84.69,0.3353,0.0000,-0.1523,1.0000,4 +114.224,51.17690049,-2.26714923,463.81,63.22,88.37,201.99,201.40,-4.21,64.78,972.25,84.45,0.3784,0.0039,-0.0605,1.0000,4 +114.612,51.17691319,-2.26653214,467.35,72.52,82.66,201.97,201.55,-5.19,66.22,968.20,84.22,0.4412,0.0000,-0.1074,1.0000,4 +115.057,51.17696604,-2.26586023,473.01,78.26,75.41,201.70,201.35,-6.50,67.46,964.61,83.97,0.4255,0.0000,-0.0527,1.0000,4 +115.501,51.17706506,-2.26525254,480.87,85.75,70.68,201.49,200.76,-6.21,68.47,960.85,83.72,0.2373,0.1157,-0.0215,1.0000,4 +116.001,51.17720722,-2.26461336,488.89,74.37,67.44,201.80,201.09,-5.36,68.34,957.12,83.46,0.3353,0.0000,-0.0703,1.0000,4 +116.390,51.17736687,-2.26401341,495.21,59.80,62.24,201.84,201.37,-5.47,71.25,953.05,83.21,0.3980,-0.1367,-0.0820,1.0000,4 +116.890,51.17757157,-2.26339655,501.43,56.65,55.78,201.67,201.33,-5.43,75.15,949.07,82.95,0.3647,-0.0215,-0.0742,1.0000,4 +117.335,51.17781552,-2.26282209,506.79,52.23,48.93,201.56,201.31,-5.07,76.85,945.34,82.70,0.6255,-0.1973,-0.0605,1.0000,4 +117.835,51.17810420,-2.26230462,511.46,41.22,35.39,199.86,200.36,-6.11,80.51,941.44,82.45,0.5549,-0.1230,-0.0684,1.0000,4 +118.279,51.17843064,-2.26191553,516.79,28.66,24.33,198.36,198.69,-5.49,83.63,938.21,82.21,0.4078,0.2177,-0.0332,1.0000,4 +118.724,51.17881411,-2.26162559,520.60,23.88,18.57,198.21,198.10,-3.91,73.18,934.48,81.97,0.0981,0.5882,0.0000,1.0000,4 +119.168,51.17921251,-2.26140344,522.55,23.33,17.32,199.16,198.70,-1.76,30.88,930.93,81.71,0.2196,0.4843,0.0000,1.0000,4 +119.668,51.17965781,-2.26118805,523.94,24.25,15.99,199.94,199.46,-2.56,-5.47,926.93,81.45,0.3431,0.4667,0.0000,1.0000,4 +120.168,51.18009334,-2.26098358,528.77,29.61,17.96,200.18,199.65,-5.70,-39.69,923.28,81.19,0.3255,0.4471,0.0000,1.0000,4 +120.613,51.18047170,-2.26077988,537.27,37.12,21.41,200.05,199.33,-6.54,-58.00,919.83,80.97,0.1941,-0.3379,-0.1387,1.0000,4 +121.057,51.18082861,-2.26054743,546.81,42.24,23.31,200.15,199.35,-5.58,-50.55,916.47,80.73,0.0588,-0.3965,-0.1641,1.0000,4 +121.446,51.18120047,-2.26028871,555.22,44.44,24.17,200.35,199.70,-4.13,-28.81,912.48,80.49,0.1510,-0.5781,-0.1602,1.0000,4 +121.890,51.18157550,-2.26001050,562.26,43.47,25.27,200.71,200.07,-3.33,9.24,908.66,80.25,0.1726,-0.4258,-0.1582,1.0000,4 +122.335,51.18195235,-2.25972284,568.76,39.88,25.10,200.97,200.47,-3.59,41.01,905.40,80.02,0.2333,-0.4316,-0.1582,1.0000,4 +123.002,51.18249413,-2.25934750,574.90,32.40,19.49,201.12,200.99,-4.92,74.73,900.33,79.70,0.5274,-0.0996,-0.0469,1.0000,4 +123.502,51.18294992,-2.25910731,578.68,31.74,5.21,199.42,200.17,-5.40,82.54,896.37,79.43,0.6941,-0.0801,-0.0488,1.0000,4 +124.057,51.18342225,-2.25903573,581.87,35.49,347.53,195.91,197.39,-4.90,86.62,892.65,79.17,0.5588,-0.0332,-0.0352,1.0000,4 +124.502,51.18384078,-2.25916149,582.53,40.53,336.39,194.64,195.21,-2.76,87.94,889.33,78.93,0.5274,0.1490,0.0000,1.0000,4 +125.002,51.18426644,-2.25945547,578.26,39.75,323.05,193.55,194.28,-0.97,86.95,885.68,78.67,0.4941,0.1608,0.0000,1.0000,4 +125.613,51.18469019,-2.25993459,567.71,38.05,313.11,194.07,193.99,0.91,83.25,881.99,78.38,0.5274,0.1922,0.0000,1.0000,4 +126.113,51.18500503,-2.26046105,554.01,33.51,303.74,194.41,194.08,1.54,78.28,878.52,78.12,0.4686,0.2667,0.0000,1.0000,4 +126.613,51.18526711,-2.26105139,539.12,26.95,295.37,195.18,194.58,1.44,74.05,875.71,77.87,0.4843,0.0628,-0.0176,1.0000,4 +127.168,51.18548983,-2.26181466,522.21,19.91,284.88,195.56,195.10,0.82,69.67,871.80,77.58,0.4392,0.1804,0.0000,1.0000,4 +127.724,51.18561449,-2.26256834,509.32,20.48,276.96,196.19,195.79,-0.12,65.09,868.78,77.31,0.3471,0.2471,-0.0039,1.0000,4 +128.224,51.18567225,-2.26333640,499.81,26.57,271.31,196.90,196.52,-0.53,55.60,864.86,77.03,0.1196,0.4373,0.0000,1.0000,4 +128.891,51.18567958,-2.26429694,490.80,31.44,269.14,198.38,197.65,1.30,20.41,860.78,76.69,0.0687,0.4216,0.0000,1.0000,4 +129.446,51.18566849,-2.26505340,483.01,31.92,268.15,199.49,198.64,1.87,-7.58,856.78,76.42,0.1039,0.4118,0.0000,1.0000,4 +129.946,51.18565579,-2.26583478,473.54,32.53,268.11,200.57,199.59,2.18,-35.48,853.32,76.15,0.1451,0.3471,0.0000,1.0000,4 +130.502,51.18564852,-2.26659431,461.93,31.26,269.81,201.70,200.46,2.15,-53.61,849.43,75.89,0.4451,0.2628,0.0000,1.0000,4 +131.002,51.18565891,-2.26734332,449.22,27.19,279.02,201.86,201.28,-0.35,-60.28,846.35,75.63,0.1079,-0.4238,-0.0430,1.0000,4 +131.502,51.18572195,-2.26806385,442.07,27.27,280.55,202.55,201.63,1.00,-34.41,842.63,75.38,0.1079,-0.5938,-0.0723,1.0000,4 +132.002,51.18581118,-2.26878495,434.53,27.42,281.76,203.39,202.31,1.94,9.88,839.70,75.13,0.1804,-0.4453,-0.0801,1.0000,4 +132.502,51.18590261,-2.26951477,425.84,28.93,281.01,204.12,203.02,0.70,40.63,836.08,74.89,0.3490,-0.3594,-0.0625,1.0000,4 +132.891,51.18597269,-2.27014351,419.32,33.61,276.65,204.25,203.52,-1.65,56.63,833.35,74.66,0.3706,-0.3809,-0.0586,1.0000,4 +133.391,51.18601765,-2.27082260,415.35,40.45,269.06,203.86,203.44,-3.22,70.70,829.63,74.41,0.5745,-0.2559,-0.0352,1.0000,4 +133.835,51.18601052,-2.27149787,414.56,42.68,256.91,201.94,202.19,-5.05,75.01,826.54,74.17,0.5235,0.0000,-0.0391,1.0000,4 +134.280,51.18592333,-2.27214727,417.07,37.79,247.32,200.57,200.47,-5.33,76.79,822.99,73.91,0.6588,-0.2656,-0.0332,1.0000,4 +134.724,51.18577313,-2.27273402,421.08,41.27,232.22,197.56,198.40,-6.56,82.14,819.54,73.67,0.6039,-0.2324,-0.0215,1.0000,4 +135.113,51.18555341,-2.27323831,425.89,46.06,217.88,194.65,195.37,-5.86,86.64,816.41,73.42,0.7039,-0.0020,0.0000,1.0000,4 +135.558,51.18524741,-2.27363710,429.17,48.18,200.32,190.18,191.83,-4.83,89.01,812.50,73.18,0.6372,0.0000,0.0000,1.0000,4 +136.058,51.18486306,-2.27388997,428.86,43.01,186.09,187.52,188.44,-2.50,90.35,808.21,72.91,0.4726,0.3333,0.0000,1.0000,4 +136.502,51.18448894,-2.27397802,423.68,35.40,177.04,187.23,187.32,-0.57,84.16,804.49,72.66,0.5000,0.2843,0.0000,1.0000,4 +136.946,51.18410759,-2.27396114,415.68,27.36,169.43,187.25,187.04,-0.31,67.54,800.86,72.41,0.3451,0.5588,0.0000,1.0000,4 +137.446,51.18367920,-2.27383826,407.49,19.27,164.87,187.99,187.64,-1.80,38.63,796.88,72.13,0.2589,0.2137,-0.0371,1.0000,4 +137.891,51.18328058,-2.27366458,406.06,18.12,162.79,188.54,188.09,-2.80,27.26,793.83,71.89,0.2589,0.2333,-0.0039,1.0000,4 +138.391,51.18289179,-2.27346356,408.84,20.92,161.43,188.92,188.34,-3.30,19.30,790.12,71.64,0.0687,0.2451,0.0000,1.0000,4 +138.835,51.18250944,-2.27325208,413.31,24.94,160.70,189.29,188.63,-2.61,12.75,786.28,71.37,0.0471,0.1510,0.0000,1.0000,4 +139.280,51.18213044,-2.27303602,416.93,26.18,160.24,189.66,189.03,-2.11,11.20,782.57,71.12,0.0000,0.1314,0.0000,1.0000,4 +139.724,51.18175700,-2.27282255,419.39,27.46,159.88,190.05,189.46,-1.66,10.95,778.68,70.86,0.0000,0.1961,0.0000,1.0000,4 +140.224,51.18137222,-2.27258905,420.75,28.78,159.41,190.53,189.94,-1.30,3.54,774.70,70.60,0.1804,0.2745,0.0000,1.0000,4 diff --git a/track_data/flight_20260901_220613_021195.csv b/track_data/flight_20260901_220613_021195.csv new file mode 100644 index 00000000..98ab0ca6 --- /dev/null +++ b/track_data/flight_20260901_220613_021195.csv @@ -0,0 +1,314 @@ +# aircraft_title=Extra 300S Paint2 +# recorded_at=2026-09-01T22:06:13 +# laps_s=29.507,34.882,34.889,32.667 +timestamp_s,latitude,longitude,altitude_ft,agl_ft,heading_deg,airspeed_kts,groundspeed_kts,pitch_deg,bank_deg,torque,health_pct,stick_pitch,stick_roll,stick_yaw,stick_throttle,lap +0.000,51.18978503,-2.27596725,984.06,620.71,166.19,183.22,182.08,6.51,0.12,1115.10,100.00,-0.1582,0.0236,0.0000,1.0000,1 +0.389,51.18944292,-2.27583371,966.12,598.89,166.07,186.32,184.99,7.53,0.14,1168.66,100.00,-0.2383,0.0236,0.0000,1.0000,1 +0.833,51.18908347,-2.27569206,946.54,576.35,165.94,189.42,187.41,9.54,0.16,1173.19,100.00,-0.1836,0.0432,0.0000,1.0000,1 +1.222,51.18873986,-2.27555739,922.36,549.11,165.94,192.71,189.66,10.53,0.16,1174.41,100.00,0.0000,0.0039,0.0000,1.0000,1 +1.611,51.18840063,-2.27542401,896.41,520.27,165.99,195.93,192.48,10.66,0.16,1174.68,100.00,0.0000,0.0000,0.0000,1.0000,1 +2.056,51.18802548,-2.27527565,862.34,481.89,166.00,199.48,195.65,10.85,0.15,1177.24,100.00,-0.1211,0.0216,-0.0547,1.0000,1 +2.611,51.18753504,-2.27508161,828.97,447.69,165.99,203.81,199.72,10.96,0.16,1176.78,100.00,-0.1641,0.0314,-0.0508,1.0000,1 +3.056,51.18714588,-2.27492768,798.43,417.21,165.96,207.19,202.77,11.34,0.16,1179.42,100.00,-0.1309,0.1137,-0.0117,1.0000,1 +3.500,51.18671383,-2.27475624,763.06,381.69,165.96,210.81,205.90,11.48,0.16,1178.86,100.00,-0.0957,0.1334,0.0000,1.0000,1 +3.889,51.18635216,-2.27460975,732.95,351.50,165.96,213.91,208.63,11.38,0.14,1181.14,100.00,-0.1230,0.1588,0.0000,1.0000,1 +4.333,51.18595458,-2.27445431,700.49,319.16,165.96,216.88,211.61,11.25,-0.56,1181.31,100.00,-0.1230,0.0981,0.0000,1.0000,1 +4.722,51.18556721,-2.27430052,669.75,289.11,165.96,219.87,214.30,11.25,-0.81,1180.67,100.00,-0.1816,0.0961,0.0000,1.0000,1 +5.167,51.18515864,-2.27413788,636.40,255.28,165.95,222.72,216.85,11.63,-0.83,1183.86,100.00,-0.0117,0.0392,0.0000,1.0000,1 +5.556,51.18475564,-2.27397773,604.07,215.53,166.00,225.42,219.34,11.48,-0.83,1183.31,100.00,-0.0898,0.0275,0.0000,1.0000,1 +6.000,51.18432324,-2.27381126,568.44,179.05,166.04,228.39,222.08,11.15,-0.83,1186.07,100.00,0.0000,0.0000,0.0000,1.0000,1 +6.389,51.18392502,-2.27365058,537.61,148.30,166.07,230.72,224.66,10.82,-0.83,1186.03,100.00,0.0471,0.0765,0.0000,1.0000,1 +6.834,51.18350061,-2.27348283,504.24,114.90,166.11,233.43,227.17,10.44,-0.84,1186.49,100.00,0.1118,0.0334,0.0000,1.0000,1 +7.222,51.18306457,-2.27331165,472.65,82.71,166.16,235.70,229.66,9.66,-0.84,1188.93,100.00,0.2706,0.0000,0.0000,1.0000,1 +7.667,51.18264705,-2.27314767,444.13,54.39,166.38,237.49,232.68,6.74,-0.86,1188.44,100.00,0.1079,0.0000,0.0000,1.0000,1 +8.056,51.18222857,-2.27298390,422.29,30.98,166.47,239.10,235.44,4.30,-0.86,1191.20,100.00,0.3098,0.0000,0.0000,1.0000,1 +8.445,51.18178595,-2.27280987,407.86,15.69,166.65,239.93,237.56,0.80,-0.89,1191.10,100.00,0.2373,0.0000,0.0000,1.0000,1 +8.834,51.18136714,-2.27264919,403.66,11.61,166.68,240.54,238.36,-1.12,-0.88,1191.37,100.00,0.0000,-0.2578,0.0000,1.0000,1 +9.278,51.18093217,-2.27248309,404.41,12.49,166.63,240.98,238.73,-1.57,1.71,1192.12,100.00,0.0000,-0.1914,0.0000,1.0000,1 +9.667,51.18051509,-2.27232809,406.75,14.89,166.58,241.30,239.01,-2.04,4.73,1191.85,100.00,0.0000,-0.2402,0.0000,1.0000,1 +10.056,51.18006823,-2.27215576,410.73,18.83,166.46,241.53,239.23,-2.55,12.90,1191.40,100.00,0.0000,-0.2793,0.0000,1.0000,1 +10.445,51.17965018,-2.27199481,415.59,23.88,166.06,241.72,239.38,-3.01,24.00,1191.14,100.00,0.0000,-0.3379,0.0000,1.0000,1 +10.834,51.17922067,-2.27180984,421.42,29.99,165.29,241.81,239.52,-3.37,38.27,1190.91,100.00,0.0157,-0.3477,0.0000,1.0000,1 +11.223,51.17879659,-2.27163074,426.69,31.39,164.12,241.92,239.69,-3.52,54.79,1190.29,100.00,0.3235,-0.3789,-0.0156,1.0000,1 +11.667,51.17837811,-2.27142438,431.51,29.82,159.69,241.83,239.80,-4.46,73.67,1190.15,100.00,0.4588,-0.0410,-0.0059,1.0000,1 +12.056,51.17796090,-2.27117510,436.33,34.54,151.40,241.04,239.42,-4.80,80.08,1189.54,100.00,0.5529,0.0000,-0.0020,1.0000,1 +12.445,51.17756998,-2.27084254,441.74,39.86,138.52,239.03,237.96,-5.50,82.19,1189.41,100.00,0.7098,0.0000,-0.0039,1.0000,1 +12.889,51.17718859,-2.27029896,449.82,48.17,115.68,232.34,233.04,-7.07,84.97,1189.38,100.00,0.5314,0.0000,0.0000,1.0000,1 +13.334,51.17696293,-2.26966316,458.65,56.81,105.61,229.89,228.80,-5.66,86.55,1189.20,100.00,0.2667,0.0471,0.0000,1.0000,1 +13.723,51.17683754,-2.26904958,464.80,62.99,102.68,230.45,228.90,-3.87,87.34,1189.51,100.00,0.4627,0.0647,0.0000,1.0000,1 +14.112,51.17673857,-2.26835678,468.01,57.69,94.52,230.20,229.11,-2.77,87.78,1188.91,100.00,0.6039,0.2216,0.0000,1.0000,1 +14.556,51.17669639,-2.26765166,467.67,51.79,81.79,229.10,228.63,-2.44,84.02,1188.91,100.00,0.3922,0.5000,0.0785,1.0000,1 +15.000,51.17674974,-2.26691527,466.46,71.46,74.58,228.11,226.93,-2.21,56.50,1188.96,100.00,0.0451,0.5745,0.0079,1.0000,1 +15.445,51.17687876,-2.26614400,467.26,72.31,73.36,229.26,227.73,-1.40,21.32,1189.02,100.00,-0.0449,-0.1855,0.0000,1.0000,1 +15.889,51.17700510,-2.26547659,468.83,73.87,72.89,230.07,228.48,-1.68,25.81,1189.01,100.00,0.2471,-0.5332,-0.0078,1.0000,1 +16.334,51.17714769,-2.26474534,470.97,60.09,70.67,230.67,229.18,-3.90,47.81,1188.98,100.00,0.4412,-0.3164,0.0000,1.0000,1 +16.723,51.17729315,-2.26413887,476.21,45.31,64.45,230.20,228.81,-7.38,57.21,1188.94,100.00,0.6333,-0.0293,-0.0117,1.0000,1 +17.167,51.17747991,-2.26351293,489.89,49.49,56.01,228.76,226.70,-10.65,62.09,1188.53,100.00,0.1647,-0.4648,-0.1172,1.0000,1 +17.556,51.17771673,-2.26293915,510.16,64.34,52.32,228.54,225.45,-9.89,82.50,1188.55,100.00,0.5529,-0.3984,-0.0430,1.0000,1 +18.001,51.17801577,-2.26235035,530.55,65.94,35.34,225.63,224.91,-7.41,98.24,1187.42,100.00,0.6353,0.0000,-0.0410,1.0000,1 +18.445,51.17837233,-2.26188981,540.60,53.98,20.23,221.57,222.01,-3.15,99.56,1186.57,100.00,0.0334,0.4863,0.0000,1.0000,1 +18.889,51.17878995,-2.26160770,540.33,42.68,19.47,223.00,222.00,-0.53,76.04,1186.90,100.00,0.0000,0.5784,0.0000,1.0000,1 +19.334,51.17921127,-2.26136637,535.43,34.66,17.71,224.46,223.19,0.95,26.87,1186.22,100.00,0.0000,0.5451,0.0000,1.0000,1 +19.834,51.17967584,-2.26113008,529.69,28.01,16.74,225.89,224.59,0.76,-16.19,1186.20,100.00,0.3451,0.4196,0.0000,1.0000,1 +20.278,51.18013508,-2.26089641,525.28,23.21,19.70,226.91,225.80,-2.21,-46.85,1186.63,100.00,0.2549,-0.1406,-0.1348,1.0000,1 +20.723,51.18054346,-2.26065062,525.73,20.22,22.71,227.54,226.35,-2.63,-43.09,1186.61,100.00,0.1412,-0.5312,-0.1602,1.0000,1 +21.112,51.18095472,-2.26037549,528.51,18.21,24.02,228.39,227.03,-2.35,-12.53,1186.68,100.00,0.3039,-0.2656,-0.1133,1.0000,1 +21.556,51.18137024,-2.26007870,534.02,15.98,24.76,228.78,227.25,-5.41,6.96,1186.66,100.00,0.1412,-0.5332,-0.1680,1.0000,1 +21.945,51.18172859,-2.25982218,544.68,18.20,24.21,228.88,226.88,-6.12,38.25,1186.62,100.00,0.2353,-0.5371,-0.1680,1.0000,1 +22.334,51.18211140,-2.25955894,556.63,20.98,21.84,228.94,227.15,-7.03,71.50,1185.80,100.00,0.5274,0.0000,-0.0430,1.0000,1 +22.778,51.18257483,-2.25928406,570.09,27.69,11.69,228.10,226.84,-7.46,81.46,1185.77,100.00,0.4902,-0.4355,-0.0527,1.0000,1 +23.223,51.18300142,-2.25913750,581.24,35.53,359.49,226.44,225.83,-6.46,91.79,1184.56,100.00,0.6471,0.1059,0.0000,1.0000,1 +23.612,51.18343535,-2.25912952,588.77,44.63,344.07,222.97,223.56,-3.78,95.63,1184.51,100.00,0.4510,0.2314,0.0000,1.0000,1 +24.056,51.18386465,-2.25929790,589.11,48.64,334.36,222.54,222.31,-0.98,92.50,1184.49,100.00,0.5647,0.0177,-0.0020,1.0000,1 +24.445,51.18422691,-2.25955899,583.48,46.81,323.37,221.87,221.61,0.66,89.67,1184.16,100.00,0.5137,0.3294,0.0000,1.0000,1 +24.890,51.18461479,-2.25997595,572.51,43.60,313.54,222.02,221.27,1.22,80.48,1182.15,99.84,0.4627,0.2765,0.0000,1.0000,1 +25.334,51.18493448,-2.26050323,560.70,41.09,303.31,222.24,221.44,0.10,72.36,1179.91,99.62,0.3118,-0.0137,-0.0195,1.0000,1 +25.834,51.18520286,-2.26113612,551.23,40.73,297.41,223.00,222.03,0.08,72.03,1176.80,99.39,0.2255,-0.2891,-0.0391,1.0000,1 +26.223,51.18541849,-2.26178242,542.70,40.35,292.40,224.29,223.07,0.54,81.55,1174.83,99.18,0.5137,0.0000,0.0000,1.0000,1 +26.723,51.18560295,-2.26250780,531.51,41.33,280.95,224.38,223.47,1.05,85.40,1171.65,98.95,0.3647,0.2530,0.0000,1.0000,1 +27.167,51.18570424,-2.26323038,518.11,42.78,272.47,225.03,223.52,2.31,84.95,1169.34,98.73,0.1863,0.4431,0.0000,1.0000,1 +27.723,51.18573576,-2.26408788,499.43,37.40,267.24,226.64,224.37,3.50,65.45,1165.99,98.48,0.3216,0.3745,0.0000,1.0000,1 +28.167,51.18571276,-2.26485176,483.12,29.33,262.98,227.68,225.65,2.27,35.61,600.29,98.43,0.1784,0.6314,0.0000,1.0000,1 +28.612,51.18565735,-2.26557990,472.29,27.84,262.04,227.51,225.81,1.00,-17.64,561.39,98.55,0.3529,0.4235,-0.0059,1.0000,1 +29.056,51.18560162,-2.26630126,466.96,33.16,265.70,226.14,224.94,-2.70,-47.40,565.10,98.67,0.3745,0.4392,0.0000,1.0000,1 +29.501,51.18557042,-2.26708435,468.65,44.16,272.56,223.56,222.50,-3.99,-73.19,566.42,98.80,0.3353,0.0000,-0.0918,1.0000,1 +29.945,51.18558692,-2.26776406,472.07,54.60,277.54,221.56,220.47,-2.93,-75.39,568.10,98.93,0.0334,-0.5156,-0.0820,1.0000,1 +30.390,51.18564604,-2.26851927,473.82,64.50,279.20,220.02,218.85,-1.39,-41.96,568.67,99.06,-0.2637,-0.6035,-0.0938,1.0000,1 +30.834,51.18571678,-2.26920209,473.76,73.02,280.25,218.76,217.57,0.72,9.68,569.58,99.19,-0.1914,-0.6367,-0.1074,1.0000,1 +31.278,51.18578993,-2.26986846,469.80,79.06,281.18,217.72,216.47,0.63,66.35,570.67,99.30,0.3588,-0.4766,-0.0449,1.0000,1 +31.667,51.18586336,-2.27052377,461.21,80.51,273.19,216.33,215.32,1.50,101.37,571.41,99.43,0.3922,0.7372,0.0000,1.0000,1 +32.112,51.18589857,-2.27121567,445.09,71.82,264.04,214.60,212.45,4.87,83.55,572.61,99.56,0.4569,0.6510,0.0000,1.0000,1 +32.501,51.18586891,-2.27183237,426.87,49.23,255.91,213.35,211.48,1.76,54.10,887.51,99.65,0.4549,-0.6719,-0.2734,1.0000,1 +32.945,51.18576803,-2.27250737,415.74,35.56,246.28,212.11,211.61,-2.41,66.33,1182.67,99.47,0.7157,-0.2070,-0.1953,1.0000,1 +33.390,51.18560740,-2.27309642,414.44,34.56,228.03,208.60,209.85,-6.89,76.30,1186.19,99.24,0.5804,-0.4453,-0.1914,1.0000,1 +33.834,51.18534344,-2.27361093,420.35,40.61,210.88,204.82,205.49,-6.72,86.77,1181.70,99.01,0.6431,-0.2617,-0.1426,1.0000,1 +34.223,51.18500894,-2.27397015,426.08,44.70,194.98,202.00,202.94,-4.88,91.91,1177.66,98.79,0.7372,0.0000,-0.0898,1.0000,1 +34.612,51.18465036,-2.27415172,427.27,39.19,179.54,199.39,200.72,-2.49,92.83,1174.41,98.58,0.4078,0.3431,0.0000,1.0000,1 +35.112,51.18425179,-2.27418615,422.58,34.26,172.87,200.62,200.15,-0.18,84.34,1171.28,98.36,0.4137,0.3588,0.0000,1.0000,1 +35.556,51.18382644,-2.27411268,413.91,25.74,165.73,201.93,201.20,-0.02,71.45,651.18,98.23,0.5235,0.2216,-0.0352,1.0000,1 +35.945,51.18344843,-2.27396371,406.74,18.52,159.25,201.82,201.30,-1.15,53.97,564.97,98.35,0.0432,0.7000,0.0000,1.0000,1 +36.445,51.18302564,-2.27371187,402.58,14.54,157.99,201.22,200.34,-0.43,6.75,565.34,98.48,0.2137,0.1686,-0.1152,1.0000,1 +36.890,51.18266826,-2.27348271,401.30,13.30,157.78,200.89,200.06,-1.41,-3.69,567.68,98.61,0.0196,0.3157,0.0000,1.0000,1 +37.334,51.18227459,-2.27322934,402.12,12.09,157.93,200.08,199.23,-1.69,-17.24,567.14,98.74,0.0255,0.3157,0.0000,1.0000,1 +37.723,51.18191797,-2.27300470,403.32,11.37,158.48,199.50,198.69,-1.89,-28.58,569.10,98.86,0.2275,0.2373,0.0000,1.0000,1 +38.167,51.18155801,-2.27278875,404.59,12.66,160.28,198.79,198.06,-2.87,-37.66,569.28,98.98,0.2490,0.2079,0.0000,1.0000,1 +38.556,51.18119388,-2.27259432,407.07,15.20,162.62,198.00,197.26,-3.64,-42.40,570.45,99.10,0.1216,-0.2109,-0.0234,1.0000,1 +38.945,51.18085251,-2.27242990,410.42,18.56,163.88,197.35,196.59,-3.18,-42.29,571.26,99.22,0.1490,-0.0293,0.0000,1.0000,2 +39.390,51.18046974,-2.27226061,413.54,21.63,165.26,196.67,195.97,-2.51,-34.61,572.22,99.35,0.1726,-0.4590,-0.0449,1.0000,2 +39.834,51.18009773,-2.27211357,415.57,23.62,166.61,196.19,195.47,-2.38,-14.40,573.36,99.47,0.1765,-0.4141,-0.0586,1.0000,2 +40.223,51.17972383,-2.27197561,418.59,26.76,167.29,195.51,194.79,-3.59,7.26,574.16,99.59,0.1667,-0.3906,-0.0664,1.0000,2 +40.667,51.17934508,-2.27183532,424.17,32.61,166.90,195.23,194.34,-4.28,27.32,1115.34,99.59,0.0844,-0.4043,-0.1191,1.0000,2 +41.056,51.17901059,-2.27170656,430.14,37.36,166.29,195.92,195.09,-4.02,46.35,1187.91,99.39,0.1510,-0.4648,-0.1191,1.0000,2 +41.501,51.17863579,-2.27154911,434.55,35.43,164.63,197.02,196.41,-3.84,64.96,1184.84,99.16,0.3235,-0.3320,-0.0762,1.0000,2 +41.945,51.17825809,-2.27136434,436.56,34.57,159.74,198.32,197.90,-3.65,81.64,1180.16,98.93,0.5176,0.0000,-0.0195,1.0000,2 +42.334,51.17789694,-2.27115478,436.07,33.99,150.67,199.04,199.03,-2.66,86.57,1176.16,98.70,0.6451,0.3000,0.0000,1.0000,2 +42.723,51.17758304,-2.27089133,433.10,31.02,137.31,198.21,198.93,-2.54,83.97,1172.75,98.49,0.7039,0.1588,0.0000,1.0000,2 +43.167,51.17727449,-2.27045867,428.94,26.81,117.76,194.86,196.50,-3.97,79.68,1169.37,98.26,0.5961,0.2961,0.0000,1.0000,2 +43.612,51.17707391,-2.26995002,427.81,25.85,105.24,193.09,194.10,-5.46,73.99,1167.10,98.06,0.2157,0.3784,0.0000,1.0000,2 +44.056,51.17695181,-2.26932919,430.46,28.59,101.36,194.74,194.32,-4.36,67.13,1163.85,97.82,0.3490,-0.0312,-0.1602,1.0000,2 +44.501,51.17687219,-2.26870605,433.46,30.74,95.61,195.49,195.17,-4.98,67.01,581.58,97.81,0.3039,-0.1875,-0.1582,1.0000,2 +44.945,51.17683223,-2.26809329,437.42,30.44,90.70,194.78,194.41,-4.90,70.54,563.98,97.93,0.3471,0.0000,-0.0469,1.0000,2 +45.334,51.17682763,-2.26749693,441.03,37.22,85.30,193.51,193.25,-4.68,72.96,560.18,98.07,0.2843,0.0314,0.0000,1.0000,2 +45.723,51.17685258,-2.26692671,443.52,47.70,81.80,192.68,192.41,-3.85,69.57,563.21,98.19,0.1236,0.3784,0.0000,1.0000,2 +46.167,51.17690908,-2.26630025,444.63,49.63,80.01,192.36,191.94,-2.25,57.51,562.58,98.32,0.3098,0.2589,0.0000,1.0000,2 +46.612,51.17698480,-2.26567754,444.03,49.04,75.67,191.59,191.33,-3.80,50.70,563.83,98.45,0.4510,0.1745,0.0000,1.0000,2 +47.056,51.17708115,-2.26508264,446.68,51.57,70.56,190.45,190.28,-6.16,49.99,565.64,98.58,0.3196,-0.0801,-0.0156,1.0000,2 +47.501,51.17721083,-2.26450598,454.00,35.21,67.73,189.47,188.88,-6.26,51.18,565.74,98.71,0.3529,-0.2520,-0.0586,1.0000,2 +47.945,51.17735414,-2.26396828,462.24,26.61,63.65,188.80,188.24,-7.52,57.98,1029.20,98.75,0.4353,-0.3125,-0.0586,1.0000,2 +48.334,51.17751369,-2.26346161,471.91,29.60,57.35,188.67,188.16,-8.89,68.08,1173.01,98.56,0.4980,-0.1387,-0.0605,1.0000,2 +48.778,51.17771263,-2.26296818,483.82,34.75,49.19,188.71,188.23,-9.46,75.05,1171.99,98.33,0.4431,-0.2090,-0.0605,1.0000,2 +49.223,51.17798014,-2.26246300,497.55,32.98,41.29,189.06,188.51,-8.67,81.76,1167.39,98.09,0.4275,-0.0293,-0.0586,1.0000,2 +49.667,51.17826154,-2.26205755,508.57,29.30,32.91,189.41,189.26,-7.49,84.67,1162.27,97.86,0.4922,0.0000,-0.0293,1.0000,2 +50.112,51.17859189,-2.26169802,516.81,22.79,25.00,189.88,189.95,-6.03,86.23,1158.77,97.63,0.2628,0.3549,0.0000,1.0000,2 +50.556,51.17892499,-2.26143816,521.42,20.27,21.11,191.29,191.22,-4.15,82.82,1154.68,97.41,0.3980,0.0530,-0.0508,1.0000,2 +50.945,51.17923586,-2.26123646,522.80,18.37,16.13,192.44,192.47,-3.11,78.45,870.68,97.23,0.0000,0.8039,0.0000,1.0000,2 +51.390,51.17959838,-2.26106377,522.01,17.14,14.49,193.59,193.31,-1.12,25.56,559.67,97.32,0.0608,0.6961,0.0000,1.0000,2 +51.834,51.18001700,-2.26089629,521.18,17.59,13.96,192.84,192.58,-2.38,-25.69,553.97,97.45,0.3824,0.3608,0.0000,1.0000,2 +52.278,51.18041865,-2.26072444,523.77,20.39,18.54,192.03,191.90,-6.02,-43.41,556.63,97.58,0.1334,0.0000,0.0000,1.0000,2 +52.723,51.18078485,-2.26052637,531.99,26.51,20.75,190.76,190.15,-5.89,-46.41,555.28,97.71,0.0000,-0.2500,0.0000,1.0000,2 +53.167,51.18115113,-2.26029631,540.95,31.08,21.95,189.97,189.47,-4.85,-35.16,557.77,97.84,-0.0078,-0.4395,-0.0078,1.0000,2 +53.612,51.18150223,-2.26006064,548.33,31.59,23.15,189.30,188.87,-4.10,-9.52,557.21,97.96,0.0353,-0.6543,-0.0137,1.0000,2 +54.056,51.18183068,-2.25982840,555.03,28.60,23.88,188.57,188.18,-4.12,33.47,558.40,98.09,0.2059,-0.2832,-0.0176,1.0000,2 +54.445,51.18214819,-2.25961780,560.27,26.93,22.12,187.95,187.86,-5.14,54.22,559.52,98.20,0.3765,-0.3105,-0.0273,1.0000,2 +54.890,51.18249775,-2.25940498,565.55,25.44,16.47,186.83,187.01,-6.34,70.78,559.91,98.33,0.5980,-0.1367,-0.0332,1.0000,2 +55.278,51.18286024,-2.25923892,570.94,26.83,6.17,184.97,185.72,-7.21,79.79,849.22,98.44,0.5373,-0.0977,-0.0586,1.0000,2 +55.723,51.18322333,-2.25916515,576.40,31.75,355.84,184.40,185.27,-6.05,85.28,1160.02,98.25,0.4863,-0.2734,-0.0332,1.0000,2 +56.167,51.18359235,-2.25919146,579.53,36.52,345.37,184.35,185.32,-4.43,88.60,1163.50,98.02,0.5941,0.0647,0.0000,1.0000,2 +56.556,51.18391301,-2.25930533,579.36,38.92,334.49,184.02,185.22,-2.77,89.96,1159.63,97.82,0.4177,0.0608,0.0000,1.0000,2 +57.001,51.18424724,-2.25953797,575.09,37.55,327.37,185.46,185.85,-0.31,89.71,1155.78,97.59,0.6039,0.2510,0.0000,1.0000,2 +57.445,51.18459921,-2.25987858,565.15,33.83,318.46,187.20,187.14,1.27,82.39,1152.07,97.36,0.4353,0.1039,-0.0137,1.0000,2 +57.890,51.18491158,-2.26030467,551.35,27.99,311.22,189.12,188.55,2.16,78.71,596.62,97.29,0.4275,0.4039,0.0000,1.0000,2 +58.334,51.18516677,-2.26076439,536.18,20.27,301.14,188.83,188.50,1.16,72.55,557.28,97.41,0.6471,0.1941,0.0000,1.0000,2 +58.834,51.18539182,-2.26133909,523.28,14.83,287.93,186.52,187.16,-1.54,70.25,553.72,97.55,0.3275,0.1196,-0.0098,1.0000,2 +59.334,51.18553722,-2.26199273,514.89,15.07,282.50,186.03,186.02,-0.68,69.72,555.44,97.69,0.4039,0.2000,0.0000,1.0000,2 +59.834,51.18562395,-2.26263011,506.95,19.23,276.33,186.99,186.89,-0.95,66.50,1128.96,97.64,0.2804,0.0000,-0.0586,1.0000,2 +60.279,51.18566959,-2.26322996,500.15,25.00,272.76,188.63,188.32,-0.13,65.68,1157.98,97.42,0.2589,0.4098,0.0000,1.0000,2 +60.723,51.18568718,-2.26390406,491.78,27.12,270.64,191.14,190.47,1.28,56.87,1154.27,97.18,0.0000,0.2589,-0.0430,1.0000,2 +61.167,51.18568764,-2.26450124,481.95,24.49,269.07,192.92,192.02,2.43,44.40,572.05,97.20,0.1412,0.4314,0.0000,1.0000,2 +61.667,51.18567267,-2.26523799,468.43,19.44,267.22,193.13,192.11,2.92,19.15,554.82,97.35,0.1314,0.3765,0.0000,1.0000,2 +62.167,51.18564649,-2.26597709,456.03,16.81,266.29,193.66,192.72,1.86,-0.36,557.44,97.49,0.2745,0.3980,0.0000,1.0000,2 +62.779,51.18561319,-2.26685214,447.46,21.07,267.59,193.31,192.81,-1.70,-31.32,557.62,97.66,0.3372,0.4235,0.0000,1.0000,2 +63.334,51.18559878,-2.26759786,447.20,28.71,271.92,193.93,193.53,-3.76,-58.53,1153.56,97.49,0.4275,0.3510,0.0000,1.0000,2 +63.890,51.18562252,-2.26838986,450.19,39.94,280.26,194.74,194.45,-3.88,-73.88,722.99,97.29,0.1510,-0.2812,0.0000,1.0000,2 +64.390,51.18569579,-2.26908416,452.25,49.72,282.63,194.95,194.48,-2.13,-62.07,560.76,97.40,0.0000,-0.4824,-0.0117,1.0000,2 +64.945,51.18581775,-2.26990560,450.67,61.20,284.42,194.36,193.78,-0.32,-21.91,555.82,97.55,0.0000,-0.5391,-0.0078,1.0000,2 +65.445,51.18593084,-2.27059088,448.08,69.43,285.42,193.82,193.22,-0.20,17.14,557.28,97.68,0.0765,-0.5762,-0.0059,1.0000,2 +66.056,51.18606211,-2.27138527,443.91,72.91,283.04,193.43,192.98,-1.65,70.16,600.32,97.84,0.7921,-0.0039,0.0000,1.0000,2 +66.501,51.18614913,-2.27206158,439.10,61.59,261.66,190.42,192.60,-5.48,80.46,1156.41,97.67,0.9961,-0.1348,-0.0059,1.0000,2 +67.001,51.18611552,-2.27267515,438.92,59.02,231.87,178.93,184.74,-9.52,87.03,1160.84,97.46,0.8059,-0.0664,-0.0391,1.0000,2 +67.501,51.18587028,-2.27324667,441.38,61.39,205.03,171.97,176.12,-6.38,92.79,1156.87,97.21,0.5412,0.1784,0.0000,1.0000,2 +68.001,51.18554786,-2.27354372,438.46,58.23,191.94,171.33,172.75,-1.96,85.84,1153.71,96.98,0.4000,0.6608,0.0569,1.0000,2 +68.501,51.18514502,-2.27370657,430.43,47.55,188.86,174.88,174.69,-0.19,48.03,1150.12,96.73,0.3216,0.0000,-0.1582,1.0000,2 +69.001,51.18475838,-2.27379695,423.63,36.18,186.37,177.94,177.57,-0.34,42.74,1147.13,96.50,0.3275,-0.2852,-0.1699,1.0000,2 +69.501,51.18432904,-2.27385999,417.99,29.83,182.51,180.36,180.12,-1.43,54.61,596.38,96.41,0.4863,0.0000,-0.1445,1.0000,2 +69.945,51.18398112,-2.27387684,414.50,26.45,176.37,180.07,180.19,-3.42,61.53,553.42,96.53,0.4314,-0.0547,-0.1562,1.0000,2 +70.390,51.18360116,-2.27383515,413.58,25.61,169.02,179.17,179.44,-4.88,65.12,549.02,96.66,0.2922,0.1039,-0.0605,1.0000,2 +70.834,51.18324622,-2.27372440,415.05,27.11,164.93,178.42,178.42,-4.21,62.42,550.99,96.79,0.0000,0.4177,-0.0078,1.0000,2 +71.279,51.18288338,-2.27356832,416.22,28.23,163.42,178.78,178.57,-2.37,44.05,553.30,96.93,0.0000,0.4177,0.0000,1.0000,2 +71.668,51.18254656,-2.27339896,416.02,27.89,162.00,178.72,178.42,-1.44,22.88,552.65,97.04,0.1334,0.2412,0.0000,1.0000,2 +72.168,51.18219940,-2.27321168,415.49,24.99,161.05,178.94,178.62,-1.21,14.28,554.80,97.17,0.0275,0.3608,0.0000,1.0000,2 +72.612,51.18183322,-2.27300587,415.02,23.01,160.42,179.07,178.73,-1.06,4.55,554.91,97.31,0.0804,0.3451,0.0000,1.0000,2 +73.057,51.18148680,-2.27280758,414.54,22.52,160.07,179.13,178.78,-0.97,-9.91,556.20,97.44,0.2039,0.3726,0.0000,1.0000,2 +73.557,51.18109056,-2.27258785,413.91,21.90,160.83,179.25,178.95,-1.92,-27.83,557.78,97.59,0.2647,0.2471,0.0000,1.0000,3 +74.057,51.18070739,-2.27238335,414.43,22.50,163.41,179.08,178.86,-3.41,-35.36,558.46,97.73,0.2608,0.0000,0.0000,1.0000,3 +74.557,51.18031506,-2.27220662,417.54,25.69,165.40,178.83,178.51,-3.61,-32.93,559.70,97.86,0.0000,-0.3320,-0.0039,1.0000,3 +74.946,51.17998079,-2.27207578,420.91,29.05,166.31,178.76,178.42,-2.87,-21.96,560.68,97.98,0.2177,-0.2617,0.0000,1.0000,3 +75.446,51.17961111,-2.27193997,424.17,32.33,167.47,178.63,178.28,-3.22,-5.27,561.61,98.11,0.2039,-0.5156,0.0000,1.0000,3 +75.835,51.17926022,-2.27182190,428.16,36.73,167.74,178.43,178.05,-3.92,21.22,562.62,98.23,0.1961,-0.4277,0.0000,1.0000,3 +76.279,51.17890136,-2.27169090,433.38,38.79,166.68,178.11,177.77,-4.51,44.79,563.50,98.36,0.1706,-0.3906,0.0000,1.0000,3 +76.779,51.17854344,-2.27154116,437.74,37.49,164.75,177.81,177.69,-4.22,63.31,564.55,98.49,0.3686,-0.2773,-0.0176,1.0000,3 +77.223,51.17820014,-2.27138103,439.51,37.53,158.99,177.49,177.69,-4.54,76.16,778.40,98.61,0.5314,-0.2852,-0.0176,1.0000,3 +77.612,51.17786576,-2.27117569,439.52,37.49,148.28,177.65,178.36,-4.19,84.44,1166.45,98.43,0.5608,0.2608,0.0000,1.0000,3 +78.057,51.17756965,-2.27090926,437.75,35.69,135.43,177.21,178.46,-3.52,84.42,1172.48,98.21,0.6176,0.2333,0.0000,1.0000,3 +78.501,51.17729187,-2.27048993,434.01,31.77,119.77,176.56,177.92,-3.42,81.66,1168.24,97.96,0.6039,0.2726,0.0000,1.0000,3 +78.946,51.17709127,-2.27001400,430.41,28.29,107.87,176.47,177.52,-3.79,77.58,1164.74,97.74,0.5823,0.2549,0.0000,1.0000,3 +79.446,51.17695649,-2.26939522,427.85,25.80,99.83,177.77,178.14,-3.47,68.42,1160.97,97.49,0.2843,0.2118,0.0000,1.0000,3 +79.890,51.17689005,-2.26882025,426.36,24.16,95.76,180.20,180.14,-3.02,64.29,1157.58,97.25,0.2706,0.1608,0.0000,1.0000,3 +80.335,51.17685372,-2.26822285,424.98,18.25,91.48,181.55,181.48,-2.84,63.31,570.18,97.28,0.3510,0.3000,0.0000,1.0000,3 +80.779,51.17684585,-2.26766439,423.56,18.06,87.11,180.91,180.92,-3.30,57.73,556.57,97.41,0.3941,0.0941,0.0000,1.0000,3 +81.224,51.17686763,-2.26703914,423.42,24.85,81.28,180.68,180.79,-5.17,55.07,558.56,97.54,0.1961,0.3608,0.0000,1.0000,3 +81.668,51.17692722,-2.26643154,427.17,32.37,77.32,179.70,179.55,-5.88,47.87,557.51,97.68,0.1883,0.1608,0.0000,1.0000,3 +82.168,51.17701711,-2.26582778,434.37,39.51,74.72,179.21,178.79,-5.84,38.87,560.04,97.81,0.2000,0.2804,0.0000,1.0000,3 +82.613,51.17711853,-2.26526987,442.24,46.19,72.61,178.80,178.28,-6.48,32.06,559.75,97.94,0.2530,0.0000,0.0000,1.0000,3 +83.057,51.17723818,-2.26469141,452.85,36.69,70.44,177.84,177.11,-7.64,34.18,560.66,98.07,0.2137,-0.3672,-0.0508,1.0000,3 +83.501,51.17736626,-2.26414613,464.82,31.67,68.37,177.17,176.29,-8.26,48.41,562.22,98.20,0.3804,-0.3867,-0.0117,1.0000,3 +83.946,51.17750253,-2.26362930,477.28,35.89,64.46,177.04,176.40,-9.03,65.10,1138.75,98.11,0.3196,-0.4336,-0.0078,1.0000,3 +84.335,51.17764591,-2.26316026,488.01,39.39,60.17,177.90,177.44,-8.60,74.03,1166.11,97.90,0.5804,0.0000,0.0000,1.0000,3 +84.724,51.17781186,-2.26269085,497.59,42.24,51.84,178.38,178.51,-8.79,78.30,1162.87,97.68,0.6059,-0.2441,0.0000,1.0000,3 +85.279,51.17807032,-2.26217028,509.11,38.40,38.03,177.98,178.65,-8.72,83.40,1157.12,97.42,0.5471,0.0804,0.0000,1.0000,3 +85.668,51.17833225,-2.26181921,517.12,29.02,26.52,177.48,178.30,-7.72,85.80,1153.96,97.21,0.5647,0.0961,0.0000,1.0000,3 +86.113,51.17867330,-2.26152482,523.95,24.50,17.22,178.03,178.71,-5.84,86.22,1149.57,96.97,0.1255,0.4490,0.0000,1.0000,3 +86.557,51.17901886,-2.26133841,526.79,23.11,15.70,180.24,180.29,-3.21,69.85,1146.13,96.73,0.0667,0.4471,0.0000,1.0000,3 +87.001,51.17937873,-2.26117667,526.25,21.61,14.46,182.03,181.99,-1.10,45.11,564.32,96.76,0.0765,0.4706,0.0000,1.0000,3 +87.557,51.17984062,-2.26100052,522.92,20.03,12.54,181.81,181.69,-0.45,1.51,548.66,96.92,0.3902,0.4843,0.0000,1.0000,3 +88.057,51.18022796,-2.26086352,521.63,20.20,14.57,181.58,181.84,-5.92,-27.72,552.47,97.05,0.4098,0.4980,0.0000,1.0000,3 +88.557,51.18060928,-2.26069906,530.33,28.81,19.82,180.01,179.50,-8.99,-55.28,550.84,97.18,0.3118,0.5216,0.0000,1.0000,3 +89.001,51.18094955,-2.26049812,543.29,39.72,23.80,178.75,178.23,-8.14,-79.36,552.31,97.31,0.1157,-0.5762,-0.0117,1.0000,3 +89.390,51.18125379,-2.26029236,552.96,42.25,25.09,178.40,178.14,-6.13,-60.78,553.53,97.43,0.0000,-0.6719,-0.0098,1.0000,3 +89.835,51.18160800,-2.26001906,561.02,42.11,26.41,177.92,177.76,-4.15,-4.18,554.12,97.58,0.0824,-0.6465,-0.0117,1.0000,3 +90.335,51.18196750,-2.25972943,568.00,39.30,26.69,177.62,177.59,-4.48,43.88,555.25,97.72,0.2961,-0.4961,-0.0117,1.0000,3 +90.779,51.18230100,-2.25948099,572.65,34.98,23.24,177.86,178.12,-5.25,76.14,1106.77,97.69,0.4510,-0.2949,-0.0098,1.0000,3 +91.224,51.18263229,-2.25925924,574.65,30.13,14.86,178.86,179.65,-3.93,90.52,1155.40,97.46,0.6529,0.0000,0.0000,1.0000,3 +91.668,51.18296780,-2.25910351,572.25,25.16,0.74,178.28,179.86,-1.65,91.35,1152.19,97.23,0.5902,0.6608,0.0785,1.0000,3 +92.113,51.18333864,-2.25907179,565.27,19.09,348.42,178.40,179.49,-1.51,76.91,1148.40,97.00,0.4961,0.0000,-0.0059,1.0000,3 +92.557,51.18371362,-2.25917580,557.90,15.01,339.63,179.90,180.45,-1.79,74.42,1144.69,96.76,0.6176,0.2157,0.0000,1.0000,3 +93.001,51.18409283,-2.25940087,551.88,12.51,326.98,180.52,181.60,-3.63,73.58,1141.53,96.51,0.4059,0.0000,-0.0469,1.0000,3 +93.501,51.18444399,-2.25975841,548.85,16.02,318.40,181.96,182.48,-3.33,73.85,1138.19,96.26,0.3510,0.0000,-0.0547,1.0000,3 +93.946,51.18471049,-2.26011616,546.85,20.70,313.17,183.65,183.95,-2.70,73.18,1135.07,96.04,0.3667,0.0000,-0.0352,1.0000,3 +94.390,51.18496939,-2.26055168,543.79,25.21,308.22,185.59,185.68,-1.87,73.64,1131.89,95.81,0.3726,0.0000,-0.0410,1.0000,3 +94.890,51.18524913,-2.26113329,538.28,27.41,299.96,187.61,187.83,-1.93,75.40,1127.90,95.54,0.4745,0.0000,-0.0176,1.0000,3 +95.390,51.18545552,-2.26170485,532.57,28.99,290.38,188.63,188.99,-2.00,77.10,1124.69,95.31,0.4647,-0.0059,-0.0391,1.0000,3 +95.835,51.18560585,-2.26230245,527.25,32.36,282.42,189.94,190.17,-1.64,77.90,1121.28,95.08,0.5020,0.0000,-0.0312,1.0000,3 +96.279,51.18570027,-2.26295034,520.86,39.83,273.71,191.20,191.35,-1.23,78.66,1118.40,94.84,0.1628,0.4510,0.0000,1.0000,3 +96.835,51.18573584,-2.26365829,512.63,44.53,270.38,193.53,193.04,0.76,61.68,1114.67,94.59,0.2255,0.1961,0.0000,1.0000,3 +97.224,51.18573710,-2.26425770,504.05,44.03,268.56,195.74,194.93,1.74,46.61,1112.23,94.39,0.0373,0.4647,0.0000,1.0000,3 +97.724,51.18571959,-2.26500511,492.41,40.46,266.98,198.34,197.34,2.80,16.81,1108.57,94.14,0.0334,0.4883,0.0000,1.0000,3 +98.279,51.18569142,-2.26576415,480.35,37.88,266.14,200.95,199.78,2.98,-12.48,1105.58,93.89,0.1334,0.4667,0.0000,1.0000,3 +98.724,51.18566656,-2.26644062,469.14,36.38,266.79,203.03,201.80,2.35,-38.44,1102.37,93.68,0.3000,0.3902,0.0000,1.0000,3 +99.279,51.18564965,-2.26725394,456.14,33.21,271.50,205.17,204.08,0.97,-64.97,1099.63,93.43,0.5804,0.0020,-0.0449,1.0000,3 +99.724,51.18566681,-2.26796542,445.77,30.11,280.84,206.13,205.45,0.36,-67.12,1096.01,93.19,0.1784,-0.6582,-0.1113,1.0000,3 +100.168,51.18573028,-2.26860044,438.63,29.67,282.62,207.66,206.54,1.04,-23.94,1093.63,92.98,0.1490,-0.6953,-0.1113,1.0000,3 +100.668,51.18583801,-2.26935652,431.54,32.49,283.29,209.41,208.25,0.46,43.49,1090.27,92.74,0.3372,-0.3574,-0.0742,1.0000,3 +101.168,51.18593218,-2.27007340,424.62,37.68,277.78,210.50,209.63,-1.52,68.34,1087.05,92.50,0.4451,0.0000,-0.0332,1.0000,3 +101.613,51.18598582,-2.27074185,419.63,43.46,269.07,210.69,210.18,-2.60,71.89,1083.71,92.26,0.5392,0.0000,0.0000,1.0000,3 +102.057,51.18597936,-2.27145720,417.34,45.24,258.59,210.37,210.13,-4.01,73.90,1080.28,92.03,0.5608,-0.1250,0.0000,1.0000,3 +102.501,51.18589403,-2.27214876,418.58,39.00,247.35,209.36,209.13,-4.87,79.80,1077.17,91.78,0.5882,-0.1211,0.0000,1.0000,3 +102.946,51.18574044,-2.27275889,421.18,41.27,234.69,208.25,208.39,-4.62,84.51,1073.85,91.55,0.6451,0.0000,0.0000,1.0000,3 +103.390,51.18548707,-2.27334643,423.01,43.05,216.75,205.13,205.99,-4.27,86.60,1070.25,91.30,0.6314,0.0000,0.0000,1.0000,3 +103.890,51.18514722,-2.27378409,422.92,40.47,200.97,202.50,203.28,-3.45,84.97,1066.98,91.06,0.6628,0.0000,-0.0020,1.0000,3 +104.279,51.18479396,-2.27402654,421.46,34.89,186.13,200.28,201.22,-3.59,83.96,1063.99,90.84,0.5941,0.0745,0.0000,1.0000,3 +104.724,51.18438065,-2.27411393,419.48,31.38,173.37,198.75,199.18,-2.84,83.23,1060.88,90.60,0.2882,0.4647,0.0000,1.0000,3 +105.168,51.18401265,-2.27406811,416.62,28.48,169.29,199.93,199.38,-1.56,66.87,1057.89,90.38,0.4098,0.3843,0.0000,1.0000,3 +105.613,51.18357818,-2.27393427,413.75,25.74,163.61,200.90,200.41,-3.63,49.94,1054.66,90.14,0.2196,0.1412,0.0000,1.0000,3 +106.113,51.18314882,-2.27372977,416.70,28.87,161.04,201.84,200.99,-3.64,43.43,1051.15,89.90,0.0000,0.3177,0.0000,1.0000,3 +106.613,51.18268063,-2.27346101,421.59,33.73,159.48,203.06,202.13,-2.73,30.43,1047.34,89.62,-0.0703,0.4157,0.0000,1.0000,3 +107.113,51.18224190,-2.27318793,425.53,35.53,158.16,204.15,203.20,-2.36,3.72,1043.63,89.38,-0.0039,0.4706,0.0000,1.0000,3 +107.613,51.18184380,-2.27293469,429.16,37.30,157.72,205.07,204.11,-2.45,-30.78,1039.75,89.14,0.0961,0.3980,0.0000,1.0000,3 +108.001,51.18145834,-2.27269347,431.55,39.63,159.02,205.92,205.07,-2.60,-47.83,1036.38,88.91,0.2922,0.2098,0.0687,1.0000,3 +108.557,51.18100858,-2.27243926,433.43,41.53,163.04,206.73,205.91,-3.37,-52.28,1032.21,88.64,0.1706,-0.0332,0.0000,1.0000,4 +108.946,51.18063202,-2.27226467,436.16,44.27,164.89,207.41,206.48,-2.83,-50.41,1028.94,88.43,0.1118,-0.2168,0.0000,1.0000,4 +109.390,51.18020067,-2.27208835,438.24,46.26,166.18,208.27,207.35,-1.81,-42.53,1025.16,88.18,0.0275,-0.3555,-0.1523,1.0000,4 +109.890,51.17976857,-2.27192671,438.43,46.42,167.51,209.27,208.28,-1.05,-25.40,1021.60,87.94,0.0392,-0.4531,-0.1582,1.0000,4 +110.446,51.17924607,-2.27175096,437.90,46.15,168.56,210.42,209.37,-0.92,7.59,1017.54,87.65,0.1981,-0.3906,-0.1543,1.0000,4 +110.890,51.17878771,-2.27160037,438.25,41.42,167.95,211.14,210.14,-2.32,40.72,1013.94,87.42,0.3275,-0.4902,-0.1621,1.0000,4 +111.335,51.17837228,-2.27145012,439.57,37.62,163.98,211.47,210.68,-3.80,69.80,1010.62,87.18,0.4902,-0.3301,-0.0625,1.0000,4 +111.779,51.17797327,-2.27125342,441.10,39.16,153.67,210.88,210.60,-4.51,81.84,1007.25,86.95,0.6569,-0.0371,-0.0332,1.0000,4 +112.224,51.17757150,-2.27094162,442.76,40.82,134.54,207.26,208.44,-4.87,85.59,1003.74,86.70,0.6196,0.2432,0.0000,1.0000,4 +112.724,51.17723559,-2.27041101,444.33,42.38,117.36,203.06,203.68,-4.61,80.35,1000.31,86.45,0.6196,-0.0254,-0.1426,1.0000,4 +113.224,51.17700330,-2.26973359,446.85,44.96,106.21,202.06,201.98,-4.62,78.24,996.74,86.18,0.4529,0.0000,-0.0449,1.0000,4 +113.724,51.17687699,-2.26906635,449.24,47.34,97.58,202.16,202.00,-4.30,79.23,993.27,85.93,0.3431,0.2589,0.0000,1.0000,4 +114.168,51.17681206,-2.26838332,450.92,41.30,89.71,201.84,201.63,-3.97,74.38,989.90,85.69,0.2765,0.2882,0.0000,1.0000,4 +114.612,51.17680876,-2.26771313,452.50,41.89,85.93,202.50,201.93,-3.18,64.74,986.20,85.45,0.2686,0.3667,0.0000,1.0000,4 +115.057,51.17684177,-2.26702456,453.80,55.85,83.01,203.26,202.57,-2.66,51.62,982.73,85.20,0.2059,0.2745,0.0000,1.0000,4 +115.557,51.17689967,-2.26634421,455.10,60.14,79.38,203.88,203.27,-4.43,44.01,979.17,84.96,0.3353,-0.4160,-0.1055,1.0000,4 +116.001,51.17698372,-2.26566333,461.92,67.34,73.39,203.32,202.47,-8.34,51.19,975.54,84.72,0.2333,-0.4355,-0.1152,1.0000,4 +116.446,51.17710445,-2.26503862,475.73,78.06,69.71,203.05,201.64,-8.37,71.44,971.87,84.47,0.5059,-0.2617,-0.0215,1.0000,4 +116.890,51.17725508,-2.26439665,489.12,62.86,63.67,202.68,201.75,-7.62,82.92,968.30,84.23,0.1236,0.0098,-0.0605,1.0000,4 +117.335,51.17743320,-2.26379808,499.09,61.38,61.80,202.87,202.10,-5.41,80.09,964.30,83.99,0.5627,-0.1641,-0.0645,1.0000,4 +117.835,51.17764217,-2.26319845,505.71,57.64,52.51,202.41,202.29,-5.55,80.24,960.14,83.74,0.2843,0.0000,-0.1582,1.0000,4 +118.279,51.17788581,-2.26266644,510.94,52.09,42.40,201.72,201.74,-5.12,82.11,956.94,83.51,0.5902,0.0000,-0.0840,1.0000,4 +118.779,51.17823054,-2.26216062,515.14,39.44,29.20,199.77,200.16,-4.98,82.85,952.88,83.27,0.5314,0.1941,-0.0508,1.0000,4 +119.224,51.17860444,-2.26182347,518.31,26.39,21.07,199.05,199.02,-4.05,70.46,949.60,83.03,0.0000,0.5804,-0.0508,1.0000,4 +119.724,51.17903167,-2.26154961,520.76,22.63,19.73,200.09,199.60,-2.16,31.70,945.88,82.77,0.0785,0.5529,-0.0117,1.0000,4 +120.224,51.17946684,-2.26131191,522.90,24.09,18.48,200.91,200.35,-1.85,-5.55,942.40,82.53,0.2432,0.4902,0.0000,1.0000,4 +120.724,51.17989700,-2.26107191,525.54,25.83,19.39,201.51,201.02,-3.37,-43.02,938.62,82.28,0.2392,0.4059,0.0000,1.0000,4 +121.168,51.18029805,-2.26084015,528.71,27.18,22.12,201.91,201.47,-3.45,-53.60,935.29,82.05,0.1471,-0.4785,-0.1680,1.0000,4 +121.668,51.18070642,-2.26055950,531.60,25.53,24.12,202.53,202.01,-3.06,-22.39,931.38,81.79,0.3020,-0.4102,-0.1113,1.0000,4 +122.113,51.18111231,-2.26026235,537.57,25.41,25.37,202.73,201.95,-5.10,0.85,927.98,81.56,0.1000,-0.4238,-0.1699,1.0000,4 +122.557,51.18149874,-2.25997512,548.04,29.52,25.11,202.80,201.80,-5.21,24.00,924.51,81.34,0.1334,-0.4648,-0.1660,1.0000,4 +123.113,51.18194065,-2.25965614,559.81,29.65,23.88,202.83,202.00,-5.64,55.96,920.98,81.09,0.3804,-0.3320,-0.0645,1.0000,4 +123.668,51.18241556,-2.25934556,570.46,28.45,16.49,202.31,202.03,-6.37,81.86,916.37,80.83,0.5980,0.0000,-0.0195,1.0000,4 +124.224,51.18291493,-2.25912602,578.15,31.55,3.18,200.81,201.28,-5.12,87.22,912.64,80.56,0.5274,0.0000,0.0000,1.0000,4 +124.668,51.18332627,-2.25907725,580.84,35.26,351.57,199.45,200.08,-3.43,88.61,908.91,80.34,0.5274,0.0863,0.0000,1.0000,4 +125.224,51.18387041,-2.25920596,577.90,36.15,336.82,198.15,198.72,-1.04,88.53,905.18,80.06,0.5706,0.2216,0.0000,1.0000,4 +125.779,51.18433030,-2.25950910,568.55,30.80,322.44,197.05,197.52,0.03,82.81,901.67,79.81,0.5569,0.2059,0.0000,1.0000,4 +126.279,51.18471278,-2.25995798,556.91,27.79,310.90,196.61,196.65,0.20,77.22,898.32,79.56,0.4177,0.2549,0.0000,1.0000,4 +126.891,51.18507779,-2.26063809,543.11,25.72,299.51,196.93,196.83,-0.47,72.89,895.09,79.27,0.4765,0.0000,0.0000,1.0000,4 +127.446,51.18531872,-2.26129756,533.56,24.93,291.66,197.28,197.06,-0.47,73.35,891.73,79.02,0.4275,0.0000,-0.0059,1.0000,4 +127.946,51.18549605,-2.26199667,524.31,24.80,284.48,197.87,197.58,-0.17,74.01,888.74,78.76,0.3020,0.0000,-0.0293,1.0000,4 +128.502,51.18561953,-2.26275018,513.63,28.72,278.37,198.91,198.29,0.56,74.28,885.63,78.52,0.2784,0.1255,0.0000,1.0000,4 +129.113,51.18569474,-2.26363100,499.48,31.03,270.70,199.73,199.05,0.85,65.59,881.73,78.22,0.0785,0.4588,0.0000,1.0000,4 +129.668,51.18570037,-2.26447573,486.12,28.70,268.05,201.22,200.18,2.24,32.77,878.62,77.96,0.0255,0.4745,0.0000,1.0000,4 +130.224,51.18567638,-2.26532337,473.63,25.77,266.63,202.59,201.48,2.55,-4.55,874.56,77.68,0.1314,0.4353,0.0000,1.0000,4 +130.780,51.18564858,-2.26615104,461.72,25.11,267.06,203.81,202.66,1.77,-36.44,871.45,77.41,0.3549,0.3510,0.0000,1.0000,4 +131.335,51.18563371,-2.26697417,451.08,25.41,272.29,204.44,203.65,-0.33,-58.43,867.56,77.15,0.2039,0.0000,0.0000,1.0000,4 +131.835,51.18565307,-2.26768845,444.36,25.91,275.30,205.07,204.16,0.34,-59.49,864.74,76.91,0.2589,-0.2734,0.0000,1.0000,4 +132.335,51.18569908,-2.26840842,436.47,25.50,278.18,205.78,204.74,0.67,-42.17,861.20,76.67,0.2314,-0.4570,-0.0059,1.0000,4 +132.780,51.18576085,-2.26906430,430.30,27.36,279.96,206.36,205.34,0.32,-10.22,858.55,76.45,0.1490,-0.5449,-0.0273,1.0000,4 +133.280,51.18584193,-2.26980850,426.03,34.39,280.28,206.84,205.87,-0.25,33.26,854.95,76.20,0.3235,-0.4629,-0.0117,1.0000,4 +133.724,51.18591164,-2.27047863,422.50,42.07,276.30,206.89,206.18,-2.66,60.50,851.85,75.97,0.5274,0.0000,0.0000,1.0000,4 +134.169,51.18595273,-2.27120368,421.62,49.33,267.13,205.90,205.61,-4.75,71.20,848.24,75.72,0.4078,-0.2930,-0.0195,1.0000,4 +134.669,51.18593555,-2.27188749,423.92,47.99,258.09,204.95,204.69,-4.71,81.70,844.93,75.48,0.6333,0.0000,0.0000,1.0000,4 +135.113,51.18584953,-2.27256543,425.85,45.88,242.16,202.06,202.82,-4.56,85.77,841.43,75.23,0.7157,0.0000,0.0647,1.0000,4 +135.558,51.18565355,-2.27317698,426.38,46.36,221.13,196.51,198.29,-4.06,87.93,837.92,74.98,0.7098,0.1334,0.1216,1.0000,4 +136.058,51.18535418,-2.27363740,424.26,44.07,201.37,191.37,193.17,-2.72,88.34,834.50,74.72,0.4569,0.4941,0.0922,1.0000,4 +136.558,51.18495232,-2.27391521,418.69,34.69,189.34,189.99,190.23,-1.76,79.30,831.25,74.47,0.4902,0.1079,0.0000,1.0000,4 +137.002,51.18453728,-2.27403182,412.79,24.60,179.41,189.34,189.47,-2.21,69.26,827.53,74.22,0.4608,0.0020,-0.0664,1.0000,4 +137.446,51.18413189,-2.27403268,409.28,21.27,170.59,188.94,189.16,-4.73,63.30,823.80,73.97,0.3451,0.2490,-0.0059,1.0000,4 +137.946,51.18371894,-2.27392391,411.54,23.78,165.35,188.78,188.50,-5.05,57.08,820.58,73.71,0.0157,0.2510,0.0000,1.0000,4 +138.391,51.18333073,-2.27376224,415.94,28.17,163.75,189.20,188.67,-3.77,52.09,816.89,73.46,0.0000,0.1628,-0.0430,1.0000,4 +138.891,51.18293880,-2.27356806,418.97,31.03,162.18,189.70,189.21,-2.60,46.21,812.89,73.21,-0.0938,0.3392,0.0000,1.0000,4 +139.447,51.18247972,-2.27331629,419.83,31.10,160.36,190.44,189.90,-1.53,30.69,808.75,72.92,-0.0820,0.2863,0.0000,1.0000,4 +139.891,51.18208923,-2.27308032,419.50,28.28,159.18,191.10,190.53,-1.07,19.31,804.83,72.66,0.0000,0.3275,0.0000,1.0000,4 +140.391,51.18167461,-2.27282549,418.81,26.80,158.35,191.76,191.15,-0.86,7.75,801.00,72.40,0.1686,0.0745,0.0000,1.0000,4 +140.891,51.18130318,-2.27258781,418.26,26.26,158.01,192.30,191.69,-1.21,5.46,797.47,72.16,0.0981,0.1137,0.0000,1.0000,4 diff --git a/track_data/flight_20260901_221633_021100.csv b/track_data/flight_20260901_221633_021100.csv new file mode 100644 index 00000000..03d590a8 --- /dev/null +++ b/track_data/flight_20260901_221633_021100.csv @@ -0,0 +1,315 @@ +# aircraft_title=Extra 300S Paint2 +# recorded_at=2026-09-01T22:16:33 +# laps_s=29.112,34.945,33.643,33.299 +timestamp_s,latitude,longitude,altitude_ft,agl_ft,heading_deg,airspeed_kts,groundspeed_kts,pitch_deg,bank_deg,torque,health_pct,stick_pitch,stick_roll,stick_yaw,stick_throttle,lap +0.000,51.18979680,-2.27597185,984.54,621.04,166.18,183.13,181.95,6.70,0.12,1104.68,100.00,-0.1836,0.0000,-0.0215,1.0000,1 +0.444,51.18945458,-2.27583813,966.71,599.15,166.05,186.20,184.75,8.06,0.15,1168.21,100.00,-0.2246,0.0000,-0.0273,1.0000,1 +0.833,51.18910220,-2.27569995,945.49,575.45,165.92,189.37,187.10,10.13,0.17,1173.01,100.00,-0.2129,0.0236,-0.0293,1.0000,1 +1.278,51.18875122,-2.27556169,920.08,546.31,165.90,192.75,189.29,11.63,0.18,1174.44,100.00,-0.1191,0.0412,-0.0293,1.0000,1 +1.667,51.18840782,-2.27542346,890.85,514.55,165.95,196.33,191.94,11.99,0.18,1174.71,100.00,-0.1211,0.0490,-0.0566,1.0000,1 +2.111,51.18804672,-2.27528389,855.22,474.68,165.96,199.83,194.89,12.35,0.18,1177.56,100.00,-0.0879,0.0961,-0.0332,1.0000,1 +2.667,51.18753673,-2.27508494,815.28,433.82,165.95,204.63,199.23,12.67,0.19,1177.09,100.00,-0.0449,0.1432,0.0000,1.0000,1 +3.111,51.18715498,-2.27492998,780.23,398.79,165.95,208.28,202.36,12.86,0.05,1180.00,100.00,-0.0059,0.1628,0.0000,1.0000,1 +3.556,51.18674626,-2.27476811,742.79,361.37,165.93,211.81,205.59,13.01,-0.51,1179.49,100.00,0.0000,0.0000,0.0000,1.0000,1 +3.945,51.18633694,-2.27460553,704.93,324.20,165.95,215.48,208.75,13.10,-0.56,1182.05,100.00,0.0079,0.0000,0.0000,1.0000,1 +4.389,51.18595970,-2.27445284,669.03,288.30,165.96,218.55,211.61,13.13,-0.57,1182.30,100.00,0.0334,0.0000,0.0000,1.0000,1 +4.778,51.18556969,-2.27430127,634.28,252.79,165.98,221.70,214.39,13.12,-0.57,1181.63,100.00,0.1334,0.0000,0.0000,1.0000,1 +5.167,51.18517189,-2.27414356,596.32,214.08,166.02,224.68,217.22,12.74,-0.57,1185.27,100.00,0.1981,0.0000,-0.0566,1.0000,1 +5.667,51.18470314,-2.27395799,554.22,164.84,166.16,227.75,221.10,10.77,-0.57,1184.59,100.00,0.1686,0.0000,-0.0684,1.0000,1 +6.056,51.18431011,-2.27379942,523.02,133.54,166.25,230.32,224.83,8.92,-0.58,1188.09,100.00,0.1314,0.0000,-0.0469,1.0000,1 +6.445,51.18390884,-2.27364516,497.36,108.30,166.25,232.25,227.71,7.87,-0.57,1188.01,100.00,0.2275,0.0216,-0.0117,1.0000,1 +6.889,51.18348379,-2.27347893,473.58,84.73,166.36,234.18,230.35,5.67,-0.58,1188.51,100.00,0.2039,0.0000,-0.0293,1.0000,1 +7.278,51.18305837,-2.27331654,455.52,66.89,166.38,235.62,232.74,4.23,-0.59,1190.30,100.00,0.1275,0.0000,-0.0059,1.0000,1 +7.667,51.18264135,-2.27315072,441.64,53.00,166.36,236.90,234.17,3.49,-0.60,1189.86,100.00,0.3000,0.0000,-0.0078,1.0000,1 +8.111,51.18215977,-2.27296834,429.52,38.38,166.50,238.07,235.79,1.21,-0.53,1191.17,100.00,0.1236,-0.1523,-0.0645,1.0000,1 +8.556,51.18172442,-2.27279718,423.32,31.21,166.45,238.93,236.76,1.02,1.44,1190.89,100.00,0.1941,-0.1016,-0.0684,1.0000,1 +8.945,51.18128309,-2.27263039,418.65,26.50,166.41,239.73,237.55,0.09,4.56,1191.14,100.00,0.1490,-0.2500,-0.0781,1.0000,1 +9.389,51.18085648,-2.27245689,416.49,24.44,166.25,240.28,238.15,-0.69,10.05,1191.18,100.00,0.2020,-0.0410,-0.0762,1.0000,1 +9.778,51.18039219,-2.27227509,416.39,24.43,165.87,240.79,238.64,-1.34,17.01,1190.90,100.00,0.1471,-0.2871,-0.1602,1.0000,1 +10.167,51.17997324,-2.27210035,417.63,25.71,165.34,241.18,239.01,-1.68,28.48,1190.95,100.00,0.1608,-0.2617,-0.1602,1.0000,1 +10.611,51.17949104,-2.27189001,419.56,27.69,164.08,241.54,239.39,-2.26,39.84,1190.73,100.00,0.1784,-0.3477,-0.1699,1.0000,1 +11.056,51.17905337,-2.27168154,421.92,29.14,162.30,241.77,239.66,-2.70,54.33,1190.28,100.00,0.2216,-0.3496,-0.1680,1.0000,1 +11.445,51.17862658,-2.27145485,424.06,24.20,159.41,241.93,239.91,-2.99,71.63,1190.06,100.00,0.4020,-0.3320,-0.1465,1.0000,1 +11.834,51.17822704,-2.27120965,425.30,23.33,152.00,241.65,239.99,-3.13,82.73,1189.95,100.00,0.3333,0.0745,-0.0566,1.0000,1 +12.278,51.17782644,-2.27088184,425.51,23.49,145.39,241.07,239.32,-2.07,84.97,1189.74,100.00,0.6510,0.1588,-0.0469,1.0000,1 +12.667,51.17744790,-2.27047625,423.63,21.58,131.72,239.70,238.83,-2.44,81.78,1189.72,100.00,0.7706,0.2686,-0.0879,1.0000,1 +13.056,51.17713848,-2.26993728,423.51,21.87,110.94,233.05,233.79,-6.83,75.48,1189.99,100.00,0.5373,0.0000,-0.1875,1.0000,1 +13.500,51.17695730,-2.26928773,431.71,30.18,100.96,230.56,229.19,-7.57,75.94,1190.43,100.00,0.2549,-0.1289,-0.1914,1.0000,1 +13.945,51.17685763,-2.26856336,444.60,42.51,95.36,230.51,228.55,-6.90,77.43,1190.71,100.00,0.4196,-0.1035,-0.1680,1.0000,1 +14.334,51.17681463,-2.26784274,456.77,47.59,87.03,229.56,227.80,-7.05,78.93,1189.94,100.00,0.1726,-0.0332,-0.1895,1.0000,1 +14.723,51.17682621,-2.26720622,467.04,64.09,84.69,229.79,227.83,-5.67,82.02,1190.03,100.00,0.2824,-0.2207,-0.1758,1.0000,1 +15.167,51.17686630,-2.26648961,475.54,80.80,80.42,229.89,228.36,-4.49,86.21,1188.79,100.00,0.3118,0.2255,-0.0078,1.0000,1 +15.556,51.17693527,-2.26580611,480.54,85.67,74.86,230.09,228.78,-3.41,80.42,1188.61,100.00,0.3294,0.1726,-0.0898,1.0000,1 +16.000,51.17704171,-2.26515434,482.88,87.97,69.03,230.14,228.93,-3.38,73.06,1188.50,100.00,0.3039,0.3255,0.0000,1.0000,1 +16.445,51.17720753,-2.26446369,485.82,60.92,64.13,230.36,229.00,-3.77,65.27,1188.17,100.00,0.3177,-0.2168,-0.1895,1.0000,1 +16.834,51.17739658,-2.26384330,490.09,51.05,59.33,230.55,229.17,-4.38,69.20,1188.19,100.00,0.3372,-0.0234,-0.1816,1.0000,1 +17.278,51.17761783,-2.26323530,495.08,47.26,53.55,230.55,229.23,-4.84,72.98,1187.75,100.00,0.4863,-0.1367,-0.1797,1.0000,1 +17.667,51.17790358,-2.26259712,502.46,43.25,42.01,229.36,228.49,-6.66,76.63,1187.76,100.00,0.6216,-0.0781,-0.1895,1.0000,1 +18.167,51.17825342,-2.26210315,512.76,33.68,27.06,226.40,225.90,-7.84,82.80,1187.24,100.00,0.5020,-0.2695,-0.2070,1.0000,1 +18.556,51.17864482,-2.26176634,523.74,31.47,17.82,225.03,224.03,-6.31,89.71,1187.30,100.00,0.0549,0.1236,-0.2109,1.0000,1 +18.945,51.17902754,-2.26154996,531.44,34.40,16.85,225.65,224.40,-4.05,82.52,1187.49,100.00,-0.2129,0.5059,-0.0723,1.0000,1 +19.389,51.17941127,-2.26136372,535.35,37.88,16.84,226.52,225.25,-1.42,50.38,1186.45,100.00,-0.2090,0.6176,-0.0059,1.0000,1 +19.778,51.17984066,-2.26116293,534.92,37.21,15.70,227.62,226.27,0.65,-3.00,1186.42,100.00,0.2745,0.6078,0.0000,1.0000,1 +20.278,51.18033405,-2.26093296,531.27,32.99,17.63,228.61,227.44,-1.99,-55.81,1186.24,100.00,0.5235,0.0000,-0.1875,1.0000,1 +20.723,51.18074698,-2.26071161,530.36,28.59,24.95,228.78,227.89,-3.41,-59.00,1186.19,100.00,0.2667,-0.6250,-0.3008,1.0000,1 +21.112,51.18118264,-2.26039468,535.19,29.36,27.60,229.20,227.70,-3.75,-11.51,1186.18,100.00,0.0000,-0.7090,-0.2988,1.0000,1 +21.556,51.18158856,-2.26005466,543.10,27.10,28.12,229.64,228.17,-4.30,55.67,1186.17,100.00,0.2589,0.0000,-0.2051,1.0000,1 +22.001,51.18195727,-2.25976544,548.55,18.78,24.71,229.83,228.70,-4.57,75.06,1186.15,100.00,0.3647,0.0412,-0.1953,1.0000,1 +22.390,51.18233436,-2.25949370,552.54,13.78,19.14,230.10,229.07,-4.20,76.46,1185.34,100.00,0.5961,0.0000,-0.1660,1.0000,1 +22.778,51.18271877,-2.25928028,556.29,12.67,7.08,228.83,228.57,-6.08,77.69,1185.30,100.00,0.5510,0.0000,-0.1914,1.0000,1 +23.167,51.18315487,-2.25918625,564.06,20.24,352.19,225.39,225.21,-7.57,79.64,1185.10,100.00,0.5255,0.0000,-0.2051,1.0000,1 +23.556,51.18356263,-2.25925336,574.12,32.40,342.64,224.07,223.32,-7.52,82.08,1185.17,100.00,0.5294,-0.2402,-0.1934,1.0000,1 +23.945,51.18396323,-2.25944713,585.18,46.91,331.41,222.51,222.00,-6.71,88.92,1185.40,100.00,0.5216,-0.2383,-0.1855,1.0000,1 +24.445,51.18438159,-2.25979209,594.23,62.11,319.42,221.26,221.11,-4.17,96.84,1184.34,100.00,0.5098,-0.0879,-0.1758,1.0000,1 +24.834,51.18471995,-2.26023080,595.43,71.49,309.73,220.67,220.53,-0.65,98.95,1183.83,99.94,0.3431,0.1784,-0.0840,1.0000,1 +25.279,51.18500946,-2.26073090,588.53,72.62,304.44,221.66,220.69,2.04,97.13,1180.10,99.70,0.4216,0.3608,0.0000,1.0000,1 +25.667,51.18526213,-2.26129880,574.95,66.12,297.48,222.68,220.97,4.06,87.48,1176.66,99.46,0.6098,0.1608,-0.0352,1.0000,1 +26.056,51.18545473,-2.26188405,556.59,55.34,285.91,223.05,221.24,3.92,82.06,1173.58,99.23,0.3824,0.3941,0.0000,1.0000,1 +26.556,51.18560408,-2.26260115,535.67,47.55,277.40,223.70,221.53,3.75,71.57,1171.44,98.97,0.3726,0.2784,-0.0078,1.0000,1 +26.945,51.18566590,-2.26326052,519.07,44.03,272.12,224.78,222.78,3.13,63.24,1168.30,98.75,0.2235,0.2726,-0.0449,1.0000,1 +27.390,51.18568535,-2.26394796,503.35,39.52,270.03,226.61,224.31,3.78,48.67,1167.00,98.52,0.1765,0.3510,0.0000,1.0000,1 +27.779,51.18568317,-2.26462997,488.21,32.25,268.30,228.18,225.85,3.81,31.58,1163.88,98.29,0.1824,0.4020,0.0000,1.0000,1 +28.279,51.18566497,-2.26543279,472.83,26.16,266.77,229.10,226.99,2.06,13.07,568.91,98.34,0.1490,0.4627,0.0000,1.0000,1 +28.723,51.18563717,-2.26620325,463.48,27.81,266.29,228.42,226.80,0.80,-14.78,561.63,98.48,0.2412,0.3588,0.0000,1.0000,1 +29.167,51.18561087,-2.26695862,458.35,33.02,267.92,227.01,225.59,-1.08,-43.58,566.09,98.63,0.2451,0.4294,0.0000,1.0000,1 +29.667,51.18559945,-2.26771944,456.11,38.59,271.63,225.23,223.98,-1.90,-60.95,566.35,98.77,0.1883,-0.3164,-0.0723,1.0000,1 +30.056,51.18561189,-2.26836776,455.05,44.38,274.18,223.74,222.49,-1.31,-51.56,567.68,98.91,0.0000,-0.5625,-0.1797,1.0000,1 +30.445,51.18564571,-2.26904793,453.85,50.95,275.55,222.51,221.17,-0.50,-16.85,569.15,99.05,0.0000,-0.5605,-0.1758,1.0000,1 +30.890,51.18568706,-2.26971688,452.76,59.32,276.33,221.18,219.90,-0.74,25.15,569.95,99.18,0.0000,-0.5605,-0.1738,1.0000,1 +31.278,51.18572683,-2.27037977,451.23,68.97,275.63,219.89,218.76,-1.18,64.07,571.37,99.32,0.2098,-0.4004,-0.2031,1.0000,1 +31.723,51.18576215,-2.27107951,446.70,70.86,271.89,218.82,217.70,-0.53,85.66,1015.30,99.40,0.3667,0.0000,-0.1426,1.0000,1 +32.167,51.18577665,-2.27177427,438.27,60.38,264.14,218.72,217.50,1.15,89.17,1181.27,99.15,0.5117,0.2961,-0.1250,1.0000,1 +32.556,51.18574575,-2.27241322,427.42,46.56,251.60,218.07,217.08,1.89,85.76,1179.38,98.91,0.7608,0.2824,-0.0859,1.0000,1 +33.001,51.18562850,-2.27305319,413.50,33.27,227.17,211.40,213.30,-1.52,78.81,1174.22,98.64,0.7000,0.0000,-0.2031,1.0000,1 +33.390,51.18538293,-2.27353580,406.54,26.48,207.43,204.65,206.90,-5.05,78.40,1171.61,98.40,0.6569,0.0588,-0.1816,1.0000,1 +33.778,51.18506911,-2.27383063,406.40,23.92,191.96,201.16,202.48,-6.14,79.17,1168.64,98.17,0.5000,0.1745,-0.1895,1.0000,1 +34.167,51.18471310,-2.27397951,409.77,22.22,184.29,201.58,201.33,-5.43,78.08,1165.57,97.93,0.5196,0.0000,-0.2598,1.0000,1 +34.612,51.18431275,-2.27403414,414.00,26.09,175.27,201.71,201.46,-5.40,79.07,1162.06,97.67,0.4333,0.0000,-0.2637,1.0000,1 +35.001,51.18392888,-2.27399355,418.03,30.18,167.89,201.66,201.25,-4.69,80.21,574.99,97.72,0.4137,0.0353,-0.2500,1.0000,1 +35.445,51.18352056,-2.27386614,420.69,32.76,161.21,200.56,200.25,-3.88,80.21,563.15,97.87,-0.0879,0.5431,-0.1133,1.0000,1 +35.945,51.18309118,-2.27364208,421.38,33.32,159.92,199.69,198.98,-1.15,52.07,560.57,98.03,-0.0703,0.5412,0.0000,1.0000,1 +36.390,51.18270277,-2.27340205,418.82,30.61,158.55,199.21,198.40,0.20,17.82,562.42,98.18,0.0000,0.5373,0.0000,1.0000,1 +36.834,51.18232960,-2.27316052,415.31,25.51,157.73,199.00,198.18,0.26,-17.28,564.15,98.33,0.0667,0.3098,0.0000,1.0000,1 +37.278,51.18195380,-2.27292862,411.68,19.67,158.31,198.58,197.75,0.21,-29.33,564.65,98.48,0.1981,0.0667,-0.0625,1.0000,1 +37.667,51.18162003,-2.27272720,407.62,15.46,160.43,198.30,197.56,-1.50,-31.12,566.64,98.61,0.3431,0.0000,-0.0488,1.0000,1 +38.056,51.18126530,-2.27253363,407.76,15.93,163.77,197.20,196.57,-4.25,-32.24,567.11,98.75,0.2020,0.0000,0.0000,1.0000,1 +38.501,51.18093051,-2.27237709,413.04,21.31,164.71,196.50,195.62,-4.17,-32.09,568.74,98.88,0.0549,0.0000,0.0000,1.0000,2 +38.890,51.18057900,-2.27223109,418.78,26.89,165.55,195.77,194.91,-3.71,-29.85,569.47,99.02,-0.0508,-0.2012,-0.0273,1.0000,2 +39.278,51.18023552,-2.27209744,423.63,31.73,166.35,195.10,194.33,-3.22,-23.95,570.80,99.15,0.0000,-0.5254,-0.0879,1.0000,2 +39.723,51.17981711,-2.27194843,428.71,36.89,167.43,194.43,193.68,-2.73,5.85,571.68,99.32,0.0000,-0.3613,-0.1426,1.0000,2 +40.223,51.17940369,-2.27179910,433.08,41.32,167.53,193.75,193.10,-2.73,32.16,572.93,99.47,0.0883,-0.3184,-0.1836,1.0000,2 +40.667,51.17902707,-2.27165717,435.63,42.67,166.73,193.24,192.71,-2.56,51.83,573.94,99.61,0.1196,-0.4961,-0.1895,1.0000,2 +41.056,51.17866637,-2.27151052,435.74,36.41,164.65,193.49,193.07,-2.23,70.93,1143.54,99.56,0.4647,0.0000,-0.1797,1.0000,2 +41.501,51.17832001,-2.27135176,433.26,31.12,158.89,194.38,194.13,-2.26,75.59,1187.07,99.32,0.4373,-0.1816,-0.1777,1.0000,2 +41.890,51.17796888,-2.27113906,429.56,27.40,151.62,195.39,195.21,-2.13,77.11,1183.03,99.05,0.5549,0.0000,-0.1426,1.0000,2 +42.278,51.17766061,-2.27088349,425.87,23.72,140.50,195.41,195.81,-3.29,78.09,1178.57,98.81,0.6274,0.1039,-0.1055,1.0000,2 +42.667,51.17736270,-2.27050479,423.52,21.49,123.28,193.10,194.62,-5.33,79.51,1174.69,98.55,0.4922,0.0902,-0.1523,1.0000,2 +43.167,51.17711626,-2.26996647,424.43,22.51,110.74,192.21,192.86,-5.13,80.99,1170.75,98.28,0.5274,0.2373,-0.1543,1.0000,2 +43.612,51.17695583,-2.26938541,425.95,24.02,102.22,192.89,192.96,-4.29,77.27,1167.00,98.01,0.5078,0.0216,-0.1621,1.0000,2 +44.056,51.17686741,-2.26877916,427.12,24.99,94.63,193.41,193.37,-4.26,74.53,587.98,97.99,0.4039,0.2118,-0.1523,1.0000,2 +44.501,51.17682946,-2.26817342,428.43,21.43,88.93,192.88,192.72,-4.05,72.28,565.99,98.13,0.3706,0.1020,-0.1309,1.0000,2 +44.890,51.17683290,-2.26760290,429.54,24.95,83.66,191.65,191.48,-3.90,70.70,562.15,98.27,0.1863,0.4078,-0.0117,1.0000,2 +45.334,51.17687255,-2.26699450,430.35,33.72,80.37,191.15,190.86,-3.25,56.88,565.59,98.42,0.3118,0.3745,-0.0488,1.0000,2 +45.778,51.17694048,-2.26636730,431.75,36.87,76.85,190.56,190.21,-4.21,38.70,564.95,98.57,0.3647,0.0000,-0.1758,1.0000,2 +46.223,51.17703553,-2.26575152,437.10,42.43,73.85,189.43,188.90,-6.71,34.19,566.45,98.72,0.3882,-0.0684,-0.1777,1.0000,2 +46.667,51.17715652,-2.26510634,449.04,53.15,71.07,188.15,187.02,-8.22,41.13,568.45,98.88,0.1961,0.0000,-0.1504,1.0000,2 +47.112,51.17728676,-2.26454059,462.71,48.98,69.54,187.12,185.91,-7.69,44.17,568.63,99.03,0.2804,0.0000,-0.1758,1.0000,2 +47.556,51.17742039,-2.26400145,475.53,41.68,67.07,186.04,185.04,-8.16,48.07,857.53,99.14,0.1628,-0.4707,-0.1895,1.0000,2 +48.001,51.17756451,-2.26347793,487.76,45.11,65.22,186.53,185.62,-7.72,66.95,1173.83,98.93,0.5333,-0.2988,-0.1582,1.0000,2 +48.390,51.17772094,-2.26296238,498.05,48.12,56.70,186.72,186.59,-8.56,79.25,1176.81,98.67,0.6471,-0.0020,-0.0742,1.0000,2 +48.834,51.17792828,-2.26244602,508.61,46.91,43.13,185.70,186.28,-8.63,83.33,1170.89,98.39,0.5725,-0.0703,-0.1504,1.0000,2 +49.334,51.17821292,-2.26198711,518.13,38.18,32.75,185.46,185.82,-7.18,85.93,1166.50,98.12,0.4314,0.0667,-0.1094,1.0000,2 +49.778,51.17853295,-2.26164345,525.06,28.83,26.00,186.49,186.63,-5.17,85.59,1161.20,97.84,0.5588,0.0000,-0.1836,1.0000,2 +50.223,51.17886721,-2.26137403,527.85,24.93,17.69,187.57,188.02,-4.00,85.66,1157.29,97.57,0.3451,0.2843,-0.1172,1.0000,2 +50.612,51.17922001,-2.26117965,527.55,20.87,12.65,188.28,188.42,-2.28,75.45,575.32,97.59,0.0000,0.5216,-0.0391,1.0000,2 +51.056,51.17960342,-2.26103760,524.23,18.50,11.47,188.42,188.23,-0.11,42.63,558.87,97.75,0.1667,0.5706,0.0000,1.0000,2 +51.501,51.17997409,-2.26092492,519.83,16.60,10.03,188.70,188.35,0.28,5.31,556.66,97.89,0.2922,0.6039,0.0000,1.0000,2 +51.945,51.18038148,-2.26080849,516.43,15.25,10.93,188.16,188.03,-2.26,-39.47,558.36,98.04,0.4353,0.4431,0.0000,1.0000,2 +52.445,51.18076290,-2.26068000,516.73,14.56,17.32,187.50,187.63,-4.95,-58.12,561.08,98.18,0.5196,-0.3359,-0.1875,1.0000,2 +52.890,51.18111107,-2.26050581,521.41,17.24,24.64,185.89,185.91,-6.80,-49.68,560.82,98.33,0.1412,-0.6699,-0.2090,1.0000,2 +53.278,51.18143377,-2.26027873,531.19,19.55,25.66,185.15,184.33,-6.50,-8.41,562.97,98.47,-0.0879,-0.5859,-0.2070,1.0000,2 +53.723,51.18177829,-2.26002310,543.66,22.98,25.98,184.31,183.52,-6.37,31.43,562.96,98.62,0.1157,-0.5293,-0.2031,1.0000,2 +54.167,51.18212440,-2.25976078,554.43,24.75,24.54,183.65,183.18,-6.74,56.53,1004.71,98.70,0.3882,-0.0430,-0.1992,1.0000,2 +54.612,51.18246724,-2.25952471,563.30,27.50,19.76,184.21,184.10,-7.53,65.02,1167.87,98.46,0.3706,-0.4121,-0.2090,1.0000,2 +55.056,51.18281855,-2.25932587,572.18,30.66,13.02,185.05,185.16,-7.33,83.60,1166.24,98.19,0.5784,-0.1660,-0.1680,1.0000,2 +55.501,51.18317503,-2.25919240,578.63,34.23,2.64,185.03,185.85,-5.72,90.05,1161.74,97.94,0.5804,0.1863,-0.0527,1.0000,2 +55.890,51.18352558,-2.25914760,581.11,37.36,351.72,185.17,186.30,-3.89,89.64,1156.99,97.69,0.6961,0.2549,-0.0312,1.0000,2 +56.278,51.18387772,-2.25921011,579.72,38.04,336.93,183.81,185.58,-2.55,89.23,1153.19,97.43,0.5274,0.2784,-0.0195,1.0000,2 +56.723,51.18422107,-2.25940568,574.51,34.96,328.30,184.85,185.43,-0.46,86.28,1149.38,97.18,0.5255,0.3490,0.0000,1.0000,2 +57.223,51.18460805,-2.25978543,563.42,30.77,312.30,185.17,186.36,-1.20,79.15,694.04,96.96,0.3843,-0.1953,-0.1699,1.0000,2 +57.667,51.18488168,-2.26020115,555.11,30.19,305.31,185.13,185.38,-0.30,79.11,556.16,97.08,0.3314,0.2079,-0.0996,1.0000,2 +58.112,51.18509464,-2.26067511,545.36,28.24,299.96,184.52,184.33,1.01,76.24,550.57,97.22,0.3784,0.3020,-0.0527,1.0000,2 +58.556,51.18529128,-2.26119977,533.62,23.53,293.71,184.93,184.56,0.88,66.86,554.72,97.37,0.3667,0.0000,-0.1875,1.0000,2 +59.001,51.18545168,-2.26178240,522.19,19.61,286.62,184.51,184.41,-0.25,65.35,553.62,97.53,0.4353,0.0000,-0.1758,1.0000,2 +59.501,51.18556894,-2.26238346,513.69,20.77,279.59,183.97,184.09,-1.45,65.78,555.45,97.68,0.3647,0.0000,-0.1816,1.0000,2 +59.890,51.18563143,-2.26293520,507.77,26.89,274.72,183.86,183.93,-1.40,66.41,557.59,97.81,0.2784,0.0000,-0.1855,1.0000,2 +60.334,51.18566306,-2.26351789,502.08,31.73,271.25,183.82,183.72,-0.67,67.03,558.12,97.95,0.2333,0.1098,-0.1816,1.0000,2 +60.779,51.18566966,-2.26412936,494.86,33.36,268.39,184.13,183.83,0.52,60.80,560.33,98.10,0.0000,0.3588,-0.0781,1.0000,2 +61.223,51.18565491,-2.26477399,484.82,30.31,266.75,184.74,184.09,2.39,36.29,560.98,98.26,0.0294,0.5667,0.0000,1.0000,2 +61.834,51.18561852,-2.26556185,471.18,26.44,265.13,185.57,184.70,3.27,-7.18,563.01,98.44,0.1883,0.4883,0.0000,1.0000,2 +62.334,51.18558354,-2.26626530,458.67,23.74,266.75,187.53,186.65,0.59,-35.61,1160.95,98.28,0.4020,0.3079,0.0000,1.0000,2 +62.890,51.18556556,-2.26708454,451.13,27.16,272.27,189.47,189.13,-1.91,-50.98,788.48,98.00,0.0000,0.1314,0.0000,1.0000,2 +63.445,51.18558548,-2.26778507,448.65,31.84,273.45,190.27,189.88,-0.58,-57.94,567.54,98.14,0.0000,-0.5156,-0.1270,1.0000,2 +63.945,51.18561969,-2.26848667,442.60,32.94,274.99,190.16,189.50,1.20,-38.46,562.03,98.30,0.0432,-0.5078,-0.1270,1.0000,2 +64.556,51.18567599,-2.26933152,431.50,32.37,276.45,190.41,189.53,2.35,-16.06,564.08,98.49,0.2589,-0.2129,-0.1172,1.0000,2 +65.056,51.18573037,-2.27005667,421.63,34.08,277.37,190.74,189.90,1.63,-1.76,567.04,98.67,0.3000,-0.4238,-0.1797,1.0000,2 +65.556,51.18578634,-2.27074524,414.22,35.69,277.01,190.82,190.23,-1.28,24.09,567.66,98.82,0.3569,-0.4648,-0.1797,1.0000,2 +66.056,51.18583267,-2.27141968,412.79,39.06,273.30,189.96,189.69,-4.41,55.43,569.00,98.97,0.5882,-0.5938,-0.1758,1.0000,2 +66.501,51.18584949,-2.27206799,415.30,36.21,260.97,189.00,189.54,-6.85,79.01,1154.79,98.88,0.7392,-0.2031,-0.1211,1.0000,2 +67.001,51.18578698,-2.27274420,420.39,40.61,237.95,184.09,186.28,-8.00,84.51,1178.90,98.60,0.7882,0.0000,-0.0645,1.0000,2 +67.445,51.18559801,-2.27329417,426.48,46.70,216.65,179.34,181.74,-7.44,87.30,1174.33,98.33,0.6902,0.1804,-0.0410,1.0000,2 +68.001,51.18524905,-2.27373849,431.36,50.10,197.14,177.27,178.95,-5.50,87.78,1168.70,98.02,0.5667,0.2432,0.0000,1.0000,2 +68.501,51.18490020,-2.27395119,432.17,47.46,186.87,177.14,177.98,-3.88,84.48,1164.83,97.76,0.4706,0.2079,-0.0332,1.0000,2 +69.001,51.18448668,-2.27404597,429.73,41.59,179.79,179.63,179.74,-2.19,78.53,1160.48,97.47,0.6255,0.0647,-0.0586,1.0000,2 +69.445,51.18409535,-2.27404691,425.25,37.17,168.54,180.19,180.85,-2.91,77.08,619.51,97.36,0.1804,0.4883,-0.0020,1.0000,2 +69.890,51.18374584,-2.27395327,421.46,33.30,166.08,180.70,180.56,-1.21,56.57,562.08,97.50,0.2177,0.4745,0.0000,1.0000,2 +70.334,51.18338177,-2.27380115,416.91,28.75,163.60,180.85,180.55,-1.22,38.49,556.87,97.65,0.0628,0.2667,-0.0117,1.0000,2 +70.834,51.18299405,-2.27360948,413.88,25.80,161.92,180.78,180.45,-0.89,27.18,559.39,97.81,0.1490,0.2098,0.0000,1.0000,2 +71.279,51.18264695,-2.27342418,411.79,23.70,160.91,181.24,180.86,-0.51,18.00,561.63,97.95,0.1412,0.3686,0.0000,1.0000,2 +71.723,51.18228382,-2.27321629,409.47,19.42,160.06,181.26,180.83,-0.44,3.26,561.48,98.10,0.1804,0.3569,0.0000,1.0000,2 +72.168,51.18194751,-2.27301671,407.73,15.79,159.86,181.45,181.05,-1.06,-11.13,563.98,98.25,0.2000,0.3431,0.0000,1.0000,2 +72.612,51.18160711,-2.27282600,407.12,15.12,160.57,181.45,181.08,-2.04,-24.67,564.19,98.40,0.3079,0.2569,0.0000,1.0000,2 +73.001,51.18125579,-2.27263718,408.40,16.53,163.01,181.10,180.81,-4.18,-31.67,565.66,98.55,0.2941,-0.0020,0.0000,1.0000,2 +73.501,51.18088981,-2.27246471,413.47,21.73,165.04,180.71,180.23,-4.98,-32.10,567.09,98.70,0.0608,-0.3125,-0.0273,1.0000,3 +73.946,51.18051670,-2.27231320,420.39,28.77,165.92,180.30,179.76,-4.18,-23.21,567.84,98.85,0.2098,-0.2266,-0.0117,1.0000,3 +74.390,51.18015598,-2.27217885,426.65,34.89,166.85,180.03,179.52,-3.87,-12.49,569.32,99.00,0.1059,-0.4551,-0.0625,1.0000,3 +74.835,51.17977248,-2.27204191,432.85,41.10,167.48,179.76,179.29,-3.34,10.54,570.11,99.15,0.1510,-0.4336,-0.1035,1.0000,3 +75.335,51.17938172,-2.27190072,438.04,46.45,167.30,179.52,179.16,-3.41,33.55,571.23,99.31,0.2549,-0.3301,-0.1211,1.0000,3 +75.779,51.17903386,-2.27176603,441.75,48.91,165.21,179.22,179.05,-4.63,47.95,572.48,99.45,0.3000,-0.3379,-0.1191,1.0000,3 +76.223,51.17866168,-2.27159572,446.02,47.79,161.24,178.80,178.75,-5.30,63.96,835.03,99.58,0.3451,-0.3750,-0.1504,1.0000,3 +76.668,51.17831488,-2.27140433,449.22,47.30,156.48,179.90,179.95,-4.74,79.52,1180.90,99.36,0.5431,-0.1875,-0.0566,1.0000,3 +77.112,51.17801390,-2.27119266,450.03,48.00,147.99,180.77,181.26,-3.71,87.56,1184.80,99.11,0.5451,0.1784,-0.0020,1.0000,3 +77.501,51.17772214,-2.27091119,447.73,45.56,139.07,181.65,182.15,-2.00,86.13,1180.20,98.85,0.5823,0.3392,0.0000,1.0000,3 +77.946,51.17742431,-2.27052532,441.94,39.69,125.39,182.00,182.82,-2.00,81.21,1175.83,98.58,0.5471,0.3882,0.0000,1.0000,3 +78.446,51.17716759,-2.26997530,435.21,33.03,110.62,181.50,182.46,-3.22,72.67,1171.20,98.27,0.5196,0.1863,-0.0684,1.0000,3 +78.946,51.17700992,-2.26936410,432.09,30.04,102.25,183.11,183.39,-4.07,68.32,1167.30,97.98,0.4804,0.0000,-0.1250,1.0000,3 +79.390,51.17692177,-2.26876429,432.14,29.87,94.52,184.11,184.35,-4.98,68.97,606.87,97.91,0.4137,0.1059,-0.1230,1.0000,3 +79.890,51.17688734,-2.26813893,434.37,29.91,88.96,183.46,183.48,-4.80,66.14,565.18,98.06,0.2177,0.3569,-0.0625,1.0000,3 +80.279,51.17689319,-2.26754976,436.71,35.92,86.33,183.24,183.05,-3.78,58.46,561.42,98.21,0.3372,0.1177,-0.0723,1.0000,3 +80.724,51.17692207,-2.26694520,438.63,41.65,81.41,182.38,182.37,-5.08,55.36,563.71,98.37,0.4588,0.2765,-0.0410,1.0000,3 +81.224,51.17698719,-2.26630217,443.44,48.81,76.59,181.73,181.55,-6.35,49.65,566.05,98.54,0.2196,0.0294,-0.0586,1.0000,3 +81.668,51.17707496,-2.26572381,451.05,55.96,74.47,181.05,180.58,-5.76,44.13,565.71,98.69,0.2059,0.0000,-0.0684,1.0000,3 +82.113,51.17718602,-2.26512412,458.99,58.45,72.70,180.58,180.15,-5.18,40.22,567.53,98.85,0.1784,-0.1445,-0.1504,1.0000,3 +82.613,51.17730056,-2.26457392,465.50,47.70,71.01,180.27,179.93,-4.99,44.79,568.11,98.99,0.4784,-0.0645,-0.1504,1.0000,3 +83.057,51.17742731,-2.26402281,471.85,37.32,66.70,179.37,179.24,-7.31,51.73,569.08,99.15,0.4392,-0.2207,-0.1660,1.0000,3 +83.446,51.17757216,-2.26350480,480.80,38.79,60.52,178.33,178.10,-9.47,60.03,894.22,99.26,0.4883,-0.2871,-0.1699,1.0000,3 +83.890,51.17773658,-2.26304040,492.83,44.17,53.10,178.17,177.80,-10.97,68.60,1176.66,99.05,0.4235,-0.3652,-0.1836,1.0000,3 +84.335,51.17797034,-2.26254738,508.66,51.11,43.77,178.40,177.91,-10.78,81.33,1177.92,98.77,0.6059,-0.2539,-0.1777,1.0000,3 +84.835,51.17824890,-2.26209930,523.18,49.91,31.17,177.77,178.16,-9.11,91.61,1172.94,98.48,0.3177,0.0687,-0.1719,1.0000,3 +85.279,51.17854969,-2.26177823,532.55,41.07,25.98,179.02,179.27,-6.06,94.27,1167.13,98.21,0.3020,0.3784,-0.1289,1.0000,3 +85.668,51.17885758,-2.26152352,536.35,36.97,22.86,180.89,181.03,-3.53,78.77,1163.31,97.96,0.2079,0.3863,-0.0859,1.0000,3 +86.113,51.17919537,-2.26128489,536.28,32.82,21.25,183.21,183.24,-1.78,60.44,1158.76,97.69,0.4412,0.0098,-0.1543,1.0000,3 +86.557,51.17953488,-2.26108439,534.39,28.78,15.10,184.85,185.19,-4.25,58.45,1154.81,97.43,0.1706,0.2784,0.0000,1.0000,3 +87.001,51.17990424,-2.26092987,536.43,32.39,11.00,186.15,186.12,-4.44,37.24,1150.92,97.16,-0.0664,0.6902,0.0000,1.0000,3 +87.446,51.18032210,-2.26080806,542.03,40.08,9.63,188.32,187.96,-3.34,-23.85,1146.74,96.88,0.4157,0.5784,0.0000,1.0000,3 +87.890,51.18067731,-2.26070439,546.52,44.15,12.85,189.58,189.50,-5.40,-66.40,1143.29,96.64,0.6000,-0.0020,-0.0918,1.0000,3 +88.279,51.18101357,-2.26058005,550.96,48.26,21.10,190.14,190.33,-5.22,-74.86,1139.33,96.40,0.3765,-0.2070,-0.0352,1.0000,3 +88.724,51.18138359,-2.26035899,555.99,47.59,24.67,191.55,191.34,-3.88,-58.21,1135.24,96.12,0.0353,-0.7441,-0.1699,1.0000,3 +89.168,51.18174201,-2.26010180,559.84,41.87,25.63,193.30,192.93,-2.35,3.59,1130.98,95.86,0.3177,-0.4473,-0.1680,1.0000,3 +89.613,51.18209333,-2.25983091,563.28,36.21,24.53,194.65,194.45,-4.99,45.38,1127.04,95.60,0.4392,-0.2441,-0.1602,1.0000,3 +90.057,51.18245526,-2.25958357,569.18,32.98,18.46,195.24,195.20,-7.40,63.43,1122.84,95.33,0.3490,-0.4219,-0.1660,1.0000,3 +90.501,51.18285226,-2.25937839,578.12,37.26,12.32,195.95,195.79,-6.77,85.72,1118.67,95.05,0.5706,-0.1406,-0.0352,1.0000,3 +90.946,51.18323060,-2.25924835,584.26,41.18,0.50,195.68,196.43,-4.60,93.83,1114.80,94.78,0.5902,0.0844,-0.0098,1.0000,3 +91.390,51.18363147,-2.25922473,584.63,42.04,348.50,194.90,195.74,-2.03,87.02,1110.24,94.51,0.6921,0.1706,-0.0020,1.0000,3 +91.779,51.18399525,-2.25932203,581.00,40.58,335.58,194.37,195.57,-3.05,79.27,1106.84,94.27,0.6961,0.0000,-0.0527,1.0000,3 +92.224,51.18438180,-2.25958625,578.36,42.41,320.56,191.98,193.26,-4.18,79.60,1103.45,94.00,0.4686,-0.2344,-0.1211,1.0000,3 +92.724,51.18469602,-2.25997002,577.43,48.76,312.65,193.02,193.47,-3.01,82.38,1099.82,93.74,0.4843,-0.0254,-0.0781,1.0000,3 +93.168,51.18498360,-2.26045259,574.54,54.08,304.18,193.84,194.18,-1.85,83.54,1096.16,93.47,0.4275,-0.1543,-0.0820,1.0000,3 +93.557,51.18520297,-2.26094099,569.51,56.05,297.52,194.97,195.06,-0.58,84.73,1092.80,93.22,0.4275,-0.2441,-0.0547,1.0000,3 +94.001,51.18539686,-2.26150346,560.24,54.34,289.37,196.34,196.05,1.36,91.09,1088.96,92.96,0.3882,0.1510,-0.0234,1.0000,3 +94.446,51.18554686,-2.26213898,546.75,48.62,283.26,198.11,197.02,3.60,88.57,1085.82,92.71,0.3333,0.4863,0.0000,1.0000,3 +94.946,51.18565088,-2.26278813,527.66,42.81,277.33,200.30,198.40,4.42,69.23,1081.91,92.44,0.2706,0.3784,0.0000,1.0000,3 +95.390,51.18570899,-2.26346793,507.13,35.50,272.63,202.45,200.56,3.94,57.92,1079.52,92.18,0.4137,0.0216,-0.1641,1.0000,3 +95.946,51.18572620,-2.26426983,486.93,26.98,268.20,204.80,203.22,3.32,40.33,1074.93,91.87,0.2039,0.2471,-0.1211,1.0000,3 +96.446,51.18570631,-2.26504550,471.10,19.51,266.29,207.06,205.54,2.84,19.65,1072.60,91.59,0.1020,0.4706,0.0000,1.0000,3 +96.946,51.18567388,-2.26578494,458.69,16.69,265.48,208.94,207.63,2.18,-13.03,1068.56,91.32,0.2059,0.4588,0.0000,1.0000,3 +97.446,51.18563945,-2.26657103,448.22,17.50,266.89,210.69,209.44,0.89,-42.04,1065.57,91.04,0.4471,0.0667,-0.0488,1.0000,3 +97.890,51.18562286,-2.26725814,441.20,18.69,272.24,211.57,210.72,-1.68,-51.93,1061.92,90.80,0.2882,0.0392,0.0000,1.0000,3 +98.390,51.18564194,-2.26799880,439.36,24.58,275.28,212.45,211.43,-1.50,-42.34,1058.51,90.52,0.0000,-0.6953,-0.0918,1.0000,3 +98.890,51.18569035,-2.26877362,438.75,32.45,276.74,213.60,212.45,-0.48,20.85,1054.38,90.24,0.0000,-0.5879,-0.1309,1.0000,3 +99.390,51.18574090,-2.26954186,436.33,39.92,276.41,214.56,213.46,-0.59,60.31,1050.42,89.97,0.0000,0.1177,-0.1328,1.0000,3 +99.835,51.18578075,-2.27020601,430.92,45.99,274.31,215.61,214.35,0.31,64.06,1046.51,89.71,0.5196,0.1294,-0.1738,1.0000,3 +100.279,51.18580670,-2.27094612,423.44,47.06,264.92,215.74,215.08,-2.80,65.08,1042.87,89.44,0.3138,-0.1133,-0.2266,1.0000,3 +100.724,51.18576758,-2.27169533,421.94,45.43,258.49,215.51,214.61,-3.37,66.77,1038.90,89.16,0.3412,-0.1738,-0.2285,1.0000,3 +101.168,51.18567796,-2.27237748,423.53,43.57,251.17,215.54,214.71,-4.33,76.05,1035.01,88.88,0.5863,-0.3359,-0.2129,1.0000,3 +101.668,51.18553325,-2.27305004,425.79,45.87,235.37,213.31,213.65,-4.16,88.60,1031.13,88.60,0.8117,0.1098,-0.0762,1.0000,3 +102.113,51.18528942,-2.27364346,425.43,44.39,208.45,205.16,208.13,-3.27,88.75,1027.28,88.32,0.6549,0.3745,-0.0332,1.0000,3 +102.557,51.18492979,-2.27401131,422.26,38.54,190.00,200.26,201.86,-3.49,83.48,1023.61,88.04,0.7608,0.1863,-0.0820,1.0000,3 +103.001,51.18451105,-2.27414847,419.76,31.70,173.84,197.16,198.23,-3.76,81.22,1020.21,87.76,0.2745,0.4314,0.0000,1.0000,3 +103.446,51.18410156,-2.27410683,418.09,30.01,170.06,198.34,197.83,-2.15,64.10,1016.90,87.49,0.3000,0.3628,0.0000,1.0000,3 +103.946,51.18367040,-2.27398278,416.42,28.40,166.54,199.55,198.91,-2.47,42.93,1012.92,87.20,0.2373,0.0000,-0.1602,1.0000,3 +104.390,51.18325673,-2.27381541,417.44,29.57,164.49,200.53,199.76,-2.87,40.34,1009.07,86.93,0.2079,-0.3223,-0.1758,1.0000,3 +104.890,51.18283278,-2.27362089,420.09,32.17,162.66,201.43,200.63,-2.89,50.60,1004.91,86.64,0.1588,-0.0488,-0.1152,1.0000,3 +105.335,51.18241768,-2.27340238,421.47,32.70,161.05,202.38,201.60,-2.07,54.88,1000.49,86.35,0.0000,0.5588,0.0000,1.0000,3 +105.779,51.18201131,-2.27316725,420.46,28.78,159.30,203.51,202.65,-0.76,28.28,996.39,86.07,0.1804,0.4863,0.0000,1.0000,3 +106.279,51.18159465,-2.27290870,418.74,26.66,158.03,204.66,203.69,-0.67,-11.64,992.20,85.78,0.2961,0.4529,0.0000,1.0000,3 +106.779,51.18113679,-2.27262906,418.20,26.26,160.61,205.40,204.61,-3.63,-40.01,987.74,85.47,0.1628,-0.0020,-0.0410,1.0000,4 +107.279,51.18073712,-2.27241266,422.12,30.32,162.69,205.82,204.82,-3.65,-39.29,983.67,85.20,0.0000,-0.3906,-0.1094,1.0000,4 +107.724,51.18033044,-2.27221603,427.05,35.21,163.71,206.43,205.39,-2.64,-23.81,979.49,84.92,0.0177,-0.3867,-0.0625,1.0000,4 +108.168,51.17990468,-2.27202563,431.08,39.21,164.59,207.02,206.01,-2.12,-5.13,975.57,84.64,0.0196,-0.4141,-0.0957,1.0000,4 +108.613,51.17946677,-2.27183629,434.41,42.53,165.00,207.65,206.64,-1.98,18.04,970.92,84.35,0.0275,-0.3613,-0.1270,1.0000,4 +109.113,51.17904111,-2.27164303,436.93,43.51,164.56,208.24,207.28,-1.93,40.85,966.92,84.07,0.0353,-0.4004,-0.1602,1.0000,4 +109.613,51.17859332,-2.27143682,437.11,36.42,163.32,208.83,207.97,-1.64,62.55,962.44,83.78,0.3333,-0.4629,-0.1211,1.0000,4 +110.001,51.17823311,-2.27124902,434.80,32.67,158.04,209.20,208.49,-2.00,78.73,958.84,83.53,0.7039,0.0412,-0.0547,1.0000,4 +110.390,51.17786718,-2.27102233,431.21,29.05,145.86,207.91,208.08,-2.57,81.88,955.28,83.27,0.6784,0.3059,0.0000,1.0000,4 +110.835,51.17750131,-2.27062796,427.61,25.57,130.35,205.34,205.88,-4.19,75.88,951.30,82.99,0.7216,0.0236,-0.0625,1.0000,4 +111.335,51.17720372,-2.27006064,430.04,28.32,109.81,199.48,200.68,-7.89,76.58,947.41,82.69,0.2863,0.0353,-0.0977,1.0000,4 +111.779,51.17704878,-2.26947758,437.63,36.10,104.52,199.42,198.81,-6.48,77.67,944.06,82.43,0.4216,-0.1055,-0.0703,1.0000,4 +112.224,51.17694383,-2.26885395,445.31,43.58,97.20,199.15,198.76,-5.93,79.09,940.28,82.15,0.2510,-0.2324,-0.0762,1.0000,4 +112.668,51.17688567,-2.26821580,450.82,45.40,93.52,199.39,198.85,-4.35,84.02,935.81,81.87,0.3843,0.2451,0.0000,1.0000,4 +113.112,51.17685958,-2.26754722,452.97,50.77,89.24,199.82,199.37,-2.65,77.53,931.88,81.60,0.3196,0.2039,0.0000,1.0000,4 +113.557,51.17686301,-2.26688216,451.60,56.50,85.29,200.53,200.02,-1.87,69.81,927.32,81.31,0.3412,0.3392,0.0000,1.0000,4 +114.057,51.17690138,-2.26615805,448.75,53.69,78.43,200.83,200.50,-3.33,61.53,923.02,81.01,0.4588,0.1961,0.0000,1.0000,4 +114.501,51.17698722,-2.26550257,450.23,55.42,70.62,200.07,199.90,-6.31,57.96,919.16,80.73,0.1843,-0.0508,0.0000,1.0000,4 +115.001,51.17713279,-2.26482771,458.60,55.99,67.40,200.14,199.26,-5.79,58.15,914.97,80.42,0.4314,0.1020,0.0000,1.0000,4 +115.446,51.17729954,-2.26421493,467.40,38.65,62.55,199.89,199.15,-6.88,61.44,910.65,80.14,0.2020,0.0000,-0.0273,1.0000,4 +115.890,51.17748420,-2.26364070,477.13,35.84,59.51,199.90,199.05,-6.20,65.66,906.49,79.86,0.4098,-0.2520,-0.0625,1.0000,4 +116.335,51.17770515,-2.26306807,485.65,35.58,52.74,199.44,198.97,-7.22,68.96,902.30,79.58,0.6137,0.0000,-0.1074,1.0000,4 +116.835,51.17797966,-2.26250065,497.56,35.33,37.78,196.71,197.01,-10.83,73.74,897.45,79.29,0.5431,-0.3711,-0.1816,1.0000,4 +117.279,51.17827326,-2.26211103,512.70,33.04,27.20,194.09,193.47,-10.65,85.57,894.04,79.04,0.3118,-0.2070,-0.1855,1.0000,4 +117.724,51.17864725,-2.26177980,528.72,40.41,21.88,193.73,193.05,-7.56,95.49,888.92,78.73,0.0000,0.3157,-0.1719,1.0000,4 +118.168,51.17902422,-2.26152965,537.90,40.23,20.21,194.00,193.63,-4.89,94.37,885.03,78.45,-0.1250,0.4471,-0.1875,1.0000,4 +118.613,51.17938869,-2.26131091,541.26,41.45,19.31,194.52,194.33,-2.39,75.80,880.16,78.18,-0.2070,0.7608,-0.0625,1.0000,4 +119.113,51.17980170,-2.26108316,539.88,38.75,17.99,195.58,195.17,0.31,2.11,876.13,77.90,0.3471,0.4883,-0.0840,1.0000,4 +119.613,51.18025292,-2.26084419,536.70,34.52,21.52,196.01,196.04,-4.86,-39.66,871.78,77.60,0.5196,0.0314,-0.0508,1.0000,4 +120.113,51.18063504,-2.26059637,543.46,37.80,27.08,195.00,194.28,-8.05,-30.95,867.89,77.32,0.0000,-0.6934,-0.1758,1.0000,4 +120.557,51.18099362,-2.26030279,559.11,46.96,27.62,194.88,193.44,-6.89,21.80,863.65,77.04,0.0000,-0.2012,-0.1836,1.0000,4 +121.002,51.18135074,-2.26001783,573.27,57.01,27.54,194.65,193.67,-6.78,54.73,859.86,76.78,0.2412,-0.2832,-0.1699,1.0000,4 +121.446,51.18171691,-2.25972889,584.10,55.52,24.76,194.38,194.08,-6.52,80.61,855.42,76.50,0.5117,-0.3809,-0.1035,1.0000,4 +121.890,51.18207186,-2.25947536,590.71,54.02,16.12,193.79,194.17,-5.08,91.97,850.93,76.22,0.3628,0.0138,-0.0723,1.0000,4 +122.335,51.18243631,-2.25928766,592.58,48.66,10.15,193.61,193.91,-2.33,92.91,847.16,75.96,0.4373,0.1667,-0.0547,1.0000,4 +122.835,51.18291030,-2.25915189,587.32,40.86,359.83,193.52,193.85,-0.24,84.58,842.40,75.65,0.5412,-0.0879,-0.1680,1.0000,4 +123.335,51.18333747,-2.25914755,578.01,33.44,348.81,192.87,193.04,0.33,82.54,838.68,75.37,0.6784,0.0471,-0.1172,1.0000,4 +123.891,51.18384297,-2.25931658,563.93,23.06,331.39,191.36,192.01,0.27,82.03,834.55,75.06,0.4039,0.3275,-0.1035,1.0000,4 +124.446,51.18427434,-2.25966412,549.51,14.84,321.35,191.42,191.18,0.59,72.43,830.40,74.76,0.4843,0.3412,-0.1289,1.0000,4 +125.057,51.18468977,-2.26022267,536.24,12.30,311.04,191.41,191.49,-1.27,62.63,826.47,74.42,0.4627,0.2784,-0.1992,1.0000,4 +125.613,51.18503910,-2.26085555,529.41,15.55,303.55,191.95,192.06,-2.59,62.93,822.15,74.08,0.2745,-0.2090,-0.3105,1.0000,4 +126.224,51.18532597,-2.26155851,526.00,21.10,298.01,192.48,192.41,-2.25,68.90,817.33,73.75,0.3451,-0.3691,-0.2539,1.0000,4 +126.779,51.18554309,-2.26221128,522.06,25.79,291.21,192.81,192.79,-1.51,84.72,813.28,73.46,0.5922,-0.3789,-0.1816,1.0000,4 +127.280,51.18570700,-2.26290057,513.82,31.72,277.31,191.72,192.15,0.64,90.79,809.28,73.18,0.1726,0.6196,-0.0840,1.0000,4 +127.835,51.18579281,-2.26372725,497.01,29.75,270.68,193.01,192.04,1.87,66.54,804.83,72.85,0.6353,0.2784,-0.1953,1.0000,4 +128.391,51.18579992,-2.26444490,483.50,25.77,263.24,192.85,192.60,-0.46,46.25,800.53,72.56,0.0000,0.5078,-0.0273,1.0000,4 +129.002,51.18573421,-2.26531773,476.08,28.25,261.51,194.12,193.53,0.57,-3.29,796.49,72.21,0.0941,0.6255,0.0000,1.0000,4 +129.502,51.18566357,-2.26606913,470.55,32.86,261.92,194.88,194.30,-0.23,-60.24,792.08,71.90,0.3451,0.4686,-0.1016,1.0000,4 +130.002,51.18560996,-2.26678348,462.56,34.91,270.48,195.14,194.75,0.59,-81.54,788.15,71.62,0.4490,-0.5566,-0.1953,1.0000,4 +130.557,51.18560887,-2.26755374,451.70,32.01,278.70,195.24,194.64,0.43,-54.14,783.89,71.31,0.0941,-0.4258,-0.1992,1.0000,4 +131.057,51.18567147,-2.26827435,444.70,32.51,280.24,195.88,195.17,0.70,-19.93,779.92,71.02,0.0000,-0.5859,-0.1973,1.0000,4 +131.613,51.18575542,-2.26900220,438.94,34.86,281.10,196.63,195.82,1.11,28.25,775.79,70.72,0.1902,-0.5020,-0.1855,1.0000,4 +132.113,51.18583592,-2.26971439,431.39,37.75,278.62,197.14,196.41,-0.36,54.63,771.91,70.44,0.4137,0.0373,-0.1582,1.0000,4 +132.557,51.18589166,-2.27037281,424.89,43.21,272.92,197.17,196.72,-1.88,60.49,767.78,70.16,0.3235,-0.3711,-0.1816,1.0000,4 +133.057,51.18591100,-2.27112695,420.38,46.86,266.17,197.13,196.76,-2.45,70.18,763.59,69.84,0.3941,-0.3691,-0.1816,1.0000,4 +133.557,51.18588484,-2.27178473,417.16,40.82,257.47,196.48,196.35,-2.83,78.06,759.49,69.56,0.6255,0.0000,-0.1309,1.0000,4 +134.002,51.18579831,-2.27241444,414.44,34.36,242.57,193.83,194.71,-3.83,82.01,755.61,69.28,0.6706,0.0196,-0.1133,1.0000,4 +134.446,51.18562548,-2.27298760,412.27,32.19,225.77,190.25,191.66,-3.92,84.40,751.30,68.99,0.7529,0.0020,-0.1172,1.0000,4 +134.891,51.18535597,-2.27345270,409.92,29.82,206.27,185.27,187.39,-4.01,85.06,747.38,68.71,0.6372,0.3157,-0.1641,1.0000,4 +135.446,51.18493087,-2.27380541,406.49,21.51,192.18,182.81,183.28,-3.97,68.83,742.18,68.36,0.5745,-0.3477,-0.4277,1.0000,4 +135.891,51.18456034,-2.27393535,406.63,18.72,179.97,181.26,182.20,-7.07,71.44,738.39,68.08,0.4588,-0.2422,-0.3418,1.0000,4 +136.335,51.18421495,-2.27394972,411.27,23.52,171.62,179.64,179.95,-7.31,74.78,734.59,67.82,0.2216,-0.0059,-0.3164,1.0000,4 +136.780,51.18385005,-2.27387415,417.23,29.40,169.53,180.16,179.98,-5.01,76.28,730.05,67.54,0.4137,-0.1738,-0.2305,1.0000,4 +137.224,51.18348683,-2.27376263,419.79,31.83,164.51,180.31,180.33,-3.95,79.22,725.79,67.25,0.0000,0.5765,-0.2129,1.0000,4 +137.669,51.18311438,-2.27360042,419.31,31.23,162.91,181.04,180.82,-1.51,53.68,721.48,66.96,0.0902,0.5804,-0.1738,1.0000,4 +138.113,51.18274604,-2.27341751,416.40,28.28,161.50,182.00,181.55,0.09,15.50,717.44,66.69,0.0275,0.0000,-0.2090,1.0000,4 +138.558,51.18238342,-2.27321580,412.88,23.34,160.89,182.85,182.35,0.31,6.91,713.44,66.41,0.1177,0.2490,-0.0879,1.0000,4 +139.002,51.18202893,-2.27301898,409.51,17.85,160.61,183.57,183.09,-0.04,0.84,709.36,66.13,0.2059,0.0863,-0.1270,1.0000,4 +139.502,51.18165752,-2.27281035,406.83,14.79,160.61,184.18,183.73,-1.17,-0.31,705.26,65.85,0.4392,0.0196,-0.1641,1.0000,4 +139.947,51.18129250,-2.27260575,407.75,15.84,160.92,184.20,183.83,-5.26,-0.50,701.48,65.57,0.3138,0.0373,-0.1582,1.0000,4 diff --git a/track_data/flight_20260901_225009_000000.csv b/track_data/flight_20260901_225009_000000.csv new file mode 100644 index 00000000..d0585041 --- /dev/null +++ b/track_data/flight_20260901_225009_000000.csv @@ -0,0 +1,92 @@ +# aircraft_title=Extra 300S Paint2 +# recorded_at=2026-09-01T22:50:09 +# laps_s=,,, +timestamp_s,latitude,longitude,altitude_ft,agl_ft,heading_deg,airspeed_kts,groundspeed_kts,pitch_deg,bank_deg,torque,health_pct,stick_pitch,stick_roll,stick_yaw,stick_throttle,lap +0.000,41.02165563,28.96417770,480.28,479.26,354.18,181.56,171.07,7.60,-3.84,1132.49,100.00,0.0000,0.4647,-0.0176,1.0000,1 +0.444,41.02200671,28.96412871,458.84,457.80,354.15,185.21,174.46,7.63,-25.34,1191.49,100.00,0.0608,0.4059,-0.0215,1.0000,1 +1.056,41.02249775,28.96407156,428.65,427.66,355.89,189.88,178.73,7.36,-51.85,1194.30,100.00,0.2843,0.4235,0.0000,1.0000,1 +1.444,41.02281782,28.96404674,407.84,406.88,359.34,192.76,181.38,7.10,-66.92,1195.23,100.00,0.4647,0.1098,-0.0586,1.0000,1 +1.833,41.02315524,28.96404467,384.32,383.29,6.29,195.50,184.06,7.07,-71.04,1196.17,100.00,0.2549,0.0549,-0.0508,1.0000,1 +2.278,41.02355516,28.96409933,357.57,336.56,9.77,198.80,186.99,8.15,-71.20,1197.26,100.00,0.0726,-0.2070,-0.1328,1.0000,1 +2.667,41.02388085,28.96417464,333.65,331.86,10.89,201.97,189.16,9.51,-62.86,1198.15,100.00,0.2255,-0.2480,-0.0762,1.0000,1 +3.111,41.02423470,28.96427573,304.40,302.96,13.67,205.30,191.77,9.80,-53.47,1199.17,100.00,0.2667,-0.3711,-0.0781,1.0000,1 +3.500,41.02457596,28.96439355,276.33,275.03,16.54,208.19,195.11,9.49,-35.90,1200.18,100.00,0.0941,-0.4883,-0.1797,1.0000,1 +3.889,41.02494136,28.96454513,247.89,246.68,17.61,211.25,198.48,9.60,-2.98,1201.16,100.00,0.0941,-0.4141,-0.1758,1.0000,1 +4.278,41.02528643,28.96469453,221.81,220.50,17.89,214.09,201.25,8.98,31.67,1202.04,100.00,0.1588,-0.5762,-0.1973,1.0000,1 +4.722,41.02565968,28.96485135,194.16,192.94,15.85,217.14,203.65,7.96,64.94,1202.93,100.00,0.3647,-0.1211,-0.0625,1.0000,1 +5.167,41.02605200,28.96499321,165.21,164.07,8.63,219.49,205.96,6.85,69.85,1203.89,100.00,0.5216,-0.1016,-0.0605,1.0000,1 +5.556,41.02639577,28.96506828,141.63,140.61,359.05,220.13,207.73,4.66,70.63,1204.74,100.00,0.3549,-0.1523,-0.0723,1.0000,1 +5.944,41.02680069,28.96506436,119.66,118.82,350.20,220.77,209.26,3.59,71.56,1205.57,100.00,0.4157,-0.1836,-0.0469,1.0000,1 +6.333,41.02719806,28.96497627,101.39,100.66,342.30,221.78,210.81,2.91,72.05,1206.30,100.00,0.2020,0.0000,-0.0762,1.0000,1 +6.722,41.02757048,28.96481685,85.55,84.78,338.18,223.58,212.44,3.51,72.40,1206.83,100.00,0.5059,-0.0312,-0.0195,1.0000,1 +7.111,41.02792864,28.96461780,69.74,69.03,329.38,224.57,214.04,1.93,72.11,1207.31,100.00,0.2863,0.1275,0.0000,1.0000,1 +7.611,41.02831748,28.96431010,54.37,53.77,323.29,225.69,215.69,1.87,68.04,1207.84,100.00,0.3882,0.2373,0.0000,1.0000,1 +8.056,41.02867860,28.96393278,41.25,40.87,315.10,226.50,217.61,-0.34,63.00,1208.25,100.00,0.4431,0.2412,0.0000,1.0000,1 +8.500,41.02901473,28.96347238,34.60,34.52,307.25,226.46,219.05,-2.82,59.83,1208.54,100.00,0.2118,-0.0547,-0.1387,1.0000,1 +9.000,41.02929601,28.96294757,34.97,35.02,304.03,227.36,220.24,-2.55,59.98,1208.48,100.00,0.1353,0.2235,0.0000,1.0000,1 +9.444,41.02954396,28.96243091,35.81,35.82,301.66,228.32,221.48,-1.98,59.06,1208.31,100.00,0.0667,0.2510,0.0000,1.0000,1 +9.944,41.02978142,28.96185695,35.53,35.51,299.14,229.38,222.76,-1.45,34.35,1208.13,100.00,0.0000,0.5725,0.0000,1.0000,1 +10.444,41.03001893,28.96125842,36.43,36.54,297.71,230.48,223.87,-2.02,-15.93,1207.96,100.00,0.0510,0.4451,0.0000,1.0000,1 +10.944,41.03024955,28.96064536,39.83,39.97,298.93,231.31,224.56,-2.92,-56.78,1207.64,100.00,0.3196,0.3412,-0.0137,1.0000,1 +11.389,41.03048998,28.96005577,42.47,42.56,306.42,231.70,224.84,-3.31,-78.56,1207.35,100.00,0.5471,0.2726,-0.0195,1.0000,1 +11.889,41.03076256,28.95953765,44.55,44.60,320.35,230.30,222.72,-1.74,-88.72,1207.14,100.00,0.2706,0.0000,-0.0684,1.0000,1 +12.333,41.03109361,28.95912755,43.32,43.14,325.24,230.36,221.23,0.16,-83.72,1207.19,100.00,0.1334,-0.5605,-0.1660,1.0000,1 +12.722,41.03144579,28.95878185,37.47,37.15,327.63,231.79,221.91,1.11,-49.56,1207.34,100.00,0.1765,-0.6172,-0.1719,1.0000,1 +13.222,41.03185089,28.95842599,31.07,30.89,329.55,232.92,222.97,-0.33,9.81,1207.62,100.00,0.2706,-0.5566,-0.1699,1.0000,1 +13.611,41.03221656,28.95811845,29.56,29.59,327.08,233.12,223.84,-3.73,52.79,1207.65,100.00,0.4333,-0.3281,-0.0977,1.0000,1 +14.111,41.03261904,28.95773770,33.65,33.99,316.21,232.84,224.07,-7.06,75.13,1207.57,100.00,0.7039,-0.2188,-0.0664,1.0000,1 +14.555,41.03292894,28.95732944,43.06,43.58,295.92,228.33,222.37,-8.24,86.18,1207.27,100.00,0.5608,-0.2324,-0.1074,1.0000,1 +15.000,41.03314744,28.95676334,54.76,55.17,282.33,224.06,219.77,-6.18,89.47,1207.04,100.00,0.3529,0.0000,-0.1641,1.0000,1 +15.444,41.03323865,28.95620607,62.07,62.28,274.32,224.51,220.90,-4.19,91.33,1206.90,100.00,0.6510,-0.1133,-0.1367,1.0000,1 +15.944,41.03325806,28.95551139,64.38,64.32,261.04,222.27,221.43,-1.85,80.33,1206.88,100.00,-0.2461,0.9137,0.0000,1.0000,1 +16.389,41.03317784,28.95486883,61.47,61.24,260.01,224.61,223.07,0.25,-2.36,1207.05,100.00,0.5176,0.7882,0.0000,1.0000,1 +16.944,41.03307189,28.95416835,58.37,58.28,272.36,224.78,223.99,-1.32,-106.96,1207.23,100.00,0.3686,0.3079,-0.1621,1.0000,1 +17.389,41.03305099,28.95354438,44.21,42.97,278.96,223.56,217.58,7.33,-146.13,1207.52,100.00,-0.8418,0.5059,-0.0762,1.0000,1 +17.833,41.03308628,28.95295000,23.62,23.67,275.39,220.23,220.46,-15.24,177.84,1208.66,100.00,-0.6699,0.5784,-0.0371,1.0000,1 +18.333,41.03310958,28.95233660,58.88,61.35,291.43,209.25,195.10,-21.55,116.41,1208.90,100.00,0.2843,0.0353,-0.1582,1.0000,1 +18.778,41.03319886,28.95183590,115.67,117.90,283.85,208.61,190.31,-19.52,105.79,1207.01,100.00,0.7863,0.0588,-0.0625,1.0000,1 +19.167,41.03327593,28.95135117,161.68,163.36,262.09,202.64,194.85,-14.05,109.67,1204.83,100.00,0.6725,0.4020,-0.0117,1.0000,1 +19.667,41.03323750,28.95081434,191.99,192.90,239.61,195.67,197.43,-7.71,103.14,1203.51,100.00,0.9647,0.3000,-0.0312,1.0000,1 +20.111,41.03304066,28.95033382,203.36,203.53,203.55,181.10,192.69,-6.81,84.48,1203.42,100.00,0.9882,0.3157,-0.0332,1.0000,1 +20.556,41.03269010,28.95006290,212.54,213.27,179.40,167.92,179.41,-18.05,55.05,1204.39,100.00,0.0000,0.0000,-0.0352,1.0000,1 +21.111,41.03224773,28.95001401,243.71,245.49,181.29,170.66,174.16,-14.06,52.51,1204.45,100.00,0.0000,0.0000,-0.0703,1.0000,1 +21.667,41.03182098,28.95001337,273.46,274.35,179.48,172.11,176.39,-12.89,53.14,1203.42,99.99,0.0000,0.0000,-0.0703,1.0000,1 +22.167,41.03141577,28.95002753,298.36,299.14,177.79,173.45,178.40,-11.58,53.82,1198.55,99.73,0.0000,0.0000,-0.0723,1.0000,1 +22.722,41.03090793,28.95006788,324.76,322.36,175.63,175.37,180.98,-9.84,54.49,1192.63,99.39,0.0000,0.0000,-0.0703,1.0000,1 +23.278,41.03048139,28.95011771,342.76,340.15,173.91,176.54,183.20,-8.53,55.22,1187.79,99.12,0.0000,0.0000,-0.0703,1.0000,1 +23.834,41.02998404,28.95020001,359.43,346.43,171.85,178.64,185.84,-7.00,56.25,1182.43,98.80,0.0000,0.0000,-0.0684,1.0000,1 +24.334,41.02956536,28.95028448,369.83,325.95,170.03,181.24,188.12,-5.70,57.16,1178.10,98.54,0.0000,0.0000,-0.0684,1.0000,1 +24.834,41.02912782,28.95039187,377.08,294.59,168.10,183.10,190.49,-4.34,58.10,1173.99,98.28,0.0000,0.0000,-0.0684,1.0000,1 +25.334,41.02868696,28.95052168,380.75,265.50,166.18,185.71,192.86,-2.95,58.51,1169.92,98.01,0.0000,0.0000,-0.0664,1.0000,1 +25.834,41.02825878,28.95066245,380.93,256.29,164.24,187.74,195.17,-1.70,59.29,1166.00,97.76,0.0000,0.0000,-0.0664,1.0000,1 +26.334,41.02782584,28.95082733,377.53,212.16,162.33,190.64,197.48,-0.47,60.02,1162.36,97.51,0.0000,0.0000,-0.0664,1.0000,1 +26.834,41.02740456,28.95100649,371.09,184.51,160.36,193.22,199.69,0.79,60.33,1158.80,97.26,0.0000,0.0000,-0.0664,1.0000,1 +27.334,41.02696793,28.95121295,360.90,166.35,158.33,195.75,202.02,2.04,60.61,1155.24,97.00,0.0000,0.0000,-0.0664,1.0000,1 +27.834,41.02654625,28.95143299,347.70,161.47,156.27,198.77,204.20,3.19,60.99,1152.04,96.75,0.0000,0.0000,-0.0664,1.0000,1 +28.278,41.02616148,28.95165423,332.81,158.27,154.42,201.05,206.27,4.17,61.05,1149.12,96.52,0.0000,0.0000,-0.0664,1.0000,1 +28.723,41.02576088,28.95190490,314.29,159.77,152.40,204.08,208.35,5.11,61.53,1146.35,96.29,0.0000,0.0000,-0.0664,1.0000,1 +29.223,41.02533610,28.95220116,291.24,153.51,149.99,206.91,210.57,6.06,61.66,1143.40,96.04,0.4627,0.1922,-0.0664,1.0000,1 +29.667,41.02495288,28.95248511,268.90,143.89,142.37,208.88,212.69,2.93,57.89,1140.99,95.82,0.5137,0.2000,-0.0547,1.0000,1 +30.167,41.02456300,28.95287090,252.52,135.43,131.90,209.41,213.91,-2.83,54.63,1137.96,95.56,0.5137,0.0961,-0.0430,1.0000,1 +30.612,41.02424278,28.95331567,251.70,132.23,121.86,208.68,212.44,-8.02,55.86,1134.63,95.32,0.4745,0.0451,-0.0645,1.0000,1 +31.056,41.02399909,28.95378943,263.99,141.82,113.10,207.13,208.96,-11.58,58.09,1131.03,95.09,0.3196,0.0000,-0.1953,1.0000,1 +31.500,41.02381840,28.95428860,285.58,162.94,107.75,206.55,205.81,-12.46,59.64,1126.99,94.87,-0.3340,0.0902,-0.0703,1.0000,1 +31.945,41.02367342,28.95481412,310.36,187.94,111.75,206.71,205.45,-6.96,59.46,1122.58,94.63,-0.9160,0.0569,-0.0977,1.0000,1 +32.389,41.02349975,28.95534984,320.81,203.71,137.29,197.69,205.44,13.36,65.75,1118.27,94.39,-0.9570,0.0569,-0.0957,1.0000,1 +32.889,41.02320781,28.95574790,293.59,178.09,161.80,186.52,190.83,25.79,82.50,1115.89,94.15,0.1922,0.0981,-0.1543,1.0000,1 +33.278,41.02287649,28.95594194,247.83,130.99,155.01,193.28,186.82,20.79,74.50,1115.06,93.92,0.7804,0.2882,-0.0586,1.0000,1 +33.723,41.02251544,28.95615774,195.18,76.72,133.28,192.62,190.15,9.79,59.80,1114.18,93.68,0.8235,0.2882,-0.0410,1.0000,1 +34.223,41.02220422,28.95653583,160.77,45.20,113.17,186.11,191.93,-6.43,47.09,1112.94,93.44,0.5176,0.2608,0.0000,1.0000,1 +34.723,41.02200716,28.95705900,165.07,54.66,107.23,184.95,185.47,-8.57,42.58,1110.13,93.18,-0.4941,0.0000,-0.0352,1.0000,1 +35.223,41.02186268,28.95761306,179.23,80.35,110.71,186.30,187.64,-0.74,42.93,1105.66,92.91,-0.5703,0.0000,-0.0371,1.0000,1 +35.723,41.02169912,28.95815868,176.02,88.75,116.74,187.89,189.69,9.40,44.64,1101.35,92.64,-0.3613,0.0000,-0.0332,1.0000,1 +36.278,41.02149158,28.95870628,146.28,75.89,118.13,191.78,188.72,12.00,45.34,1097.81,92.35,0.9059,0.1177,-0.0664,1.0000,1 +36.834,41.02125558,28.95928678,110.66,55.06,98.97,188.57,192.64,-8.94,45.04,1094.74,92.04,0.5274,0.0451,-0.0391,1.0000,1 +37.334,41.02115872,28.95985920,120.50,76.82,90.65,184.82,182.58,-13.14,47.00,1091.40,91.76,0.0000,0.0000,-0.1816,1.0000,1 +37.834,41.02114295,28.96042118,146.18,112.39,88.75,185.99,182.49,-11.28,48.16,1086.68,91.50,-0.0527,0.0000,-0.1738,1.0000,1 +38.334,41.02114165,28.96097072,169.01,143.90,86.85,186.45,183.20,-10.45,49.08,1081.86,91.24,-0.1250,0.0000,-0.1582,1.0000,1 +38.834,41.02115297,28.96148781,189.00,178.53,85.08,186.84,183.93,-9.56,49.60,1077.50,91.00,0.0000,0.0000,-0.0195,1.0000,1 +39.278,41.02117722,28.96202724,207.65,202.04,nan,187.65,184.79,nan,50.00,1073.15,90.76,0.0000,0.0000,-0.0234,1.0000,1 +39.501,41.02118430,28.96212357,208.46,202.04,83.33,187.71,184.91,-8.84,50.00,1071.87,90.70,0.0000,0.0000,-0.0254,1.0000,1 +40.612,41.02131567,28.96355373,248.59,248.99,78.04,190.41,187.34,-6.44,52.36,1062.01,90.12,0.0000,0.0000,-0.0234,1.0000,1 +41.223,41.02140631,28.96418994,260.51,260.89,75.87,191.72,188.52,-5.45,53.45,1056.99,89.81,0.0000,0.0000,-0.0234,1.0000,1 diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 00000000..6a5697a6 --- /dev/null +++ b/ui/__init__.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +""" +模块:ui +职责:功能页注册入口(生产 / 冒烟含 stub) +依赖:app_frame.AppShell、各 Feature 页 +""" + +from app_frame import AppShell + +from .auth_page import AuthPage +from .hardware_settings_dialog import show_hardware_monitor_settings +from .mission_stub_page import MissionStubPage +from .sim_check_page import SimCheckPage +from .training_assist_page import TrainingAssistPage + + +def register_flow_features(shell: AppShell) -> None: + """注册启动流程轻量页:自检 → 登录。parent=shell 避免无父控件闪成顶层窗。""" + shell.register(SimCheckPage(shell)) + shell.register(AuthPage(shell)) + + +def register_training_feature(shell: AppShell) -> None: + """注册训练助手(含 Qt Charts / 摇杆枚举,较重);已注册则跳过。""" + if shell.registry.get("training_assist") is not None: + return + shell.register(TrainingAssistPage(shell)) + + +def register_production_features(shell: AppShell) -> None: + """注册正式功能页:自检 → 登录 → 训练助手。""" + register_flow_features(shell) + register_training_feature(shell) + + +def register_default_features(shell: AppShell) -> None: + """生产页 + MissionStub(冒烟/扩展示例)。""" + register_production_features(shell) + shell.register(MissionStubPage(shell)) + + +__all__ = [ + "SimCheckPage", + "AuthPage", + "TrainingAssistPage", + "MissionStubPage", + "show_hardware_monitor_settings", + "register_flow_features", + "register_training_feature", + "register_production_features", + "register_default_features", +] diff --git a/ui/auth_page.py b/ui/auth_page.py new file mode 100644 index 00000000..edc41f55 --- /dev/null +++ b/ui/auth_page.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +""" +模块:ui.auth_page +职责:登录流程 Feature 页(转发 AuthPanel) +依赖:app_frame.AuthPanel / Feature +""" + +from PySide2.QtCore import Signal +from PySide2.QtWidgets import QVBoxLayout + +from app_frame import AuthPanel, CapabilitySet, Feature + + +class AuthPage(Feature): + """登录页:对 Controller 暴露凭证/状态 API。""" + + feature_id = "flow.auth" + title = "登录" + + loginRequested = Signal(str, str) + + def __init__(self, parent=None): + super().__init__(parent) + self.panel = AuthPanel(self) + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.panel) + self.panel.loginRequested.connect(self.loginRequested.emit) + + def get_credentials(self): + """返回面板当前用户名/密码。""" + return self.panel.get_credentials() + + def set_status(self, text: str, kind: str = "muted") -> None: + self.panel.set_status(text, kind) + + def set_login_enabled(self, enabled: bool) -> None: + self.panel.set_login_enabled(enabled) + + def clear_password(self) -> None: + self.panel.clear_password() + + def apply_capabilities(self, caps: CapabilitySet) -> None: + """鉴权页无能力裁剪。""" + pass diff --git a/ui/hardware_settings_dialog.py b/ui/hardware_settings_dialog.py new file mode 100644 index 00000000..def4bb07 --- /dev/null +++ b/ui/hardware_settings_dialog.py @@ -0,0 +1,348 @@ +# -*- coding: utf-8 -*- +""" +模块:ui.hardware_settings_dialog +职责:USB 控制器选择、轴映射与校准设定页 +依赖:models.hardware_monitor、app_frame UITheme +""" + +from __future__ import annotations + +from typing import Dict, List, Optional + +from PySide2.QtCore import Qt, QTimer +from PySide2.QtWidgets import ( + QCheckBox, + QComboBox, + QDialog, + QFormLayout, + QHBoxLayout, + QLabel, + QPushButton, + QVBoxLayout, + QWidget, +) + +from app_frame import UITheme, WindowGeom, apply_fixed_window, show_theme_alert +from models.hardware_monitor import ( + AXIS_ROLE_LABELS, + AXIS_ROLES, + HardwareMonitor, + HardwareMonitorConfig, + JoystickInfo, + capture_neutral, + capture_travel_extents, + list_joysticks, + load_config, + physical_axis_label, + save_config, + seed_ranges_from_device, +) +from services.simulator import DATA_POLL_MS + + +class HardwareMonitorSettingsDialog(QDialog): + """设定页:选择控制器、映射四轴、确认中立与最大行程。""" + + _DIALOG_W = 520 + _DIALOG_H = 520 + + def __init__(self, parent=None, config: Optional[HardwareMonitorConfig] = None): + super().__init__(parent) + self.setWindowTitle("手柄设定") + self.setModal(True) + w, h = WindowGeom.scale_dialog_size(self._DIALOG_W, self._DIALOG_H) + UITheme.apply_scale(w, h, self._DIALOG_W, self._DIALOG_H) + apply_fixed_window(self, w, h) + + self._config = config or load_config() + self._devices: List[JoystickInfo] = [] + self._travel_samples: List[List[float]] = [] + self._capturing_travel = False + self._monitor = HardwareMonitor(self._config) + + self._live_timer = QTimer(self) + self._live_timer.setInterval(DATA_POLL_MS) + self._live_timer.timeout.connect(self._on_live_tick) + + root = QVBoxLayout(self) + root.setContentsMargins( + UITheme.scaled(24, 12), + UITheme.scaled(20, 10), + UITheme.scaled(24, 12), + UITheme.scaled(20, 10), + ) + root.setSpacing(UITheme.scaled(10, 6)) + + title = QLabel("USB 游戏控制器") + title.setObjectName("DialogTitle") + title.setAlignment(Qt.AlignCenter) + root.addWidget(title) + + dev_row = QHBoxLayout() + dev_row.setSpacing(UITheme.scaled(8, 6)) + dev_lbl = QLabel("设备") + dev_lbl.setObjectName("Muted") + dev_row.addWidget(dev_lbl) + self._device_combo = QComboBox() + self._device_combo.currentIndexChanged.connect(self._on_device_changed) + dev_row.addWidget(self._device_combo, 1) + btn_refresh = QPushButton("刷新") + btn_refresh.setObjectName("IconSm") + btn_refresh.clicked.connect(self._refresh_devices) + dev_row.addWidget(btn_refresh) + root.addLayout(dev_row) + + self._info_label = QLabel("") + self._info_label.setObjectName("Muted") + self._info_label.setWordWrap(True) + root.addWidget(self._info_label) + + map_box = QWidget() + map_box.setAutoFillBackground(False) + form = QFormLayout(map_box) + form.setContentsMargins(0, 0, 0, 0) + form.setSpacing(UITheme.scaled(8, 6)) + form.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter) + + self._axis_combos: Dict[str, QComboBox] = {} + self._invert_checks: Dict[str, QCheckBox] = {} + self._raw_labels: Dict[str, QLabel] = {} + + for role in AXIS_ROLES: + row = QWidget() + row.setAutoFillBackground(False) + hl = QHBoxLayout(row) + hl.setContentsMargins(0, 0, 0, 0) + hl.setSpacing(UITheme.scaled(8, 6)) + combo = QComboBox() + cal = self._config.axis(role) + self._fill_axis_combo(combo, max(4, cal.index + 1), cal.index) + combo.currentIndexChanged.connect(self._sync_mapping_from_ui) + self._axis_combos[role] = combo + hl.addWidget(combo, 1) + inv = QCheckBox("反转") + inv.setChecked(bool(cal.invert)) + inv.toggled.connect(self._sync_mapping_from_ui) + self._invert_checks[role] = inv + hl.addWidget(inv) + raw_lbl = QLabel("—") + raw_lbl.setObjectName("Muted") + raw_lbl.setMinimumWidth(UITheme.scaled(72, 56)) + self._raw_labels[role] = raw_lbl + hl.addWidget(raw_lbl) + form.addRow(AXIS_ROLE_LABELS.get(role, role), row) + + root.addWidget(map_box) + + self._status = QLabel("请选择设备后完成校准") + self._status.setObjectName("Muted") + self._status.setWordWrap(True) + root.addWidget(self._status) + + cal_row = QHBoxLayout() + cal_row.setSpacing(UITheme.scaled(10, 6)) + self._btn_neutral = QPushButton("确认中立位") + self._btn_neutral.setObjectName("FlatButton") + self._btn_neutral.clicked.connect(self._on_capture_neutral) + cal_row.addWidget(self._btn_neutral) + self._btn_travel = QPushButton("开始采集行程") + self._btn_travel.setObjectName("FlatButton") + self._btn_travel.clicked.connect(self._on_toggle_travel) + cal_row.addWidget(self._btn_travel) + root.addLayout(cal_row) + + hint = QLabel( + "校准:杆置于中立后点「确认中立位」;再点「开始采集行程」," + "把俯仰/滚转/偏航/油门推到最大与最小,然后点「确认最大行程」。" + ) + hint.setObjectName("Muted") + hint.setWordWrap(True) + root.addWidget(hint) + root.addStretch(1) + + btn_row = QHBoxLayout() + btn_row.addStretch(1) + btn_cancel = QPushButton("取消") + btn_cancel.setObjectName("FlatButton") + btn_cancel.clicked.connect(self.reject) + btn_row.addWidget(btn_cancel) + btn_save = QPushButton("保存") + btn_save.setObjectName("FlatButton") + btn_save.clicked.connect(self._on_save) + btn_row.addWidget(btn_save) + btn_row.addStretch(1) + root.addLayout(btn_row) + + self._refresh_devices() + self._live_timer.start() + + def closeEvent(self, event) -> None: + self._live_timer.stop() + super().closeEvent(event) + + @staticmethod + def _fill_axis_combo(combo: QComboBox, axis_count: int, selected: int) -> None: + combo.blockSignals(True) + combo.clear() + n = max(1, int(axis_count)) + for i in range(n): + combo.addItem(f"{physical_axis_label(i)} ({i})", i) + combo.setCurrentIndex(max(0, min(n - 1, int(selected)))) + combo.blockSignals(False) + + def _refresh_devices(self) -> None: + self._devices = list_joysticks(rescan=True) + self._device_combo.blockSignals(True) + self._device_combo.clear() + if not self._devices: + self._device_combo.addItem("(未检测到控制器)", -1) + self._info_label.setText("后端:pygame / SDL") + else: + sel = 0 + for i, d in enumerate(self._devices): + label = f"{d.name} [#{d.index} 轴:{d.num_axes} 键:{d.num_buttons}]" + self._device_combo.addItem(label, d.index) + if self._config.device.matches(d.identity()): + sel = i + self._device_combo.setCurrentIndex(sel) + self._device_combo.blockSignals(False) + self._on_device_changed() + + def _current_device(self) -> Optional[JoystickInfo]: + idx = self._device_combo.currentData() + if idx is None or int(idx) < 0: + return None + for d in self._devices: + if d.index == int(idx): + return d + return None + + def _on_device_changed(self, _i: int = 0) -> None: + dev = self._current_device() + if dev is None: + self._info_label.setText("请连接 USB 游戏控制器后点击刷新(pygame)") + self._monitor.apply_config(self._config) + return + self._config.device = dev.identity() + if not self._config.calibrated: + seed_ranges_from_device(self._config, dev) + self._monitor.apply_config(self._config) + vid = f"{dev.vendor_id:04X}" if dev.vendor_id else "----" + pid = f"{dev.product_id:04X}" if dev.product_id else "----" + self._info_label.setText( + f"pygame | VID {vid} PID {pid} | " + f"校准:{'已完成' if self._config.calibrated else '未完成'}" + ) + self._rebuild_axis_combos(dev.num_axes) + self._apply_mapping_to_combos() + + def _rebuild_axis_combos(self, axis_count: int) -> None: + for role in AXIS_ROLES: + cal = self._config.axis(role) + cal.clamp_index(axis_count) + self._fill_axis_combo(self._axis_combos[role], axis_count, cal.index) + + def _apply_mapping_to_combos(self) -> None: + for role in AXIS_ROLES: + cal = self._config.axis(role) + combo = self._axis_combos[role] + combo.blockSignals(True) + # currentData 可能因轴数变化失效,按 index 对齐 + found = combo.findData(cal.index) + combo.setCurrentIndex(found if found >= 0 else 0) + combo.blockSignals(False) + self._invert_checks[role].blockSignals(True) + self._invert_checks[role].setChecked(bool(cal.invert)) + self._invert_checks[role].blockSignals(False) + + def _sync_mapping_from_ui(self, *_args) -> None: + dev = self._current_device() + axis_count = dev.num_axes if dev is not None else None + for role in AXIS_ROLES: + cal = self._config.axis(role) + data = self._axis_combos[role].currentData() + cal.index = int(data) if data is not None else 0 + cal.invert = bool(self._invert_checks[role].isChecked()) + cal.clamp_index(axis_count) + self._monitor.apply_config(self._config) + + def _on_live_tick(self) -> None: + raw = self._monitor.poll_raw() + if raw is None: + for role in AXIS_ROLES: + self._raw_labels[role].setText("—") + if self._capturing_travel: + self._status.setText("采集中…(设备断开)") + return + for role in AXIS_ROLES: + idx = self._config.axis(role).index + val = raw[idx] if 0 <= idx < len(raw) else float("nan") + self._raw_labels[role].setText(f"{val:+.3f}") + if self._capturing_travel: + self._travel_samples.append(list(raw)) + self._status.setText( + f"采集中…已采样 {len(self._travel_samples)} 帧,推满各轴后点「确认最大行程」" + ) + + def _on_capture_neutral(self) -> None: + raw = self._monitor.poll_raw() + if raw is None: + show_theme_alert(self, message="未读取到控制器数据", show_title=False) + return + self._sync_mapping_from_ui() + capture_neutral(self._config, raw) + self._monitor.apply_config(self._config) + self._status.setText("中立位已确认") + + def _on_toggle_travel(self) -> None: + if not self._capturing_travel: + raw = self._monitor.poll_raw() + if raw is None: + show_theme_alert(self, message="未读取到控制器数据", show_title=False) + return + self._sync_mapping_from_ui() + self._travel_samples = [list(raw)] + self._capturing_travel = True + self._btn_travel.setText("确认最大行程") + self._status.setText("采集中…请将各轴推到最大与最小位置") + return + if len(self._travel_samples) < 5: + show_theme_alert(self, message="采样不足,请继续推动各轴", show_title=False) + return + capture_travel_extents(self._config, self._travel_samples) + self._capturing_travel = False + self._travel_samples = [] + self._btn_travel.setText("开始采集行程") + self._monitor.apply_config(self._config) + self._status.setText("最大行程已确认,可保存") + dev = self._current_device() + if dev is not None: + vid = f"{dev.vendor_id:04X}" if dev.vendor_id else "----" + pid = f"{dev.product_id:04X}" if dev.product_id else "----" + self._info_label.setText( + f"pygame | VID {vid} PID {pid} | 校准:已完成" + ) + + def _on_save(self) -> None: + dev = self._current_device() + if dev is None: + show_theme_alert(self, message="请先选择控制器", show_title=False) + return + self._sync_mapping_from_ui() + self._config.device = dev.identity() + if not self._config.calibrated: + show_theme_alert( + self, + message="尚未完成行程校准,仍可保存映射;监控前建议完成校准。", + show_title=False, + ) + save_config(self._config) + self.accept() + + +def show_hardware_monitor_settings(parent=None) -> Optional[HardwareMonitorConfig]: + """打开手柄设定页;保存成功返回配置,取消返回 None。""" + dlg = HardwareMonitorSettingsDialog(parent) + if dlg.exec_() == QDialog.Accepted: + return load_config() + return None diff --git a/ui/mission_stub_page.py b/ui/mission_stub_page.py new file mode 100644 index 00000000..bfef52ff --- /dev/null +++ b/ui/mission_stub_page.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +""" +模块:ui.mission_stub_page +职责:第二功能页 stub,证明只改 ui/ 即可扩展 +依赖:app_frame.Feature / ActionBar / StatusBar +""" + +from PySide2.QtCore import Qt, Signal +from PySide2.QtWidgets import QLabel, QVBoxLayout + +from app_frame import ActionBar, CapabilitySet, Feature, StatusBar, UITheme + + +class MissionStubPage(Feature): + """布局与训练助手完全不同的扩展示例页。""" + + feature_id = "mission_stub" + title = "任务 Stub" + + primaryClicked = Signal() + secondaryClicked = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + layout = QVBoxLayout(self) + layout.setContentsMargins(32, 32, 32, 32) + layout.setSpacing(16) + + title = QLabel("飞行任务 B(示例)") + title.setObjectName("Title") + title.setAlignment(Qt.AlignCenter) + + hint = QLabel("此页仅作框架扩展示例:布局与训练助手完全不同,未改 app_frame.py。") + hint.setObjectName("Subtitle") + hint.setWordWrap(True) + hint.setAlignment(Qt.AlignCenter) + + self.actions = ActionBar( + button_defs=[ + {"id": "primary", "text": "开始任务"}, + {"id": "secondary", "text": "返回训练助手"}, + ] + ) + self.actions.clicked.connect(self._on_action) + self.status = StatusBar("stub 就绪") + + layout.addStretch(1) + layout.addWidget(title) + layout.addWidget(hint) + layout.addSpacing(UITheme.scaled(20, 10)) + layout.addWidget(self.actions) + layout.addWidget(self.status) + layout.addStretch(2) + + def _on_action(self, button_id: str) -> None: + if button_id == "primary": + self.primaryClicked.emit() + self.status.set_status("已点击开始任务(无业务)", "ok") + elif button_id == "secondary": + self.secondaryClicked.emit() + + def apply_capabilities(self, caps: CapabilitySet) -> None: + """Stub 页无能力裁剪。""" + pass + + def set_status(self, text: str, kind: str = "muted") -> None: + self.status.set_status(text, kind) diff --git a/ui/sim_check_page.py b/ui/sim_check_page.py new file mode 100644 index 00000000..fd186a25 --- /dev/null +++ b/ui/sim_check_page.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +""" +模块:ui.sim_check_page +职责:开机自检流程 Feature 页(转发 SimCheckPanel) +依赖:app_frame.SimCheckPanel / Feature +""" + +from PySide2.QtCore import Signal +from PySide2.QtWidgets import QVBoxLayout + +from app_frame import CapabilitySet, Feature, SimCheckPanel + + +class SimCheckPage(Feature): + """自检页:信号灯项与底部动作按钮。""" + + feature_id = "flow.sim_check" + title = "" + + actionClicked = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self.panel = SimCheckPanel(self) + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.panel) + self.panel.actionClicked.connect(self.actionClicked.emit) + + def set_subtitle(self, text: str) -> None: + self.panel.set_subtitle(text) + + def set_item(self, index: int, state: str, detail: str) -> None: + self.panel.set_item(index, state, detail) + + def set_action(self, text: str, enabled: bool = True) -> None: + self.panel.set_action(text, enabled) + + def set_busy(self, busy: bool) -> None: + self.panel.set_busy(busy) + + def apply_capabilities(self, caps: CapabilitySet) -> None: + """自检页无能力裁剪。""" + pass diff --git a/ui/training_assist_page.py b/ui/training_assist_page.py new file mode 100644 index 00000000..e5117d99 --- /dev/null +++ b/ui/training_assist_page.py @@ -0,0 +1,343 @@ +# -*- coding: utf-8 -*- +""" +模块:ui.training_assist_page +职责:训练助手主功能页(指标 / 圈板 / 图表 / 操作栏) +依赖:app_frame 积木、charts.TrendChart、CompareStylePanel +""" + +from typing import Optional + +from PySide2.QtCore import QEvent, QObject, Qt, Signal +from PySide2.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QWidget + +from app_frame import ( + CAP_CHART, + CAP_CHART_SAVE, + CAP_COLD_CABIN, + CAP_LAPS, + CAP_METRICS, + CAP_SIM_RATE, + ActionBar, + CapabilitySet, + CardFrame, + Feature, + LapBoard, + MetricStrip, + StatusBar, + UITheme, +) +from charts import TrendChart +from charts.widgets.compare_style_panel import CompareStylePanel + + +_START_MONITOR_NEED_MISSION = "请先选择飞行任务" +# 操作栏 FlatButton 固定尺寸(设计分辨率) +_ACTION_BTN_W = 108 +_ACTION_BTN_H = 36 +# 「保存当前数据」文案更长,单独固定宽度 +_MONITOR_BTN_W = 148 + + +class _DisabledForbiddenCursorFilter(QObject): + """禁用控件上悬停显示禁止光标。""" + + def eventFilter(self, watched, event): + if event.type() == QEvent.Enter and not watched.isEnabled(): + watched.setCursor(Qt.ForbiddenCursor) + elif event.type() == QEvent.Leave: + watched.unsetCursor() + return False + + +class TrainingAssistPage(Feature): + """Controller 面向的训练页 Feature API。""" + + feature_id = "training_assist" + title = "模拟飞行训练助手" + + startMonitorClicked = Signal() + rateClicked = Signal() + monitorClicked = Signal() + speedSourceChanged = Signal(str) + + def __init__(self, parent=None): + super().__init__(parent) + self._monitor_mode: Optional[str] = None + self._mission_ready = False + self._start_monitor_cursor_filter = _DisabledForbiddenCursorFilter(self) + pad = UITheme.scaled(20, 8) + gap = UITheme.scaled(14, 6) + layout = QVBoxLayout(self) + layout.setContentsMargins(pad, pad, pad, pad) + layout.setSpacing(gap) + + self.metrics = MetricStrip() + self.laps = LapBoard() + + chart_card = CardFrame() + chart_l = QVBoxLayout(chart_card) + chart_l.setContentsMargins( + UITheme.scaled(12, 6), + UITheme.scaled(12, 6), + UITheme.scaled(12, 6), + UITheme.scaled(12, 6), + ) + self.chart = TrendChart() + self.chart.set_save_caption_provider(self._chart_save_caption) + self.chart.compareActiveChanged.connect(self._on_compare_active_changed) + self.chart.compareHintChanged.connect(self._on_compare_hint) + self.chart.speedSourceChanged.connect(self.speedSourceChanged.emit) + chart_l.addWidget(self.chart) + + btn_card = CardFrame() + btn_l = QHBoxLayout(btn_card) + btn_l.setContentsMargins( + UITheme.scaled(8, 4), + UITheme.scaled(8, 4), + UITheme.scaled(8, 4), + UITheme.scaled(8, 4), + ) + btn_l.setSpacing(UITheme.scaled(10, 6)) + self.actions = ActionBar( + button_defs=[ + {"id": "start_monitor", "text": "开始监测"}, + {"id": "sim_rate", "text": "开启倍速"}, + {"id": "chart_save", "text": "保存图表"}, + {"id": "chart_compare", "text": "对比数据"}, + ], + alignment=Qt.AlignLeft, + ) + self.actions.clicked.connect(self._on_action) + btn_w = UITheme.scaled(_ACTION_BTN_W, 90) + btn_h = UITheme.scaled(_ACTION_BTN_H, 30) + for bid in ("start_monitor", "sim_rate", "chart_save", "chart_compare"): + btn = self.actions.button(bid) + if btn is not None: + btn.setFixedSize(btn_w, btn_h) + btn_l.addWidget(self.actions, 0, Qt.AlignLeft | Qt.AlignVCenter) + + self.compare_panel = CompareStylePanel() + self.compare_panel.styleChanged.connect(self.chart.apply_compare_style) + btn_l.addWidget(self.compare_panel, 1, Qt.AlignVCenter) + + btn_l.addStretch(1) + # 右侧固定占位:隐藏「保存当前数据」时仍保留宽度,避免中间按钮位移 + self._monitor_slot = QWidget() + self._monitor_slot.setObjectName("MonitorSlot") + self._monitor_slot.setAttribute(Qt.WA_StyledBackground, True) + self._monitor_slot.setAutoFillBackground(False) + slot_l = QHBoxLayout(self._monitor_slot) + slot_l.setContentsMargins(0, 0, 0, 0) + slot_l.setSpacing(0) + self.monitor_btn = QPushButton("保存当前数据") + self.monitor_btn.setObjectName("MonitorButton") + self.monitor_btn.setProperty("monitorKind", "save") + mon_w = UITheme.scaled(_MONITOR_BTN_W, 124) + self.monitor_btn.setFixedSize(mon_w, btn_h) + self.monitor_btn.setVisible(False) + self.monitor_btn.clicked.connect(self.monitorClicked.emit) + slot_l.addWidget(self.monitor_btn, 0, Qt.AlignRight | Qt.AlignVCenter) + self._monitor_slot.setFixedSize(mon_w, btn_h) + btn_l.addWidget(self._monitor_slot, 0, Qt.AlignRight | Qt.AlignVCenter) + + self.status = StatusBar("正在检测模拟器...") + + layout.addWidget(self.metrics) + layout.addWidget(self.laps) + layout.addWidget(chart_card, 1) + layout.addWidget(btn_card) + layout.addWidget(self.status) + + self._chart_host = chart_card + self._apply_start_monitor_style("start") + start_btn = self.actions.button("start_monitor") + if start_btn is not None: + start_btn.installEventFilter(self._start_monitor_cursor_filter) + self._apply_start_monitor_availability() + + def _on_action(self, button_id: str) -> None: + if button_id == "start_monitor": + if self._monitor_mode in ("stop", "pending"): + self.monitorClicked.emit() + elif self._mission_ready: + self.startMonitorClicked.emit() + elif button_id == "sim_rate": + self.rateClicked.emit() + elif button_id == "chart_save": + self.chart.save_chart() + elif button_id == "chart_compare": + if self.chart.is_compare_active(): + self.chart.clear_compare() + else: + self.chart.load_compare_dialog() + + def _on_compare_active_changed(self, active: bool, label: str = "") -> None: + self.actions.set_button( + "chart_compare", + text="关闭对比" if active else "对比数据", + ) + if active: + self.compare_panel.set_style(self.chart.compare_style()) + self.compare_panel.set_label(label or self.chart.compare_label()) + self.compare_panel.setVisible(True) + else: + self.compare_panel.setVisible(False) + self.compare_panel.set_label("") + + def _on_compare_hint(self, text: str) -> None: + if text: + self.set_status(text, "warn") + + def _apply_start_monitor_style(self, mode: str) -> None: + btn = self.actions.button("start_monitor") + if btn is None: + return + if mode == "stop": + btn.setText("停止监测") + btn.setObjectName("MonitorButton") + btn.setProperty("monitorKind", "stop") + else: + btn.setText("开始监测") + btn.setObjectName("FlatButton") + btn.setProperty("monitorKind", "") + btn.style().unpolish(btn) + btn.style().polish(btn) + + def _chart_save_caption(self): + rows = [] + for i, (_name, tm) in enumerate(self.laps._lap_labels): + rows.append((f"LAP {i + 1:02d}", tm.text())) + return rows, self.laps.total_label.text() + + def apply_capabilities(self, caps: CapabilitySet) -> None: + """按 CapabilitySet 显隐指标/圈板/图表与相关按钮。""" + self.metrics.setVisible(caps.has(CAP_METRICS)) + self.laps.setVisible(caps.has(CAP_LAPS)) + self._chart_host.setVisible(caps.has(CAP_CHART)) + can_save = caps.has(CAP_CHART_SAVE) + self.chart.set_save_enabled(can_save) + self.actions.set_button_visible("chart_save", can_save) + self.actions.set_button_visible("chart_compare", can_save) + self.actions.set_button_visible("start_monitor", caps.has(CAP_COLD_CABIN)) + self.actions.set_button_visible("sim_rate", caps.has(CAP_SIM_RATE)) + + def set_cht(self, value) -> None: + """更新 CHT 读数。""" + self.metrics.set_cht(value) + + def set_torque(self, value) -> None: + """更新扭矩读数。""" + self.metrics.set_torque(value) + + def set_health(self, pct) -> None: + """更新健康度读数。""" + self.metrics.set_health(pct) + + def set_laps( + self, + times: list, + total=None, + lap_penalties: Optional[list] = None, + total_penalty=None, + ) -> None: + """更新圈时板。""" + self.laps.set_laps(times, total, lap_penalties=lap_penalties, total_penalty=total_penalty) + + def set_session_mode(self, mode: str = "freeflight") -> None: + """更新会话模式标签。""" + self.laps.set_session_mode(mode) + + def set_mission_title(self, title: Optional[str] = None) -> None: + """更新任务名称标签。""" + self.laps.set_mission_title(title) + + def set_status(self, text: str, kind: str = "muted") -> None: + """更新底部运行状态文案。""" + self.status.set_status(text, kind) + + def set_rate_status(self, text: str, kind: str = "muted") -> None: + """更新倍速相关状态文案。""" + self.status.set_rate_status(text, kind) + + def set_rate_button(self, text: str) -> None: + """更新倍速按钮文案。""" + self.actions.set_button("sim_rate", text=text) + + def set_start_monitor_mission_ready(self, ready: bool) -> None: + """任务是否已选好,决定「开始监测」可点。""" + self._mission_ready = bool(ready) + self._apply_start_monitor_availability() + + def _apply_start_monitor_availability(self) -> None: + btn = self.actions.button("start_monitor") + if btn is None: + return + if self._monitor_mode in ("stop", "pending"): + btn.setEnabled(True) + btn.setToolTip("") + return + enabled = self._mission_ready + btn.setEnabled(enabled) + btn.setToolTip("" if enabled else _START_MONITOR_NEED_MISSION) + + def set_monitor_button(self, mode: Optional[str] = None) -> None: + """ + 说明:切换监测按钮外观 + 参数: + mode — "stop"|"pending"|"save"|None + """ + self._monitor_mode = mode + if mode in ("stop", "pending"): + self._apply_start_monitor_style("stop") + self.monitor_btn.setVisible(False) + elif mode == "save": + self._apply_start_monitor_style("start") + self.monitor_btn.setVisible(True) + self.monitor_btn.setText("保存当前数据") + self.monitor_btn.setProperty("monitorKind", "save") + else: + self._apply_start_monitor_style("start") + self.monitor_btn.setVisible(False) + self._apply_start_monitor_availability() + self.monitor_btn.style().unpolish(self.monitor_btn) + self.monitor_btn.style().polish(self.monitor_btn) + + def update_chart(self, history) -> None: + """刷新趋势图缓冲。""" + self.chart.update(history) + + def set_track_points(self, points) -> None: + """写入当前会话航迹点。""" + self.chart.set_track_points(points) + + def reset_track_filters(self) -> None: + """复位航迹图圈过滤复选。""" + self.chart.reset_track_filters() + + def reset_speed_band_filters(self) -> None: + """兼容旧名:复位航迹图圈过滤。""" + self.reset_track_filters() + + def speed_band_source(self) -> str: + """航迹着色速度源:airspeed | groundspeed。""" + return self.chart.speed_band_source() + + def set_stick_axes(self, pitch=0.0, roll=0.0, yaw=0.0, throttle=0.0, connected=True) -> None: + """更新操纵输入附图。""" + self.chart.set_stick_axes(pitch, roll, yaw, throttle, connected) + + def set_stick(self, axes) -> None: + """写入 StickAxes 到操纵输入附图。""" + self.chart.set_stick(axes) + + def resume_live_follow(self) -> None: + """恢复时间轴跟随与实时摇杆刷新(重新开始监测时调用)。""" + self.chart.resume_live_follow() + + def add_lap_marker(self, ts=None, lap_number=None) -> None: + """在趋势图添加圈完成竖线。""" + self.chart.add_lap_marker(ts, lap_number) + + def clear_lap_markers(self) -> None: + """清除趋势图圈标。""" + self.chart.clear_lap_markers() diff --git "a/\350\210\252\350\277\271\345\233\276.html" "b/\350\210\252\350\277\271\345\233\276.html" new file mode 100644 index 00000000..efc1bfa7 --- /dev/null +++ "b/\350\210\252\350\277\271\345\233\276.html" @@ -0,0 +1,696 @@ + + + + + + 赛道航迹速度图 - Extra 300S + + + + +
+

🏁 赛道航迹速度图

+

加载CSV → 比例正确的赛道图 + 速度着色

+ + +
+
+ 📂 +
点击或拖拽上传 CSV
+ +
未加载
+
+
+ + +
+ + + +
+ + + + + +
+ +
Extra 300S · 速度着色
+
+ + + +
+ + + + + \ No newline at end of file