增加web_api
This commit is contained in:
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
1734
API_DOCUMENTATION.md
Normal file
1734
API_DOCUMENTATION.md
Normal file
File diff suppressed because it is too large
Load Diff
782
API_ENDPOINTS.md
Normal file
782
API_ENDPOINTS.md
Normal file
@ -0,0 +1,782 @@
|
||||
# GasFlux Web API 端点参考
|
||||
|
||||
## 概述
|
||||
|
||||
GasFlux Web API 是一个基于 Flask 的 RESTful API,用于上传数据文件、执行气体通量分析处理,并下载处理结果。
|
||||
|
||||
**基础信息:**
|
||||
- **基础 URL**: `http://localhost:5000`
|
||||
- **认证**: 无需认证
|
||||
- **数据格式**: JSON
|
||||
- **文件大小限制**: 100MB
|
||||
- **支持的文件类型**: `.xlsx`, `.xls`, `.yaml`, `.yml`
|
||||
|
||||
---
|
||||
|
||||
## 🔍 监控和健康检查
|
||||
|
||||
### 1. 获取健康状态
|
||||
**端点**: `GET /health`
|
||||
|
||||
**描述**: 获取 API 的健康状态、系统信息和性能指标。
|
||||
|
||||
**响应示例** (200):
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "健康检查完成",
|
||||
"data": {
|
||||
"status": "healthy",
|
||||
"version": "1.0.0",
|
||||
"timestamp": 1705257600.123,
|
||||
"uptime": "2h 30m 15s",
|
||||
"storage": {
|
||||
"uploads_writable": true,
|
||||
"outputs_writable": true
|
||||
},
|
||||
"tasks": {
|
||||
"active_count": 2,
|
||||
"total_tracked": 15,
|
||||
"total_processed": 13,
|
||||
"success_rate_percent": 92.31
|
||||
},
|
||||
"performance": {
|
||||
"requests_per_second": 0.08,
|
||||
"avg_response_time_ms": 234.56,
|
||||
"error_rate_percent": 1.5
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例** (503):
|
||||
```json
|
||||
{
|
||||
"code": 503,
|
||||
"message": "服务不可用",
|
||||
"data": {
|
||||
"status": "degraded",
|
||||
"version": "1.0.0",
|
||||
"timestamp": 1705257600.123,
|
||||
"uptime": "1h 30m 45s",
|
||||
"storage": {
|
||||
"uploads_writable": true,
|
||||
"outputs_writable": true
|
||||
},
|
||||
"tasks": {
|
||||
"active_count": 0,
|
||||
"total_tracked": 5,
|
||||
"total_processed": 3,
|
||||
"success_rate_percent": 60.0
|
||||
},
|
||||
"performance": {
|
||||
"requests_per_second": 0.12,
|
||||
"avg_response_time_ms": 145.67,
|
||||
"error_rate_percent": 50.0
|
||||
},
|
||||
"issues": [
|
||||
"错误率过高 (50.0%)",
|
||||
"活跃任务数量过多 (25)"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**状态码**:
|
||||
- `200`: API 健康
|
||||
- `503`: API 不健康
|
||||
|
||||
---
|
||||
|
||||
### 2. 获取系统统计信息
|
||||
**端点**: `GET /stats`
|
||||
|
||||
**描述**: 获取详细的 API 统计信息、性能指标和系统监控数据。
|
||||
|
||||
**响应示例** (200):
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "统计信息获取成功",
|
||||
"data": {
|
||||
"summary": {
|
||||
"uptime_seconds": 3600.5,
|
||||
"uptime_formatted": "1h 0m 0s",
|
||||
"requests_total": 150,
|
||||
"requests_per_second": 0.04,
|
||||
"error_rate_percent": 2.0,
|
||||
"active_tasks": 1
|
||||
},
|
||||
"requests": {
|
||||
"by_method": {
|
||||
"GET": 120,
|
||||
"POST": 30
|
||||
},
|
||||
"by_status": {
|
||||
"200": 145,
|
||||
"400": 3,
|
||||
"500": 2
|
||||
},
|
||||
"top_endpoints": {
|
||||
"/task/abc123": 45,
|
||||
"/health": 30,
|
||||
"/": 25
|
||||
}
|
||||
},
|
||||
"tasks": {
|
||||
"total_created": 25,
|
||||
"total_completed": 20,
|
||||
"total_failed": 2,
|
||||
"success_rate_percent": 90.91,
|
||||
"by_status": {
|
||||
"pending": 1,
|
||||
"processing": 1,
|
||||
"completed": 20,
|
||||
"failed": 2
|
||||
}
|
||||
},
|
||||
"performance": {
|
||||
"avg_response_time_ms": 245.67,
|
||||
"max_response_time_ms": 1250.34,
|
||||
"min_response_time_ms": 12.45
|
||||
},
|
||||
"system": {
|
||||
"memory_usage_percent": 45.2,
|
||||
"memory_used_gb": 7.3,
|
||||
"memory_total_gb": 16.0,
|
||||
"disk_usage_percent": 23.1,
|
||||
"disk_used_gb": 46.8,
|
||||
"disk_total_gb": 203.2
|
||||
},
|
||||
"recent_tasks": [
|
||||
{
|
||||
"task_id": "abc123-def456",
|
||||
"status": "completed",
|
||||
"age_seconds": 45.2,
|
||||
"message": "处理完成成功"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**状态码**:
|
||||
- `200`: 成功
|
||||
|
||||
---
|
||||
|
||||
### 3. 重置统计信息
|
||||
**端点**: `POST /stats/reset`
|
||||
|
||||
**描述**: 重置所有 API 统计数据(管理员功能)。
|
||||
|
||||
**响应示例** (200):
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "统计信息重置成功",
|
||||
"data": {
|
||||
"timestamp": 1705257600.123
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**状态码**:
|
||||
- `200`: 成功
|
||||
|
||||
---
|
||||
|
||||
### 4. 获取配置信息
|
||||
**端点**: `GET /config`
|
||||
|
||||
**描述**: 获取当前应用配置信息和支持的环境变量。
|
||||
|
||||
**响应示例** (200):
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "配置信息获取成功",
|
||||
"data": {
|
||||
"configuration": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 5000,
|
||||
"debug": false,
|
||||
"base_dir": "/app",
|
||||
"upload_folder": "/app/web_api_data/uploads",
|
||||
"output_folder": "/app/web_api_data/outputs",
|
||||
"max_content_length": 104857600,
|
||||
"log_level": "INFO",
|
||||
"log_file": "logs/gasflux_api.log",
|
||||
"cors_origins": ["*"],
|
||||
"task_cleanup_interval": 3600,
|
||||
"max_task_age": 86400,
|
||||
"threads": 8,
|
||||
"connection_limit": 100,
|
||||
"channel_timeout": 300
|
||||
},
|
||||
"environment_variables": {
|
||||
"supported": [
|
||||
"GASFLUX_HOST", "GASFLUX_PORT", "GASFLUX_DEBUG",
|
||||
"GASFLUX_UPLOAD_FOLDER", "GASFLUX_OUTPUT_FOLDER",
|
||||
"GASFLUX_MAX_CONTENT_LENGTH", "GASFLUX_LOG_LEVEL",
|
||||
"GASFLUX_LOG_FILE", "GASFLUX_CORS_ORIGINS",
|
||||
"GASFLUX_TASK_CLEANUP_INTERVAL", "GASFLUX_MAX_TASK_AGE",
|
||||
"GASFLUX_THREADS", "GASFLUX_CONNECTION_LIMIT",
|
||||
"GASFLUX_CHANNEL_TIMEOUT"
|
||||
],
|
||||
"current_values": {
|
||||
"GASFLUX_HOST": "0.0.0.0",
|
||||
"GASFLUX_PORT": "5000",
|
||||
"GASFLUX_DEBUG": "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**状态码**:
|
||||
- `200`: 成功
|
||||
|
||||
---
|
||||
|
||||
## 📤 文件上传和管理
|
||||
|
||||
### 5. 文件上传和处理
|
||||
**端点**: `POST /upload`
|
||||
|
||||
**描述**: 上传数据文件并启动异步处理任务。
|
||||
|
||||
**请求参数** (multipart/form-data):
|
||||
- `file` (必需): 数据文件 (.xlsx 或 .xls 格式)
|
||||
- `config` (可选): 配置文件 (.yaml 或 .yml 格式)
|
||||
|
||||
**响应示例** (202):
|
||||
```json
|
||||
{
|
||||
"code": 202,
|
||||
"message": "任务已接受并加入处理队列",
|
||||
"data": {
|
||||
"status": "accepted",
|
||||
"job_id": "abc123-def456-ghi789",
|
||||
"task_status_url": "/task/abc123-def456-ghi789"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应示例** (400):
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "无效的数据文件类型。只允许 .xlsx 和 .xls 格式。",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应示例** (413):
|
||||
```json
|
||||
{
|
||||
"code": 413,
|
||||
"message": "文件过大。最大尺寸为 100MB。",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
**状态码**:
|
||||
- `202`: 任务已接受
|
||||
- `400`: 请求参数错误
|
||||
- `413`: 文件过大
|
||||
|
||||
---
|
||||
|
||||
## 📊 任务管理和监控
|
||||
|
||||
### 6. 分页查询任务列表
|
||||
**端点**: `GET /tasks`
|
||||
|
||||
**描述**: 分页查询所有任务,支持按状态过滤、排序和分页。返回体为前端友好的瘦身结构:仅包含任务状态信息;当任务已完成时会包含 `downloads` 直达下载链接。
|
||||
|
||||
**查询参数**:
|
||||
- `page` (可选): 页码 (默认: 1)
|
||||
- `page_size` (可选): 每页任务数量 (默认: 20, 最大: 100)
|
||||
- `sort_by` (可选): 排序字段 ('created_at', 'updated_at', 'status') (默认: 'updated_at')
|
||||
- `sort_order` (可选): 排序顺序 ('asc', 'desc') (默认: 'desc')
|
||||
- `status` (可选): 按任务状态过滤,支持多个状态用逗号分隔
|
||||
|
||||
**响应示例** (200):
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "任务列表查询成功",
|
||||
"data": {
|
||||
"tasks": [
|
||||
{
|
||||
"task_id": "abc123-def456-ghi789",
|
||||
"status": "completed",
|
||||
"message": "Processing completed successfully",
|
||||
"updated_at": 1705257700.456,
|
||||
"downloads": {
|
||||
"data_xlsx": "/download/abc123-def456-ghi789/08_34_01_5m.processed_data.xlsx",
|
||||
"report_ch4": "/download/abc123-def456-ghi789/08_34_01_5m.processed_ch4_report.html",
|
||||
"report_co2": "/download/abc123-def456-ghi789/08_34_01_5m.processed_co2_report.html",
|
||||
"config": "/download/abc123-def456-ghi789/08_34_01_5m.processed_config.yaml",
|
||||
"metadata": "/download/abc123-def456-ghi789/08_34_01_5m.processed_output_vars.json"
|
||||
}
|
||||
},
|
||||
{
|
||||
"task_id": "xyz789-abc123-def456",
|
||||
"status": "processing",
|
||||
"message": "正在进行数据分析...",
|
||||
"updated_at": 1705257650.321
|
||||
}
|
||||
],
|
||||
"total": 25,
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"total_pages": 2,
|
||||
"has_next": true,
|
||||
"has_prev": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**状态码**:
|
||||
- `200`: 成功
|
||||
- `400`: 参数无效
|
||||
|
||||
---
|
||||
|
||||
### 7. 获取任务池统计信息
|
||||
**端点**: `GET /tasks/stats`
|
||||
|
||||
**描述**: 获取任务池的统计信息,包括各状态任务数量、活跃任务数等。
|
||||
|
||||
**响应示例** (200):
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "任务池统计信息查询成功",
|
||||
"data": {
|
||||
"total_tasks": 25,
|
||||
"status_counts": {
|
||||
"pending": 3,
|
||||
"processing": 2,
|
||||
"completed": 18,
|
||||
"failed": 2
|
||||
},
|
||||
"active_tasks": 2,
|
||||
"queued_tasks": 3,
|
||||
"completed_tasks": 18,
|
||||
"failed_tasks": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**状态码**:
|
||||
- `200`: 成功
|
||||
|
||||
---
|
||||
|
||||
### 8. 获取活跃任务列表
|
||||
**端点**: `GET /tasks/active`
|
||||
|
||||
**描述**: 获取当前正在处理的任务列表(瘦身结构)。当列表中包含已完成任务时会附带 `downloads` 直达下载链接。
|
||||
|
||||
**响应示例** (200):
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "活跃任务查询成功",
|
||||
"data": {
|
||||
"active_tasks": [
|
||||
{
|
||||
"task_id": "abc123-def456-ghi789",
|
||||
"status": "processing",
|
||||
"message": "正在生成报告...",
|
||||
"updated_at": 1705257650.321
|
||||
},
|
||||
{
|
||||
"task_id": "completed-task-123",
|
||||
"status": "completed",
|
||||
"message": "Processing completed successfully",
|
||||
"updated_at": 1705257550.456,
|
||||
"downloads": {
|
||||
"data_xlsx": "/download/completed-task-123/processed_data.xlsx",
|
||||
"report_html": "/download/completed-task-123/data_report.html"
|
||||
}
|
||||
}
|
||||
],
|
||||
"count": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**状态码**:
|
||||
- `200`: 成功
|
||||
|
||||
---
|
||||
|
||||
### 9. 获取队列任务列表
|
||||
**端点**: `GET /tasks/queue`
|
||||
|
||||
**描述**: 获取等待处理的任务队列(瘦身结构)。当列表中包含已完成任务时会附带 `downloads` 直达下载链接。
|
||||
|
||||
**响应示例** (200):
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "队列任务查询成功",
|
||||
"data": {
|
||||
"queued_tasks": [
|
||||
{
|
||||
"task_id": "xyz789-abc123-def456",
|
||||
"status": "pending",
|
||||
"message": "等待处理",
|
||||
"updated_at": 1705257400.123
|
||||
},
|
||||
{
|
||||
"task_id": "completed-queued-task",
|
||||
"status": "completed",
|
||||
"message": "Processing completed successfully",
|
||||
"updated_at": 1705257450.321,
|
||||
"downloads": {
|
||||
"data_xlsx": "/download/completed-queued-task/processed_data.xlsx",
|
||||
"report_html": "/download/completed-queued-task/analysis_report.html"
|
||||
}
|
||||
}
|
||||
],
|
||||
"count": 2,
|
||||
"queue_position_info": "任务按创建时间排序,较早的任务优先处理"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**状态码**:
|
||||
- `200`: 成功
|
||||
|
||||
---
|
||||
|
||||
### 10. 查询任务状态
|
||||
**端点**: `GET /task/{task_id}`
|
||||
|
||||
**描述**: 查询异步处理任务的当前状态和进度信息。
|
||||
|
||||
**路径参数**:
|
||||
- `task_id`: 任务 ID (UUID 格式)
|
||||
|
||||
**响应示例** - 处理中 (200):
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "任务查询成功",
|
||||
"data": {
|
||||
"task_id": "abc123-def456-ghi789",
|
||||
"status": "processing",
|
||||
"message": "GasFlux 分析完成,正在生成报告...",
|
||||
"updated_at": 1705257600.123
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例** - 处理完成 (200):
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "任务查询成功",
|
||||
"data": {
|
||||
"task_id": "abc123-def456-ghi789",
|
||||
"status": "completed",
|
||||
"message": "处理完成成功",
|
||||
"updated_at": 1705257600.123,
|
||||
"results": [
|
||||
{
|
||||
"name": "08_34_01_5m.processed_ch4_report.html",
|
||||
"rel_path": "abc123-def456-ghi789/08_34_01_5m.processed_ch4_report.html",
|
||||
"download_url": "/download/abc123-def456-ghi789/08_34_01_5m.processed_ch4_report.html",
|
||||
"size": 245760,
|
||||
"type": "report"
|
||||
},
|
||||
{
|
||||
"name": "08_34_01_5m.processed_data.xlsx",
|
||||
"rel_path": "abc123-def456-ghi789/08_34_01_5m.processed_data.xlsx",
|
||||
"download_url": "/download/abc123-def456-ghi789/08_34_01_5m.processed_data.xlsx",
|
||||
"size": 153600,
|
||||
"type": "data"
|
||||
},
|
||||
{
|
||||
"name": "08_34_01_5m.processed_config.yaml",
|
||||
"rel_path": "abc123-def456-ghi789/08_34_01_5m.processed_config.yaml",
|
||||
"download_url": "/download/abc123-def456-ghi789/08_34_01_5m.processed_config.yaml",
|
||||
"size": 2048,
|
||||
"type": "config"
|
||||
},
|
||||
{
|
||||
"name": "08_34_01_5m.processed_output_vars.json",
|
||||
"rel_path": "abc123-def456-ghi789/08_34_01_5m.processed_output_vars.json",
|
||||
"download_url": "/download/abc123-def456-ghi789/08_34_01_5m.processed_output_vars.json",
|
||||
"size": 4096,
|
||||
"type": "metadata"
|
||||
}
|
||||
],
|
||||
"downloads": {
|
||||
"data_xlsx": "/download/abc123-def456-ghi789/08_34_01_5m.processed_data.xlsx",
|
||||
"report_ch4": "/download/abc123-def456-ghi789/08_34_01_5m.processed_ch4_report.html",
|
||||
"report_co2": "/download/abc123-def456-ghi789/08_34_01_5m.processed_co2_report.html",
|
||||
"config": "/download/abc123-def456-ghi789/08_34_01_5m.processed_config.yaml",
|
||||
"metadata": "/download/abc123-def456-ghi789/08_34_01_5m.processed_output_vars.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例** - 任务不存在 (404):
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "任务未找到",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
**状态码**:
|
||||
- `200`: 成功
|
||||
- `404`: 任务不存在
|
||||
|
||||
---
|
||||
|
||||
### 11. 更新任务状态
|
||||
**端点**: `PUT /task/{task_id}`
|
||||
|
||||
**描述**: 更新任务的状态、信息或优先级。
|
||||
|
||||
**路径参数**:
|
||||
- `task_id`: 任务 ID (UUID 格式)
|
||||
|
||||
**请求参数** (JSON):
|
||||
- `status` (可选): 新的任务状态
|
||||
- `message` (可选): 状态消息或错误描述
|
||||
- `priority` (可选): 任务优先级 (normal/high/low)
|
||||
|
||||
**响应示例** (200):
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "任务更新成功",
|
||||
"data": {
|
||||
"task_id": "abc123-def456-ghi789",
|
||||
"status": "updated",
|
||||
"task_info": {
|
||||
"status": "completed",
|
||||
"message": "手动标记为完成",
|
||||
"updated_at": 1705257600.123,
|
||||
"priority": "normal"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应示例** (404):
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "任务未找到",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应示例** (400):
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "无效状态。必须是以下之一: pending, processing, completed, failed",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
**状态码**:
|
||||
- `200`: 成功
|
||||
- `400`: 无效请求
|
||||
- `404`: 任务不存在
|
||||
|
||||
---
|
||||
|
||||
### 12. 删除任务
|
||||
**端点**: `DELETE /task/{task_id}`
|
||||
|
||||
**描述**: 删除任务及其所有相关的文件和数据。
|
||||
|
||||
**路径参数**:
|
||||
- `task_id`: 任务 ID (UUID 格式)
|
||||
|
||||
**响应示例** (200):
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "任务及相关文件删除成功",
|
||||
"data": {
|
||||
"task_id": "abc123-def456-ghi789",
|
||||
"status": "deleted",
|
||||
"details": {
|
||||
"folders_deleted": 1,
|
||||
"total_size_deleted": 307200,
|
||||
"task_status": "completed"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应示例** (404):
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "任务未找到",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应示例** (409):
|
||||
```json
|
||||
{
|
||||
"code": 409,
|
||||
"message": "无法删除当前正在处理或等待处理的任务",
|
||||
"data": {
|
||||
"task_status": "processing"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**状态码**:
|
||||
- `200`: 成功
|
||||
- `404`: 任务不存在
|
||||
- `409`: 任务正在处理
|
||||
|
||||
---
|
||||
|
||||
## 📋 报告管理和查询
|
||||
|
||||
### 13. 分页查询已生成报告
|
||||
**端点**: `GET /reports`
|
||||
|
||||
**描述**: 分页查询所有已生成的处理报告,支持排序和过滤。
|
||||
|
||||
**查询参数**:
|
||||
- `page` (可选): 页码 (默认: 1)
|
||||
- `per_page` (可选): 每页报告数量 (默认: 20, 最大: 100)
|
||||
- `sort_by` (可选): 排序字段 (默认: created_at)
|
||||
- `sort_order` (可选): 排序顺序 (默认: desc)
|
||||
- `status` (可选): 按任务状态过滤
|
||||
|
||||
**响应示例** (200):
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "报告列表获取成功",
|
||||
"data": {
|
||||
"reports": [
|
||||
{
|
||||
"task_id": "abc123-def456-ghi789",
|
||||
"report_name": "08_34_01_5m",
|
||||
"status": "completed",
|
||||
"created_at": 1705257600.123,
|
||||
"file_count": 4,
|
||||
"total_size": 307200,
|
||||
"processing_time_seconds": 125.67,
|
||||
"main_report": {
|
||||
"name": "08_34_01_5m.processed_ch4_report.html",
|
||||
"size": 245760,
|
||||
"type": "report",
|
||||
"download_url": "/download/abc123-def456-ghi789/08_34_01_5m.processed/2026-01-14_10-33-29-961698_processing_run/08_34_01_5m.processed_ch4_report.html"
|
||||
},
|
||||
"all_files": [
|
||||
{
|
||||
"name": "08_34_01_5m.processed_ch4_report.html",
|
||||
"size": 245760,
|
||||
"type": "report",
|
||||
"download_url": "/download/abc123-def456-ghi789/08_34_01_5m.processed/2026-01-14_10-33-29-961698_processing_run/08_34_01_5m.processed_ch4_report.html"
|
||||
},
|
||||
{
|
||||
"name": "08_34_01_5m.processed_data.csv",
|
||||
"size": 153600,
|
||||
"type": "data",
|
||||
"download_url": "/download/abc123-def456-ghi789/08_34_01_5m.processed/2026-01-14_10-33-29-961698_processing_run/08_34_01_5m.processed_data.csv"
|
||||
},
|
||||
{
|
||||
"name": "08_34_01_5m.processed_config.yaml",
|
||||
"size": 2048,
|
||||
"type": "config",
|
||||
"download_url": "/download/abc123-def456-ghi789/08_34_01_5m.processed/2026-01-14_10-33-29-961698_processing_run/08_34_01_5m.processed_config.yaml"
|
||||
},
|
||||
{
|
||||
"name": "08_34_01_5m.processed_output_vars.json",
|
||||
"size": 4096,
|
||||
"type": "metadata",
|
||||
"download_url": "/download/abc123-def456-ghi789/08_34_01_5m.processed/2026-01-14_10-33-29-961698_processing_run/08_34_01_5m.processed_output_vars.json"
|
||||
}
|
||||
],
|
||||
"run_directory": "abc123-def456-ghi789/08_34_01_5m.processed/2026-01-14_10-33-29-961698_processing_run"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"per_page": 20,
|
||||
"total_reports": 45,
|
||||
"total_pages": 3,
|
||||
"has_next": true,
|
||||
"has_prev": false
|
||||
},
|
||||
"filters": {
|
||||
"sort_by": "created_at",
|
||||
"sort_order": "desc",
|
||||
"status": null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应示例** (400):
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Invalid parameter: per_page must be between 1 and 100",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
**状态码**:
|
||||
- `200`: 成功
|
||||
- `400`: 参数无效
|
||||
|
||||
---
|
||||
|
||||
## 📁 文件下载
|
||||
|
||||
### 14. 下载处理结果
|
||||
**端点**: `GET /download/{filename}`
|
||||
|
||||
**描述**: 下载处理后的结果文件,支持传入完整文件路径。
|
||||
|
||||
**路径参数**:
|
||||
- `filename`: 文件的完整路径或相对路径 (包含任务ID)
|
||||
|
||||
**状态码**:
|
||||
- `200`: 成功下载文件
|
||||
- `403`: 访问被拒绝
|
||||
- `404`: 文件不存在
|
||||
- `400`: 路径不是文件
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Web 界面
|
||||
|
||||
### 15. Web 管理界面
|
||||
**端点**: `GET /`
|
||||
|
||||
**描述**: 访问用户友好的 Web 界面,支持文件上传、任务监控和结果下载。
|
||||
|
||||
**状态码**:
|
||||
- `200`: 成功
|
||||
|
||||
---
|
||||
|
||||
*最后更新: 2026年1月29日*
|
||||
|
||||
*GasFlux Web API 版本: 1.0.0*
|
||||
255
ENVIRONMENT_VARIABLES.md
Normal file
255
ENVIRONMENT_VARIABLES.md
Normal file
@ -0,0 +1,255 @@
|
||||
# GasFlux Web API - 环境变量配置指南
|
||||
|
||||
本文档介绍如何使用环境变量来配置 GasFlux Web API 的行为。
|
||||
|
||||
## 概述
|
||||
|
||||
GasFlux 支持通过环境变量进行灵活配置,无需修改代码即可适应不同的部署环境和需求。
|
||||
|
||||
## 环境变量列表
|
||||
|
||||
### 服务器配置
|
||||
|
||||
| 变量名 | 默认值 | 描述 |
|
||||
|--------|--------|------|
|
||||
| `GASFLUX_HOST` | `0.0.0.0` | 服务器监听主机地址 |
|
||||
| `GASFLUX_PORT` | `5000` | 服务器监听端口 |
|
||||
| `GASFLUX_DEBUG` | `false` | 是否启用调试模式 (true/false) |
|
||||
|
||||
### 目录配置
|
||||
|
||||
| 变量名 | 默认值 | 描述 |
|
||||
|--------|--------|------|
|
||||
| `GASFLUX_UPLOAD_FOLDER` | `web_api_data/uploads` | 上传文件存储目录 |
|
||||
| `GASFLUX_OUTPUT_FOLDER` | `web_api_data/outputs` | 处理结果输出目录 |
|
||||
|
||||
### 文件和性能配置
|
||||
|
||||
| 变量名 | 默认值 | 描述 |
|
||||
|--------|--------|------|
|
||||
| `GASFLUX_MAX_CONTENT_LENGTH` | `104857600` (100MB) | 最大上传文件大小(字节) |
|
||||
| `GASFLUX_THREADS` | `8` | Waitress 服务器线程数 |
|
||||
| `GASFLUX_CONNECTION_LIMIT` | `100` | 最大并发连接数 |
|
||||
| `GASFLUX_CHANNEL_TIMEOUT` | `300` | 连接超时时间(秒) |
|
||||
|
||||
### 日志配置
|
||||
|
||||
| 变量名 | 默认值 | 描述 |
|
||||
|--------|--------|------|
|
||||
| `GASFLUX_LOG_LEVEL` | `INFO` | 日志级别 (DEBUG/INFO/WARNING/ERROR/CRITICAL) |
|
||||
| `GASFLUX_LOG_FILE` | `logs/gasflux_api.log` | 日志文件路径 |
|
||||
|
||||
### 安全和跨域配置
|
||||
|
||||
| 变量名 | 默认值 | 描述 |
|
||||
|--------|--------|------|
|
||||
| `GASFLUX_CORS_ORIGINS` | `*` | 允许的 CORS 源,用逗号分隔 |
|
||||
|
||||
### 任务管理配置
|
||||
|
||||
| 变量名 | 默认值 | 描述 |
|
||||
|--------|--------|------|
|
||||
| `GASFLUX_TASK_CLEANUP_INTERVAL` | `3600` | 任务清理检查间隔(秒) |
|
||||
| `GASFLUX_MAX_TASK_AGE` | `86400` | 任务最大保留时间(秒,24小时) |
|
||||
|
||||
## 配置示例
|
||||
|
||||
### 开发环境
|
||||
|
||||
```bash
|
||||
# 开发环境配置
|
||||
export GASFLUX_HOST=127.0.0.1
|
||||
export GASFLUX_PORT=5000
|
||||
export GASFLUX_DEBUG=true
|
||||
export GASFLUX_LOG_LEVEL=DEBUG
|
||||
```
|
||||
|
||||
### 生产环境
|
||||
|
||||
```bash
|
||||
# 生产环境配置
|
||||
export GASFLUX_HOST=0.0.0.0
|
||||
export GASFLUX_PORT=80
|
||||
export GASFLUX_DEBUG=false
|
||||
export GASFLUX_LOG_LEVEL=INFO
|
||||
export GASFLUX_THREADS=16
|
||||
export GASFLUX_CONNECTION_LIMIT=200
|
||||
export GASFLUX_MAX_CONTENT_LENGTH=524288000 # 500MB
|
||||
```
|
||||
|
||||
### Docker 部署
|
||||
|
||||
```bash
|
||||
# Docker 环境变量
|
||||
docker run -p 80:5000 \
|
||||
-e GASFLUX_HOST=0.0.0.0 \
|
||||
-e GASFLUX_PORT=5000 \
|
||||
-e GASFLUX_LOG_LEVEL=INFO \
|
||||
gasflux-api
|
||||
```
|
||||
|
||||
### Windows 服务
|
||||
|
||||
创建 `start_gasflux.bat`:
|
||||
|
||||
```batch
|
||||
@echo off
|
||||
set GASFLUX_HOST=0.0.0.0
|
||||
set GASFLUX_PORT=80
|
||||
set GASFLUX_LOG_LEVEL=INFO
|
||||
set GASFLUX_THREADS=8
|
||||
|
||||
GasFluxAPI.exe
|
||||
```
|
||||
|
||||
### Linux systemd
|
||||
|
||||
创建 `/etc/systemd/system/gasflux.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=GasFlux Web API
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=gasflux
|
||||
Environment=GASFLUX_HOST=0.0.0.0
|
||||
Environment=GASFLUX_PORT=80
|
||||
Environment=GASFLUX_LOG_LEVEL=INFO
|
||||
Environment=GASFLUX_THREADS=8
|
||||
ExecStart=/path/to/GasFluxAPI
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
## 运行时配置检查
|
||||
|
||||
启动应用后,可以通过以下方式检查当前配置:
|
||||
|
||||
### API 端点
|
||||
|
||||
```bash
|
||||
curl http://localhost:5000/config
|
||||
```
|
||||
|
||||
### 启动日志
|
||||
|
||||
应用启动时会输出当前配置信息:
|
||||
|
||||
```
|
||||
Configuration: {
|
||||
'host': '0.0.0.0',
|
||||
'port': 5000,
|
||||
'debug': False,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 安全考虑
|
||||
|
||||
1. **生产环境**:
|
||||
- 设置 `GASFLUX_DEBUG=false`
|
||||
- 配置适当的 `GASFLUX_CORS_ORIGINS`
|
||||
- 使用防火墙限制访问
|
||||
|
||||
2. **文件权限**:
|
||||
- 确保上传和输出目录有适当的权限
|
||||
- 定期清理旧文件
|
||||
|
||||
### 性能调优
|
||||
|
||||
1. **线程数**:根据 CPU 核心数设置 `GASFLUX_THREADS`
|
||||
2. **连接限制**:根据服务器容量设置 `GASFLUX_CONNECTION_LIMIT`
|
||||
3. **文件大小**:根据实际需求调整 `GASFLUX_MAX_CONTENT_LENGTH`
|
||||
|
||||
### 日志管理
|
||||
|
||||
1. **日志轮转**:定期备份和清理日志文件
|
||||
2. **日志级别**:
|
||||
- 开发环境:`DEBUG`
|
||||
- 生产环境:`INFO` 或 `WARNING`
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **端口占用**:
|
||||
```bash
|
||||
# 检查端口使用
|
||||
netstat -tulpn | grep :5000
|
||||
```
|
||||
|
||||
2. **权限问题**:
|
||||
```bash
|
||||
# 确保目录权限正确
|
||||
chmod -R 755 web_api_data/
|
||||
```
|
||||
|
||||
3. **配置不生效**:
|
||||
- 确保环境变量在应用启动前设置
|
||||
- 检查变量名拼写是否正确
|
||||
|
||||
### 调试配置
|
||||
|
||||
启用详细日志查看配置加载:
|
||||
|
||||
```bash
|
||||
export GASFLUX_LOG_LEVEL=DEBUG
|
||||
# 启动应用
|
||||
```
|
||||
|
||||
## 配置验证脚本
|
||||
|
||||
创建一个验证脚本 `check_config.py`:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Configuration validation script"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
def check_config():
|
||||
"""Check current configuration."""
|
||||
print("GasFlux Configuration Check")
|
||||
print("=" * 40)
|
||||
|
||||
# Server config
|
||||
print(f"Host: {os.getenv('GASFLUX_HOST', '0.0.0.0')}")
|
||||
print(f"Port: {os.getenv('GASFLUX_PORT', '5000')}")
|
||||
print(f"Debug: {os.getenv('GASFLUX_DEBUG', 'false')}")
|
||||
print(f"Log Level: {os.getenv('GASFLUX_LOG_LEVEL', 'INFO')}")
|
||||
|
||||
# Directory config
|
||||
base_dir = Path.cwd()
|
||||
upload_dir = base_dir / os.getenv('GASFLUX_UPLOAD_FOLDER', 'web_api_data/uploads')
|
||||
output_dir = base_dir / os.getenv('GASFLUX_OUTPUT_FOLDER', 'web_api_data/outputs')
|
||||
|
||||
print(f"Upload Folder: {upload_dir}")
|
||||
print(f"Output Folder: {output_dir}")
|
||||
|
||||
# Check directories
|
||||
print("
|
||||
Directory Status:")
|
||||
print(f"Upload dir exists: {upload_dir.exists()}")
|
||||
print(f"Output dir exists: {output_dir.exists()}")
|
||||
|
||||
# Performance config
|
||||
print("
|
||||
Performance Config:")
|
||||
print(f"Threads: {os.getenv('GASFLUX_THREADS', '8')}")
|
||||
print(f"Connection Limit: {os.getenv('GASFLUX_CONNECTION_LIMIT', '100')}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_config()
|
||||
```
|
||||
|
||||
运行验证:
|
||||
```bash
|
||||
python check_config.py
|
||||
```
|
||||
203
EXE_BUILD_README.md
Normal file
203
EXE_BUILD_README.md
Normal file
@ -0,0 +1,203 @@
|
||||
# GasFlux Web API - Executable Build Guide
|
||||
|
||||
This guide explains how to build and deploy the GasFlux Web API as a standalone executable using Waitress WSGI server.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before building the executable, ensure you have all dependencies installed:
|
||||
|
||||
```bash
|
||||
# Install all required packages
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Install build tools
|
||||
pip install pyinstaller waitress
|
||||
```
|
||||
|
||||
## Building the Executable
|
||||
|
||||
### Windows
|
||||
|
||||
Run the build script:
|
||||
|
||||
```cmd
|
||||
build_exe.bat
|
||||
```
|
||||
|
||||
Or manually:
|
||||
|
||||
```cmd
|
||||
pyinstaller --onefile ^
|
||||
--name GasFluxAPI ^
|
||||
--hidden-import waitress ^
|
||||
--hidden-import flask ^
|
||||
--hidden-import werkzeug ^
|
||||
--hidden-import yaml ^
|
||||
--hidden-import pandas ^
|
||||
--hidden-import numpy ^
|
||||
--hidden-import flask_cors ^
|
||||
--add-data "src\gasflux\gasflux_config.yaml;src\gasflux" ^
|
||||
--add-data "API_DOCUMENTATION.md;." ^
|
||||
--exclude-module matplotlib ^
|
||||
--exclude-module tkinter ^
|
||||
server_waitress.py
|
||||
```
|
||||
|
||||
### Linux/macOS
|
||||
|
||||
Make the build script executable and run it:
|
||||
|
||||
```bash
|
||||
chmod +x build_exe.sh
|
||||
./build_exe.sh
|
||||
```
|
||||
|
||||
Or manually:
|
||||
|
||||
```bash
|
||||
pyinstaller --onefile \
|
||||
--name GasFluxAPI \
|
||||
--hidden-import waitress \
|
||||
--hidden-import flask \
|
||||
--hidden-import werkzeug \
|
||||
--hidden-import yaml \
|
||||
--hidden-import pandas \
|
||||
--hidden-import numpy \
|
||||
--hidden-import flask_cors \
|
||||
--add-data "src/gasflux/gasflux_config.yaml:src/gasflux" \
|
||||
--add-data "API_DOCUMENTATION.md:." \
|
||||
--exclude-module matplotlib \
|
||||
--exclude-module tkinter \
|
||||
server_waitress.py
|
||||
```
|
||||
|
||||
## Running the Executable
|
||||
|
||||
After successful build, you'll find `GasFluxAPI.exe` (Windows) or `GasFluxAPI` (Linux/macOS) in the `dist/` directory.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
GasFluxAPI.exe
|
||||
|
||||
# Linux/macOS
|
||||
./GasFluxAPI
|
||||
```
|
||||
|
||||
The server will start on `http://localhost:5000` by default.
|
||||
|
||||
### Command Line Options
|
||||
|
||||
Waitress supports various command-line options. You can create a wrapper script to customize:
|
||||
|
||||
```bash
|
||||
# Custom port and host
|
||||
waitress-serve --listen=0.0.0.0:8080 server_waitress:app
|
||||
|
||||
# Production settings
|
||||
waitress-serve --threads=16 --connection-limit=200 server_waitress:app
|
||||
```
|
||||
|
||||
## Deployment Considerations
|
||||
|
||||
### Directory Structure
|
||||
|
||||
When deploying the executable, ensure these directories exist and are writable:
|
||||
|
||||
```
|
||||
your-deployment-directory/
|
||||
├── GasFluxAPI(.exe) # The executable
|
||||
├── web_api_data/ # Data directory (auto-created)
|
||||
│ ├── uploads/ # Uploaded files
|
||||
│ └── outputs/ # Processing results
|
||||
├── logs/ # Log files (auto-created)
|
||||
└── gasflux_config.yaml # Configuration (optional)
|
||||
```
|
||||
|
||||
### Firewall Configuration
|
||||
|
||||
Make sure the server port (default 5000) is open in your firewall.
|
||||
|
||||
### Production Deployment
|
||||
|
||||
For production use:
|
||||
|
||||
1. **Use a reverse proxy**: Place Nginx or Apache in front of the executable
|
||||
2. **SSL/TLS**: Configure HTTPS
|
||||
3. **Process management**: Use systemd, supervisor, or similar
|
||||
4. **Logging**: Configure log rotation
|
||||
|
||||
### Example systemd Service (Linux)
|
||||
|
||||
Create `/etc/systemd/system/gasflux-api.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=GasFlux Web API
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=your-user
|
||||
WorkingDirectory=/path/to/deployment
|
||||
ExecStart=/path/to/deployment/GasFluxAPI
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable gasflux-api
|
||||
sudo systemctl start gasflux-api
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **"Permission denied"**: Ensure the executable has execute permissions and data directories are writable
|
||||
|
||||
2. **"Port already in use"**: Change the port or stop other services using port 5000
|
||||
|
||||
3. **"Module not found"**: Some dependencies might be missing. Try:
|
||||
```bash
|
||||
pip install --upgrade pyinstaller
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
4. **Large executable size**: The `--exclude-module` options help reduce size, but some scientific packages are large
|
||||
|
||||
### Performance Tuning
|
||||
|
||||
- **Threads**: Increase `--threads` for more concurrent requests
|
||||
- **Connection limit**: Adjust `--connection-limit` based on server capacity
|
||||
- **Memory**: Monitor memory usage, especially with large data processing
|
||||
|
||||
### Logs
|
||||
|
||||
Logs are written to:
|
||||
- Console output (when running interactively)
|
||||
- `logs/gasflux_api.log` file
|
||||
|
||||
Check logs for startup errors and runtime issues.
|
||||
|
||||
## API Documentation
|
||||
|
||||
Once running, access the API documentation at:
|
||||
- Web interface: `http://your-server:5000`
|
||||
- API docs: See `API_DOCUMENTATION.md`
|
||||
- Statistics: `GET /stats` - Real-time API statistics and monitoring
|
||||
- Health check: `GET /health` - System health with performance metrics
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Default configuration listens on all interfaces (`0.0.0.0`)
|
||||
- No authentication is configured by default
|
||||
- Consider adding authentication and SSL for production use
|
||||
- Regularly update dependencies for security patches
|
||||
422
GASFLUX_CONFIG_DOCUMENTATION.md
Normal file
422
GASFLUX_CONFIG_DOCUMENTATION.md
Normal file
@ -0,0 +1,422 @@
|
||||
# GasFlux 配置清单完整说明文档
|
||||
|
||||
## 概述
|
||||
|
||||
GasFlux 是一个用于处理无人机气体通量数据的完整管道系统。本文档详细说明了所有配置文件参数及其使用方法。
|
||||
|
||||
## 配置文件结构
|
||||
|
||||
GasFlux 使用 YAML 格式的配置文件,主要包含以下几个部分:
|
||||
|
||||
1. **输出设置** (`output_dir`)
|
||||
2. **必需列定义** (`required_cols`)
|
||||
3. **气体配置** (`gases`)
|
||||
4. **处理策略** (`strategies`)
|
||||
5. **背景校正设置** (`algorithmic_baseline_settings`)
|
||||
6. **半变异函数设置** (`semivariogram_settings`)
|
||||
7. **普通克里金设置** (`ordinary_kriging_settings`)
|
||||
8. **可选过滤器** (`filters`) - 高级配置
|
||||
|
||||
## 详细参数说明
|
||||
|
||||
### 1. 输出设置
|
||||
|
||||
```yaml
|
||||
output_dir: ./output # 输出目录路径
|
||||
```
|
||||
|
||||
- **类型**: 字符串
|
||||
- **描述**: 指定处理结果的输出目录
|
||||
- **默认值**: `./output`
|
||||
- **注意**: 可以使用相对路径或绝对路径
|
||||
|
||||
### 2. 必需列定义
|
||||
|
||||
```yaml
|
||||
required_cols:
|
||||
latitude: [-90, 90] # 纬度范围
|
||||
longitude: [-180, 180] # 经度范围
|
||||
height_ato: [0, 200] # 相对起飞高度(米)
|
||||
windspeed: [0, 20] # 风速(m/s)
|
||||
winddir: [0, 360] # 风向(度)
|
||||
temperature: [-50, 60] # 温度(°C)
|
||||
pressure: [900, 1100] # 气压(hPa)
|
||||
```
|
||||
|
||||
- **格式**: 字典,键为列名,值为 `[最小值, 最大值]` 数组
|
||||
- **作用**: 数据验证和范围检查
|
||||
- **注意**:
|
||||
- 所有值必须是 float64 类型
|
||||
- 不允许 NaN 值
|
||||
- 超出范围的值会报错
|
||||
|
||||
### 3. 气体配置
|
||||
|
||||
```yaml
|
||||
gases:
|
||||
co2: [300, 500] # 二氧化碳浓度范围(ppm)
|
||||
ch4: [1.5, 10.0] # 甲烷浓度范围(ppm)
|
||||
```
|
||||
|
||||
- **格式**: 字典,键为气体名称,值为 `[最小值, 最大值]` 数组
|
||||
- **作用**: 指定要处理的 gases 及其合理浓度范围
|
||||
- **注意**:
|
||||
- 气体名称必须与数据中的列名完全匹配
|
||||
- 范围用于数据验证
|
||||
|
||||
### 4. 处理策略
|
||||
|
||||
```yaml
|
||||
strategies:
|
||||
background: "algorithm" # 背景校正方法
|
||||
sensor: "insitu" # 传感器类型
|
||||
spatial: "curtain" # 空间处理模式: "curtain" 或 "spiral"
|
||||
interpolation: "kriging" # 插值方法
|
||||
```
|
||||
|
||||
- **background**: 目前只支持 `"algorithm"`
|
||||
- **sensor**: 目前只支持 `"insitu"`
|
||||
- **spatial**: `"curtain"` (平面模式) 或 `"spiral"` (螺旋模式)
|
||||
- **interpolation**: 目前只支持 `"kriging"`
|
||||
|
||||
## 背景校正算法设置
|
||||
|
||||
### 算法总览
|
||||
|
||||
```yaml
|
||||
algorithmic_baseline_settings:
|
||||
algorithm: fastchrom # 选择使用的算法
|
||||
# 以下是各算法的具体参数
|
||||
```
|
||||
|
||||
### 支持的算法
|
||||
|
||||
#### 1. FastChrom 算法 (推荐用于大多数情况)
|
||||
|
||||
```yaml
|
||||
algorithmic_baseline_settings:
|
||||
algorithm: fastchrom
|
||||
fastchrom:
|
||||
half_window: 6 # 半窗口大小,用于基线拟合
|
||||
threshold: "custom" # 阈值方法,推荐使用 "custom"
|
||||
min_fwhm: ~ # 最小峰宽,~ 表示 null
|
||||
interp_half_window: 3 # 插值半窗口
|
||||
smooth_half_window: 3 # 平滑半窗口
|
||||
weights: ~ # 权重,~ 表示 null
|
||||
max_iter: 100 # 最大迭代次数
|
||||
min_length: 2 # 最小段长度
|
||||
```
|
||||
|
||||
#### 2. Dietrich 算法
|
||||
|
||||
```yaml
|
||||
algorithmic_baseline_settings:
|
||||
algorithm: dietrich
|
||||
dietrich:
|
||||
poly_order: 5 # 多项式阶数
|
||||
smooth_half_window: 5 # 平滑半窗口
|
||||
```
|
||||
|
||||
#### 3. FABC (Fully Automatic Baseline Correction) 算法
|
||||
|
||||
```yaml
|
||||
algorithmic_baseline_settings:
|
||||
algorithm: fabc
|
||||
fabc:
|
||||
lam: 10000 # 平滑参数,值越大基线越平滑
|
||||
scale: 10 # 小波变换尺度
|
||||
diff_order: 2 # 微分矩阵阶数
|
||||
```
|
||||
|
||||
#### 4. Golotvin 算法
|
||||
|
||||
```yaml
|
||||
algorithmic_baseline_settings:
|
||||
algorithm: golotvin
|
||||
golotvin:
|
||||
half_window: 2 # 半窗口大小
|
||||
sections: 10 # 分段数量
|
||||
```
|
||||
|
||||
## 克里金插值设置
|
||||
|
||||
### 半变异函数设置
|
||||
|
||||
```yaml
|
||||
semivariogram_settings:
|
||||
model: spherical # 变异函数模型: spherical, gaussian, exponential
|
||||
estimator: cressie # 估计器: cressie, matheron, dowd
|
||||
n_lags: 20 # 滞后期数
|
||||
bin_func: even # 分箱函数: even, uniform
|
||||
fit_method: lm # 拟合方法: lm, manual
|
||||
maxlag: 100 # 最大滞后距离(米)
|
||||
tolerance: 10 # 方向容差(度)
|
||||
azimuth: 0 # 方位角(度,0为水平向右)
|
||||
bandwidth: 20 # 带宽(米)
|
||||
```
|
||||
|
||||
### 普通克里金设置
|
||||
|
||||
```yaml
|
||||
ordinary_kriging_settings:
|
||||
min_points: 3 # 最小邻点数
|
||||
max_points: 100 # 最大邻点数
|
||||
grid_resolution: 500 # 网格分辨率
|
||||
min_nodes: 10 # 最小网格节点数
|
||||
y_min: ~ # 最小y值覆盖,手动覆盖,~表示使用默认
|
||||
cut_ground: True # 是否切割地面以下的值
|
||||
```
|
||||
|
||||
## 空间处理模式详解
|
||||
|
||||
### 平面模式 (Curtain Mode)
|
||||
|
||||
**适用场景**: 直线飞行路径,沿着固定方向的多个平行航线
|
||||
|
||||
```yaml
|
||||
strategies:
|
||||
spatial: "curtain"
|
||||
|
||||
# 推荐的半变异函数设置(平面模式)
|
||||
semivariogram_settings:
|
||||
model: spherical
|
||||
estimator: cressie
|
||||
n_lags: 15
|
||||
bin_func: even
|
||||
fit_method: lm
|
||||
maxlag: 80 # 根据飞行距离调整
|
||||
tolerance: 15 # 较大的容差适应直线路径
|
||||
azimuth: 0 # 沿着飞行方向
|
||||
bandwidth: 25 # 较大的带宽适应航线间距
|
||||
|
||||
ordinary_kriging_settings:
|
||||
min_points: 5
|
||||
max_points: 80
|
||||
grid_resolution: 200 # 较高的分辨率
|
||||
min_nodes: 10
|
||||
y_min: 20 # 设置最小高度
|
||||
cut_ground: True
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 应用风偏移校正
|
||||
- 沿着最大单调序列提取航线
|
||||
- 适合规则的栅格飞行模式
|
||||
|
||||
### 螺旋模式 (Spiral Mode)
|
||||
|
||||
**适用场景**: 螺旋或圆形飞行路径
|
||||
|
||||
```yaml
|
||||
strategies:
|
||||
spatial: "spiral"
|
||||
|
||||
# 推荐的半变异函数设置(螺旋模式)
|
||||
semivariogram_settings:
|
||||
model: spherical
|
||||
estimator: cressie
|
||||
n_lags: 20
|
||||
bin_func: even
|
||||
fit_method: lm
|
||||
maxlag: 100 # 较大的最大距离适应螺旋半径
|
||||
tolerance: 30 # 更大的容差适应圆形路径
|
||||
azimuth: 0 # 径向方向
|
||||
bandwidth: 20 # 适应螺旋间距
|
||||
|
||||
ordinary_kriging_settings:
|
||||
min_points: 3
|
||||
max_points: 100
|
||||
grid_resolution: 500 # 较低的分辨率适应圆形区域
|
||||
min_nodes: 10
|
||||
y_min: ~ # 自动确定最小高度
|
||||
cut_ground: True
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 不应用风偏移校正(假设风垂直于螺旋)
|
||||
- 计算圆偏差和中心
|
||||
- 重新居中方位角
|
||||
- 使用周长距离作为x坐标
|
||||
|
||||
## 可选过滤器(高级配置)
|
||||
|
||||
```yaml
|
||||
filters:
|
||||
course_filter:
|
||||
azimuth_filter: 15 # 方位角过滤容差(度)
|
||||
azimuth_window: 8 # 滚动中位数窗口大小
|
||||
elevation_filter: 8 # 海拔过滤容差(度)
|
||||
```
|
||||
|
||||
## 完整配置示例
|
||||
|
||||
### 平面模式完整配置
|
||||
|
||||
```yaml
|
||||
output_dir: ./curtain_output
|
||||
|
||||
required_cols:
|
||||
latitude: [-90, 90]
|
||||
longitude: [-180, 180]
|
||||
height_ato: [0, 200]
|
||||
windspeed: [0, 20]
|
||||
winddir: [0, 360]
|
||||
temperature: [-50, 60]
|
||||
pressure: [900, 1100]
|
||||
|
||||
gases:
|
||||
co2: [300, 500]
|
||||
ch4: [1.5, 10.0]
|
||||
|
||||
strategies:
|
||||
background: "algorithm"
|
||||
sensor: "insitu"
|
||||
spatial: "curtain"
|
||||
interpolation: "kriging"
|
||||
|
||||
algorithmic_baseline_settings:
|
||||
algorithm: fastchrom
|
||||
fastchrom:
|
||||
half_window: 6
|
||||
threshold: "custom"
|
||||
min_fwhm: ~
|
||||
interp_half_window: 3
|
||||
smooth_half_window: 3
|
||||
weights: ~
|
||||
max_iter: 100
|
||||
min_length: 2
|
||||
|
||||
semivariogram_settings:
|
||||
model: spherical
|
||||
estimator: cressie
|
||||
n_lags: 15
|
||||
bin_func: even
|
||||
fit_method: lm
|
||||
maxlag: 80
|
||||
tolerance: 15
|
||||
azimuth: 0
|
||||
bandwidth: 25
|
||||
|
||||
ordinary_kriging_settings:
|
||||
min_points: 5
|
||||
max_points: 80
|
||||
grid_resolution: 200
|
||||
min_nodes: 10
|
||||
y_min: 20
|
||||
cut_ground: True
|
||||
```
|
||||
|
||||
### 螺旋模式完整配置
|
||||
|
||||
```yaml
|
||||
output_dir: ./spiral_output
|
||||
|
||||
required_cols:
|
||||
latitude: [-90, 90]
|
||||
longitude: [-180, 180]
|
||||
height_ato: [0, 200]
|
||||
windspeed: [0, 20]
|
||||
winddir: [0, 360]
|
||||
temperature: [-50, 60]
|
||||
pressure: [900, 1100]
|
||||
|
||||
gases:
|
||||
co2: [300, 500]
|
||||
ch4: [1.5, 10.0]
|
||||
|
||||
strategies:
|
||||
background: "algorithm"
|
||||
sensor: "insitu"
|
||||
spatial: "spiral"
|
||||
interpolation: "kriging"
|
||||
|
||||
algorithmic_baseline_settings:
|
||||
algorithm: fastchrom
|
||||
fastchrom:
|
||||
half_window: 6
|
||||
threshold: "custom"
|
||||
min_fwhm: ~
|
||||
interp_half_window: 3
|
||||
smooth_half_window: 3
|
||||
weights: ~
|
||||
max_iter: 100
|
||||
min_length: 2
|
||||
|
||||
semivariogram_settings:
|
||||
model: spherical
|
||||
estimator: cressie
|
||||
n_lags: 20
|
||||
bin_func: even
|
||||
fit_method: lm
|
||||
maxlag: 100
|
||||
tolerance: 30
|
||||
azimuth: 0
|
||||
bandwidth: 20
|
||||
|
||||
ordinary_kriging_settings:
|
||||
min_points: 3
|
||||
max_points: 100
|
||||
grid_resolution: 500
|
||||
min_nodes: 10
|
||||
y_min: ~
|
||||
cut_ground: True
|
||||
```
|
||||
|
||||
## 参数调优指南
|
||||
|
||||
### 空间模式选择
|
||||
|
||||
1. **平面模式 (curtain)**:
|
||||
- 飞行路径呈直线或平行航线
|
||||
- 数据点分布在规则的栅格中
|
||||
- 适合大型区域的系统采样
|
||||
|
||||
2. **螺旋模式 (spiral)**:
|
||||
- 飞行路径呈螺旋或圆形
|
||||
- 数据点围绕中心点分布
|
||||
- 适合点源排放的详细采样
|
||||
|
||||
### 半变异函数调优
|
||||
|
||||
1. **maxlag**: 设置为数据空间范围的 80-100%
|
||||
2. **tolerance**: 平面模式 10-20°,螺旋模式 20-40°
|
||||
3. **bandwidth**: 根据飞行间距调整,过大会影响精度
|
||||
4. **n_lags**: 通常 10-25,根据数据量调整
|
||||
|
||||
### 克里金插值调优
|
||||
|
||||
1. **grid_resolution**: 影响输出网格密度
|
||||
2. **min_points/max_points**: 平衡计算速度和精度
|
||||
3. **cut_ground**: 根据是否有地面高程数据决定
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见错误
|
||||
|
||||
1. **"autodetected range of [nan, nan] is not finite"**
|
||||
- 检查气体列是否有NaN值
|
||||
- 确认气体名称与数据列名匹配
|
||||
|
||||
2. **"`ydata` must not be empty!"**
|
||||
- 检查半变异函数参数是否过于严格
|
||||
- 减少 maxlag 或增加 tolerance
|
||||
|
||||
3. **Missing columns**
|
||||
- 确认数据列名与配置中的 gases 键匹配
|
||||
- 检查必需列是否存在
|
||||
|
||||
### 性能优化
|
||||
|
||||
1. **减少计算时间**: 降低 grid_resolution,减少 max_points
|
||||
2. **提高精度**: 增加 n_lags,调整半变异函数参数
|
||||
3. **内存优化**: 适当减少 max_points 和 grid_resolution
|
||||
|
||||
## 版本信息
|
||||
|
||||
- **GasFlux 版本**: 开发版
|
||||
- **最后更新**: 2025年2月
|
||||
- **文档版本**: 1.0
|
||||
|
||||
---
|
||||
|
||||
*本配置文档基于 GasFlux 系统的实际代码实现编写。如有疑问,请参考源码注释或提交 Issue。*
|
||||
57
README.md
57
README.md
@ -14,6 +14,7 @@ GasFlux 是一个专门用于处理无人机或飞行器采集的气体浓度数
|
||||
- **灵活配置**: 基于 YAML 的配置系统,支持自定义参数
|
||||
- **可视化输出**: 自动生成数据分析图表和报告
|
||||
- **命令行工具**: 提供直观的 CLI 接口
|
||||
- **Web API 服务**: 提供完整的 RESTful API,支持异步处理、实时监控和详细统计
|
||||
- **跨平台支持**: 支持 Windows、macOS 和 Linux
|
||||
|
||||
## 📋 系统要求
|
||||
@ -77,6 +78,62 @@ gasflux process /path/to/data/directory
|
||||
python src/gasflux/run_example.py your_data.xlsx
|
||||
```
|
||||
|
||||
## 🌐 Web API 服务
|
||||
|
||||
GasFlux 提供完整的 Web API 服务,支持通过 HTTP 接口进行数据处理。详见以下文档:
|
||||
|
||||
- [API 文档](API_DOCUMENTATION.md) - 完整的 API 接口说明
|
||||
- [环境变量配置](ENVIRONMENT_VARIABLES.md) - 环境变量配置指南
|
||||
- [Waitress 部署](WAITRESS_DEPLOYMENT.md) - 生产环境部署指南
|
||||
|
||||
### 启动 Web 服务
|
||||
|
||||
```bash
|
||||
# 启动 API 服务(使用默认配置)
|
||||
python run_api.py
|
||||
|
||||
# 或使用自定义环境变量
|
||||
export GASFLUX_PORT=8080
|
||||
export GASFLUX_LOG_LEVEL=DEBUG
|
||||
python run_api.py
|
||||
|
||||
# 服务将在配置的地址和端口启动
|
||||
# 访问 http://localhost:5000 查看 Web 界面
|
||||
```
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
GasFlux 支持通过环境变量进行灵活配置。详见 [ENVIRONMENT_VARIABLES.md](ENVIRONMENT_VARIABLES.md) 获取完整配置指南。
|
||||
|
||||
**常用配置示例**:
|
||||
|
||||
```bash
|
||||
# 端口配置
|
||||
export GASFLUX_PORT=8080
|
||||
|
||||
# 日志配置
|
||||
export GASFLUX_LOG_LEVEL=DEBUG
|
||||
export GASFLUX_LOG_FILE=/var/log/gasflux/api.log
|
||||
|
||||
# 性能配置
|
||||
export GASFLUX_THREADS=16
|
||||
export GASFLUX_MAX_CONTENT_LENGTH=524288000 # 500MB
|
||||
|
||||
# 目录配置
|
||||
export GASFLUX_UPLOAD_FOLDER=/data/uploads
|
||||
export GASFLUX_OUTPUT_FOLDER=/data/outputs
|
||||
```
|
||||
|
||||
### API 特性
|
||||
|
||||
- **异步处理**: 支持大文件处理,不阻塞客户端
|
||||
- **实时监控**: 通过任务 ID 实时查询处理状态
|
||||
- **文件管理**: 自动文件上传、处理和下载
|
||||
- **健康检查**: 系统状态监控和诊断
|
||||
- **详细日志**: 完整的请求日志和性能监控
|
||||
- **统计监控**: 实时 API 统计、性能指标和系统资源监控
|
||||
- **环境变量配置**: 通过环境变量灵活配置,无需修改代码
|
||||
|
||||
## 📖 使用指南
|
||||
|
||||
### 配置文件
|
||||
|
||||
271
WAITRESS_DEPLOYMENT.md
Normal file
271
WAITRESS_DEPLOYMENT.md
Normal file
@ -0,0 +1,271 @@
|
||||
# GasFlux Web API - Waitress WSGI 部署指南
|
||||
|
||||
本文档介绍如何使用 Waitress WSGI 服务器打包和部署 GasFlux Web API 为独立可执行文件。
|
||||
|
||||
## 概述
|
||||
|
||||
Waitress 是一个纯 Python WSGI 服务器,适合生产环境使用。本部署方案将 Flask 应用与 Waitress 打包为单个可执行文件,无需额外安装 Python 环境。
|
||||
|
||||
## 文件说明
|
||||
|
||||
### 核心文件
|
||||
|
||||
- **`server_waitress.py`** - Waitress 服务器入口点
|
||||
- **`src/gasflux/app.py`** - Flask 应用定义
|
||||
- **`build_exe.bat`** - Windows 构建脚本
|
||||
- **`build_exe.sh`** - Linux/macOS 构建脚本
|
||||
- **`EXE_BUILD_README.md`** - 详细构建和部署指南
|
||||
|
||||
### 构建产物
|
||||
|
||||
- **`dist/GasFluxAPI.exe`** (Windows) 或 **`dist/GasFluxAPI`** (Linux/macOS) - 独立可执行文件
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
pip install pyinstaller waitress
|
||||
```
|
||||
|
||||
### 2. 构建可执行文件
|
||||
|
||||
**Windows:**
|
||||
```cmd
|
||||
build_exe.bat
|
||||
```
|
||||
|
||||
**Linux/macOS:**
|
||||
```bash
|
||||
chmod +x build_exe.sh
|
||||
./build_exe.sh
|
||||
```
|
||||
|
||||
### 3. 运行服务器
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
dist\GasFluxAPI.exe
|
||||
|
||||
# Linux/macOS
|
||||
./dist/GasFluxAPI
|
||||
```
|
||||
|
||||
服务器将在 `http://localhost:5000` 启动。
|
||||
|
||||
## 服务器配置
|
||||
|
||||
### 默认配置
|
||||
|
||||
```python
|
||||
host = '0.0.0.0' # 监听所有接口
|
||||
port = 5000 # 默认端口
|
||||
threads = 8 # 工作线程数
|
||||
connection_limit = 100 # 最大并发连接
|
||||
timeout = 300 # 请求超时(秒)
|
||||
```
|
||||
|
||||
### 自定义配置
|
||||
|
||||
修改 `server_waitress.py` 中的参数:
|
||||
|
||||
```python
|
||||
# 自定义端口
|
||||
port = 8080
|
||||
|
||||
# 增加线程数
|
||||
threads = 16
|
||||
|
||||
# 调整超时时间
|
||||
timeout = 600 # 10分钟
|
||||
```
|
||||
|
||||
## 部署架构
|
||||
|
||||
### 单文件部署
|
||||
|
||||
```
|
||||
部署目录/
|
||||
├── GasFluxAPI(.exe) # 可执行文件
|
||||
├── web_api_data/ # 数据目录(自动创建)
|
||||
│ ├── uploads/ # 上传文件
|
||||
│ └── outputs/ # 处理结果
|
||||
├── logs/ # 日志文件(自动创建)
|
||||
└── gasflux_config.yaml # 配置文件(可选)
|
||||
```
|
||||
|
||||
### 生产部署建议
|
||||
|
||||
#### 使用反向代理
|
||||
|
||||
```nginx
|
||||
# nginx 配置示例
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 使用进程管理器
|
||||
|
||||
**systemd (Linux):**
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=GasFlux Web API
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
WorkingDirectory=/path/to/deployment
|
||||
ExecStart=/path/to/deployment/GasFluxAPI
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
**Windows 服务:**
|
||||
|
||||
使用 NSSM (Non-Sucking Service Manager) 将 exe 注册为 Windows 服务。
|
||||
|
||||
## 性能优化
|
||||
|
||||
### Waitress 参数调优
|
||||
|
||||
```python
|
||||
serve(
|
||||
app,
|
||||
host='0.0.0.0',
|
||||
port=5000,
|
||||
threads=16, # 根据 CPU 核心数调整
|
||||
connection_limit=200, # 最大并发连接
|
||||
timeout=300, # 长请求超时
|
||||
backlog=2048, # 连接队列长度
|
||||
recv_bytes=8192, # 接收缓冲区
|
||||
send_bytes=8192, # 发送缓冲区
|
||||
)
|
||||
```
|
||||
|
||||
### 监控和日志
|
||||
|
||||
- **访问日志**: `logs/gasflux_api.log`
|
||||
- **控制台输出**: 启动时显示的控制台信息
|
||||
- **健康检查**: `GET /health` 端点
|
||||
|
||||
### 资源使用
|
||||
|
||||
- **内存**: 每个工作进程约 50-200MB(取决于数据处理量)
|
||||
- **CPU**: 多线程处理,建议 4+ 核心
|
||||
- **磁盘**: 日志和临时文件存储
|
||||
|
||||
## 安全考虑
|
||||
|
||||
### 基本安全措施
|
||||
|
||||
1. **防火墙配置**: 只开放必要端口
|
||||
2. **用户权限**: 以非 root 用户运行
|
||||
3. **文件权限**: 数据目录限制访问权限
|
||||
|
||||
### 生产环境建议
|
||||
|
||||
1. **HTTPS**: 使用反向代理配置 SSL
|
||||
2. **认证**: 添加 API 密钥或 JWT 认证
|
||||
3. **限流**: 实现请求频率限制
|
||||
4. **监控**: 设置日志监控和告警
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **端口占用**
|
||||
```bash
|
||||
# 检查端口使用
|
||||
netstat -tulpn | grep :5000
|
||||
# 或 Windows: netstat -ano | findstr :5000
|
||||
```
|
||||
|
||||
2. **权限问题**
|
||||
```bash
|
||||
# 确保可执行文件有执行权限
|
||||
chmod +x GasFluxAPI
|
||||
# 确保数据目录可写
|
||||
chmod -R 755 web_api_data/
|
||||
```
|
||||
|
||||
3. **构建失败**
|
||||
```bash
|
||||
# 清理旧构建
|
||||
rm -rf build/ dist/
|
||||
# 重新安装依赖
|
||||
pip install --upgrade pyinstaller waitress
|
||||
```
|
||||
|
||||
4. **内存不足**
|
||||
- 减少线程数
|
||||
- 增加服务器内存
|
||||
- 优化数据处理流程
|
||||
|
||||
### 调试模式
|
||||
|
||||
临时启用详细日志:
|
||||
|
||||
```bash
|
||||
# 修改 server_waitress.py
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
```
|
||||
|
||||
## 维护和更新
|
||||
|
||||
### 更新部署
|
||||
|
||||
1. 构建新版本可执行文件
|
||||
2. 备份数据目录
|
||||
3. 停止旧服务
|
||||
4. 替换可执行文件
|
||||
5. 启动新服务
|
||||
|
||||
### 备份策略
|
||||
|
||||
- **数据**: `web_api_data/` 目录
|
||||
- **配置**: `gasflux_config.yaml`
|
||||
- **日志**: `logs/` 目录
|
||||
|
||||
## API 使用
|
||||
|
||||
部署完成后,API 端点与开发环境相同:
|
||||
|
||||
- **健康检查**: `GET /health`
|
||||
- **文件上传**: `POST /upload`
|
||||
- **任务状态**: `GET /task/{task_id}`
|
||||
- **文件下载**: `GET /download/{filename}`
|
||||
- **Web 界面**: `GET /`
|
||||
|
||||
详细 API 文档请参考 `API_DOCUMENTATION.md`。
|
||||
|
||||
## 总结
|
||||
|
||||
使用 Waitress 打包的方案提供了:
|
||||
|
||||
- ✅ **零依赖部署**: 单文件可执行
|
||||
- ✅ **生产就绪**: WSGI 服务器,适合高并发
|
||||
- ✅ **跨平台**: 支持 Windows/Linux/macOS
|
||||
- ✅ **易于维护**: 简单的部署和更新流程
|
||||
- ✅ **完整功能**: 保留所有 Flask 应用功能
|
||||
|
||||
这种部署方式特别适合:
|
||||
- Windows 服务器环境
|
||||
- 简单的生产部署需求
|
||||
- 需要独立可执行文件的场景
|
||||
62
build_exe.bat
Normal file
62
build_exe.bat
Normal file
@ -0,0 +1,62 @@
|
||||
@echo off
|
||||
REM GasFlux Web API - Build executable with PyInstaller
|
||||
REM This script builds a standalone executable using Waitress WSGI server
|
||||
|
||||
echo Building GasFlux Web API executable...
|
||||
|
||||
REM Check if PyInstaller is installed
|
||||
python -c "import PyInstaller" >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo Error: PyInstaller is not installed. Please run:
|
||||
echo pip install pyinstaller waitress
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Check if Waitress is installed
|
||||
python -c "import waitress" >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo Error: Waitress is not installed. Please run:
|
||||
echo pip install waitress
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Create dist directory if it doesn't exist
|
||||
if not exist "dist" mkdir dist
|
||||
|
||||
echo Creating executable with PyInstaller...
|
||||
|
||||
REM Build the executable
|
||||
pyinstaller --onefile ^
|
||||
--name GasFluxAPI ^
|
||||
--hidden-import waitress ^
|
||||
--hidden-import flask ^
|
||||
--hidden-import werkzeug ^
|
||||
--hidden-import yaml ^
|
||||
--hidden-import pandas ^
|
||||
--hidden-import numpy ^
|
||||
--hidden-import flask_cors ^
|
||||
--hidden-import psutil ^
|
||||
--add-data "src\gasflux\gasflux_config.yaml;src\gasflux" ^
|
||||
--add-data "API_DOCUMENTATION.md;." ^
|
||||
--exclude-module matplotlib ^
|
||||
--exclude-module tkinter ^
|
||||
server_waitress.py
|
||||
|
||||
if errorlevel 1 (
|
||||
echo Error: Failed to build executable
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Build completed successfully!
|
||||
echo Executable created: dist\GasFluxAPI.exe
|
||||
echo.
|
||||
echo To run the server:
|
||||
echo GasFluxAPI.exe
|
||||
echo.
|
||||
echo The server will start on http://localhost:5000
|
||||
echo.
|
||||
pause
|
||||
55
build_exe.sh
Normal file
55
build_exe.sh
Normal file
@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
# GasFlux Web API - Build executable with PyInstaller
|
||||
# This script builds a standalone executable using Waitress WSGI server
|
||||
|
||||
echo "Building GasFlux Web API executable..."
|
||||
|
||||
# Check if PyInstaller is installed
|
||||
if ! python3 -c "import PyInstaller" 2>/dev/null; then
|
||||
echo "Error: PyInstaller is not installed. Please run:"
|
||||
echo "pip install pyinstaller waitress"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Waitress is installed
|
||||
if ! python3 -c "import waitress" 2>/dev/null; then
|
||||
echo "Error: Waitress is not installed. Please run:"
|
||||
echo "pip install waitress"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create dist directory if it doesn't exist
|
||||
mkdir -p dist
|
||||
|
||||
echo "Creating executable with PyInstaller..."
|
||||
|
||||
# Build the executable
|
||||
pyinstaller --onefile \
|
||||
--name GasFluxAPI \
|
||||
--hidden-import waitress \
|
||||
--hidden-import flask \
|
||||
--hidden-import werkzeug \
|
||||
--hidden-import yaml \
|
||||
--hidden-import pandas \
|
||||
--hidden-import numpy \
|
||||
--hidden-import flask_cors \
|
||||
--hidden-import psutil \
|
||||
--add-data "src/gasflux/gasflux_config.yaml:src/gasflux" \
|
||||
--add-data "API_DOCUMENTATION.md:." \
|
||||
--exclude-module matplotlib \
|
||||
--exclude-module tkinter \
|
||||
server_waitress.py
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to build executable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Build completed successfully!"
|
||||
echo "Executable created: dist/GasFluxAPI"
|
||||
echo ""
|
||||
echo "To run the server:"
|
||||
echo "./dist/GasFluxAPI"
|
||||
echo ""
|
||||
echo "The server will start on http://localhost:5000"
|
||||
@ -1,48 +0,0 @@
|
||||
@echo off
|
||||
echo ========================================
|
||||
echo GasFlux RunExample EXE打包工具 (Windows)
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
cd /d "%~dp0"
|
||||
|
||||
echo [1/4] 检查Python环境...
|
||||
python --version
|
||||
if errorlevel 1 (
|
||||
echo 错误:未找到Python
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [2/4] 升级pip...
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
echo [3/4] 安装PyInstaller...
|
||||
python -m pip install pyinstaller
|
||||
if errorlevel 1 (
|
||||
echo 错误:PyInstaller安装失败
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [4/4] 开始打包...
|
||||
python -m PyInstaller --clean gasflux_run_example.spec
|
||||
if errorlevel 1 (
|
||||
echo 错误:打包失败
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo 打包完成!
|
||||
echo ========================================
|
||||
echo.
|
||||
echo 可执行文件位置:dist\GasFlux_RunExample.exe
|
||||
echo.
|
||||
echo 使用方法:
|
||||
echo GasFlux_RunExample.exe input.xlsx
|
||||
echo GasFlux_RunExample.exe input.xlsx --output result.csv
|
||||
echo GasFlux_RunExample.exe input.xlsx --no-gasflux
|
||||
echo.
|
||||
pause
|
||||
@ -1,48 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "========================================"
|
||||
echo "GasFlux RunExample EXE打包工具 (Linux/Mac)"
|
||||
echo "========================================"
|
||||
echo
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "[1/4] 检查Python环境..."
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "错误:未找到Python3"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 --version
|
||||
|
||||
echo "[2/4] 升级pip..."
|
||||
python3 -m pip install --upgrade pip
|
||||
|
||||
echo "[3/4] 安装PyInstaller..."
|
||||
python3 -m pip install pyinstaller
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "错误:PyInstaller安装失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[4/4] 开始打包..."
|
||||
python3 -m PyInstaller --clean gasflux_run_example.spec
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "错误:打包失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "========================================"
|
||||
echo "打包完成!"
|
||||
echo "========================================"
|
||||
echo
|
||||
echo "可执行文件位置:dist/GasFlux_RunExample"
|
||||
echo
|
||||
echo "使用方法:"
|
||||
echo " ./GasFlux_RunExample input.xlsx"
|
||||
echo " ./GasFlux_RunExample input.xlsx --output result.csv"
|
||||
echo " ./GasFlux_RunExample input.xlsx --no-gasflux"
|
||||
echo
|
||||
30
env.example
Normal file
30
env.example
Normal file
@ -0,0 +1,30 @@
|
||||
# GasFlux Web API - Environment Variables Example
|
||||
# Copy this file to your environment and modify as needed
|
||||
|
||||
# Server Configuration
|
||||
GASFLUX_HOST=0.0.0.0
|
||||
GASFLUX_PORT=5000
|
||||
GASFLUX_DEBUG=false
|
||||
|
||||
# Directory Configuration
|
||||
GASFLUX_UPLOAD_FOLDER=web_api_data/uploads
|
||||
GASFLUX_OUTPUT_FOLDER=web_api_data/outputs
|
||||
|
||||
# File Size Limits (in bytes)
|
||||
GASFLUX_MAX_CONTENT_LENGTH=104857600 # 100MB
|
||||
|
||||
# Logging Configuration
|
||||
GASFLUX_LOG_LEVEL=INFO
|
||||
GASFLUX_LOG_FILE=logs/gasflux_api.log
|
||||
|
||||
# CORS Configuration
|
||||
GASFLUX_CORS_ORIGINS=*
|
||||
|
||||
# Task Management
|
||||
GASFLUX_TASK_CLEANUP_INTERVAL=3600 # 1 hour
|
||||
GASFLUX_MAX_TASK_AGE=86400 # 24 hours
|
||||
|
||||
# Performance Tuning
|
||||
GASFLUX_THREADS=8
|
||||
GASFLUX_CONNECTION_LIMIT=100
|
||||
GASFLUX_CHANNEL_TIMEOUT=300
|
||||
@ -1,52 +0,0 @@
|
||||
output_dir: ./advanced_output
|
||||
|
||||
required_cols:
|
||||
latitude: [-90, 90]
|
||||
longitude: [-180, 180]
|
||||
height_ato: [-100, 500]
|
||||
windspeed: [1, 15]
|
||||
winddir: [0, 360]
|
||||
temperature: [0, 30]
|
||||
pressure: [980, 1030]
|
||||
|
||||
gases:
|
||||
ch4: [1.8, 5.0]
|
||||
co2: [400, 500]
|
||||
|
||||
|
||||
strategies:
|
||||
background: "algorithm"
|
||||
sensor: "insitu"
|
||||
spatial: "curtain"
|
||||
interpolation: "kriging"
|
||||
|
||||
algorithmic_baseline_settings:
|
||||
algorithm: dietrich
|
||||
dietrich:
|
||||
poly_order: 4
|
||||
smooth_half_window: 4
|
||||
|
||||
semivariogram_settings:
|
||||
model: gaussian
|
||||
estimator: cressie
|
||||
n_lags: 15
|
||||
bin_func: even
|
||||
fit_method: lm
|
||||
maxlag: 80
|
||||
tolerance: 15
|
||||
azimuth: 0
|
||||
bandwidth: 25
|
||||
|
||||
ordinary_kriging_settings:
|
||||
min_points: 5
|
||||
max_points: 80
|
||||
grid_resolution: 200
|
||||
min_nodes: 10
|
||||
y_min: 20
|
||||
cut_ground: True
|
||||
|
||||
filters:
|
||||
course_filter:
|
||||
azimuth_filter: 15
|
||||
azimuth_window: 8
|
||||
elevation_filter: 8
|
||||
Binary file not shown.
Binary file not shown.
@ -1,69 +0,0 @@
|
||||
# GasFlux configuration file for basic usage example
|
||||
|
||||
output_dir: ./10m
|
||||
|
||||
# Required columns and their maximum valid ranges
|
||||
required_cols:
|
||||
latitude: [-90, 90]
|
||||
longitude: [-180, 180]
|
||||
height_ato: [0, 50] # meters above takeoff
|
||||
windspeed: [0, 50] # m/s
|
||||
winddir: [0, 360] # degrees
|
||||
temperature: [-50, 60] # degrees Celsius
|
||||
pressure: [900, 1100] # hPa/mb
|
||||
|
||||
# Optional gas columns and their maximum ppm concentration ranges.
|
||||
# Relative concentrations are used, so absolute offset can be incorrect as long as gain and linearity are correct.
|
||||
gases:
|
||||
ch4: [2, 2.5]
|
||||
|
||||
|
||||
strategies:
|
||||
background: "algorithm" # Currently only algorithmic baseline correction (via pybaselines) is supported
|
||||
sensor: "insitu" # Currently only in-situ sensor data is supported
|
||||
spatial: "spiral" # Spatial processing mode: "curtain" and "spiral" are supported
|
||||
interpolation: "kriging" # Currently only kriging interpolation is supported
|
||||
|
||||
# Baseline correction algorithm settings
|
||||
algorithmic_baseline_settings:
|
||||
algorithm: fastchrom
|
||||
fastchrom: {
|
||||
"half_window": 6,
|
||||
"threshold": "custom", #
|
||||
"min_fwhm": ~,
|
||||
"interp_half_window": 3,
|
||||
"smooth_half_window": 3,
|
||||
"weights": ~,
|
||||
"max_iter": 100,
|
||||
"min_length": 2}
|
||||
fabc : {
|
||||
"lam": 10000, # The smoothing parameter. Larger values will create smoother baselines. Default is 1e6.
|
||||
"scale": 10, # The scale at which to calculate the continuous wavelet transform. Should be approximately equal to the index-based full-width-at-half-maximum of the peaks or features in the data. Default is None, which will use half of the value from optimize_window(), which is not always a good value, but at least scales with the number of data points and gives a starting point for tuning the parameter.
|
||||
"diff_order": 2} # The order of the differential matrix. Must be greater than 0. Default is 2 (second order differential matrix). Typical values are 2 or 1.
|
||||
dietrich : {
|
||||
"poly_order": 5,
|
||||
"smooth_half_window": 5,}
|
||||
golotvin : {
|
||||
"half_window": 2,
|
||||
"sections": 10}
|
||||
|
||||
# Kriging settings - aggressively optimized for Spiral mode
|
||||
semivariogram_settings:
|
||||
model: exponential # Changed to exponential model for better circular data fitting
|
||||
estimator: cressie # Robust estimator for variogram calculation
|
||||
n_lags: 50 # Further increased to 50 for better variogram resolution
|
||||
bin_func: even # Even binning function
|
||||
fit_method: lm # Least squares fitting method
|
||||
### Aggressively increased search ranges for circular/spiral data distribution
|
||||
maxlag: 5000 # Dramatically increased to 5000m for comprehensive coverage
|
||||
tolerance: 180 # Increased to 180° to allow full circular search
|
||||
azimuth: 0 # Horizontal direction maintained
|
||||
bandwidth: 300 # Further increased to 300m for maximum search bandwidth
|
||||
# fit_sigma: linear # this should allow for a spatial uncertainty but currently producing bugs
|
||||
ordinary_kriging_settings:
|
||||
min_points: 1 # Reduced to 1 to allow interpolation even with sparse data
|
||||
max_points: 20 # Further reduced to 20 to minimize computational load
|
||||
grid_resolution: 100 # Increased density to 100 for finer interpolation grid
|
||||
min_nodes: 20 # Increased to 20 to ensure sufficient grid nodes
|
||||
y_min: ~ # Automatically determine minimum y value
|
||||
cut_ground: False # Keep ground cutting disabled
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,61 +0,0 @@
|
||||
# GasFlux 基本使用示例配置文件
|
||||
# 这个配置文件演示了最基本的设置,用于处理简单的气体通量数据
|
||||
|
||||
# 输出目录
|
||||
output_dir: ./output
|
||||
|
||||
# 数据验证参数
|
||||
required_cols:
|
||||
latitude: [-90, 90] # 纬度范围
|
||||
longitude: [-180, 180] # 经度范围
|
||||
height_ato: [0, 200] # 相对起飞高度(米)
|
||||
windspeed: [0, 20] # 风速(m/s)
|
||||
winddir: [0, 360] # 风向(度)
|
||||
temperature: [-50, 60] # 温度(°C)
|
||||
pressure: [900, 1100] # 气压(hPa)
|
||||
|
||||
# 气体配置:气体名称和浓度范围(ppmv)
|
||||
gases:
|
||||
ch4: [1.5, 10.0] # 甲烷
|
||||
co2: [300, 500] # 二氧化碳
|
||||
|
||||
# 处理策略
|
||||
strategies:
|
||||
background: "algorithm" # 背景校正方法
|
||||
sensor: "insitu" # 传感器类型
|
||||
spatial: "curtain" # 空间处理模式
|
||||
interpolation: "kriging" # 插值方法
|
||||
|
||||
# 背景校正算法设置
|
||||
algorithmic_baseline_settings:
|
||||
algorithm: fastchrom # 使用FastChrom算法
|
||||
fastchrom:
|
||||
half_window: 3 # 半窗口大小
|
||||
threshold: "custom" # 阈值方法
|
||||
min_fwhm: ~
|
||||
interp_half_window: 2
|
||||
smooth_half_window: 2
|
||||
weights: ~
|
||||
max_iter: 50
|
||||
min_length: 2
|
||||
|
||||
# 克里金插值设置
|
||||
semivariogram_settings:
|
||||
model: spherical # 半变异函数模型
|
||||
estimator: cressie # 估计器
|
||||
n_lags: 10 # 滞后期数
|
||||
bin_func: even # 分箱函数
|
||||
fit_method: lm # 拟合方法
|
||||
maxlag: 50 # 最大滞后距离
|
||||
tolerance: 10 # 方向容差
|
||||
azimuth: 0 # 方位角
|
||||
bandwidth: 15 # 带宽
|
||||
|
||||
# 普通克里金设置
|
||||
ordinary_kriging_settings:
|
||||
min_points: 3 # 最小邻点数
|
||||
max_points: 50 # 最大邻点数
|
||||
grid_resolution: 100 # 网格分辨率
|
||||
min_nodes: 5 # 最小网格节点数
|
||||
y_min: ~ # 最小y值
|
||||
cut_ground: False # 是否切割地面
|
||||
@ -1,820 +0,0 @@
|
||||
timestamp,latitude,longitude,height_ato,windspeed,winddir,temperature,pressure,ch4,course_elevation,course_azimuth
|
||||
2016-02-12 08:34:01,40.349137,115.7855289,11.865000000000009,4.85383,126.121,18.1,957.6,2.28753,8.50882,74.9353
|
||||
2016-02-12 08:34:02,40.349137,115.7855289,11.865000000000009,3.5045,142.744,18.1,957.6,2.27667,8.50882,74.9353
|
||||
2016-02-12 08:34:02,40.349137,115.7855296,9.830000000000041,3.30667,145.626,18.1,957.4,2.28621,7.26866,75.2618
|
||||
2016-02-12 08:34:02,40.3491369,115.7855295,9.830000000000041,3.19669,147.2,18.1,957.4,2.29012,6.6954,75.3488
|
||||
2016-02-12 08:34:02,40.3491366,115.7855289,9.830000000000041,3.1958,146.33,18.1,957.4,2.28968,5.15029,75.4473
|
||||
2016-02-12 08:34:02,40.3491365,115.7855279,9.830000000000041,2.24203,142.349,18.1,957.4,2.27675,3.60147,75.8082
|
||||
2016-02-12 08:34:02,40.3491365,115.7855279,9.830000000000041,2.24203,142.349,18.1,957.4,2.28111,3.60147,75.8082
|
||||
2016-02-12 08:34:02,40.3491362,115.7855267,9.830000000000041,2.65173,132.045,18.1,957.4,2.27628,2.90435,75.7617
|
||||
2016-02-12 08:34:03,40.3491359,115.7855254,9.830000000000041,3.12274,124.934,18.1,957.4,2.26905,1.51899,75.2541
|
||||
2016-02-12 08:34:04,40.3491342,115.7855197,9.482000000000028,3.62267,96.3253,18.1,957.4,2.26865,2.08601,74.3647
|
||||
2016-02-12 08:34:05,40.3491317,115.7855115,6.3040000000000305,3.48429,103.687,18.1,957.4,2.25985,-1.48779,74.6518
|
||||
2016-02-12 08:34:06,40.349131,115.7855068,3.1860000000000355,3.08523,110.241,18.1,957.4,2.25816,-5.50997,78.0309
|
||||
2016-02-12 08:34:07,40.3491309,115.7855052,0.45100000000002183,2.57202,106.603,18.1,957.4,2.26128,-2.11963,61.7648
|
||||
2016-02-12 08:34:08,40.3491307,115.7855046,0.06300000000004502,1.67764,109.782,18.0,957.4,2.26535,-6.78676,47.9709
|
||||
2016-02-12 08:34:09,40.3491332,115.7855083,0.0,1.87728,168.504,18.0,957.4,2.26713,-11.0121,47.0035
|
||||
2016-02-12 08:34:10,40.3491444,115.7855253,0.813000000000045,2.70038,170.113,18.0,957.4,2.27136,-6.98884,46.7738
|
||||
2016-02-12 08:34:11,40.3491573,115.7855439,1.1070000000000277,4.09542,158.538,17.9,957.4,2.26672,-3.50721,45.9275
|
||||
2016-02-12 08:34:12,40.3491708,115.7855627,0.8600000000000136,4.92415,152.325,17.9,957.4,2.26796,-3.67191,47.1102
|
||||
2016-02-12 08:34:13,40.3491838,115.7855814,1.4639999999999986,5.05104,149.272,17.9,957.4,2.26253,-2.6092,47.3419
|
||||
2016-02-12 08:34:14,40.3491963,115.7855993,1.2660000000000196,4.7566,146.774,17.8,957.4,2.25574,-3.68899,47.5405
|
||||
2016-02-12 08:34:15,40.3492086,115.7856167,0.9460000000000264,4.4861,146.945,17.8,957.4,2.27209,-4.73718,47.8124
|
||||
2016-02-12 08:34:16,40.3492212,115.7856339,1.1129999999999995,4.49042,147.234,17.8,957.4,2.2685,-3.71094,47.8571
|
||||
2016-02-12 08:34:17,40.3492333,115.785651,1.330000000000041,4.67508,147.701,17.8,957.4,2.26577,-4.10178,47.8265
|
||||
2016-02-12 08:34:18,40.3492453,115.7856683,0.3660000000000423,4.80069,142.768,17.8,957.4,2.26449,-4.00834,51.3819
|
||||
2016-02-12 08:34:19,40.3492558,115.7856869,0.42900000000003047,4.41849,139.131,17.8,957.4,2.2629,-3.5961,55.2403
|
||||
2016-02-12 08:34:20,40.3492652,115.7857063,0.5040000000000191,4.17348,135.791,17.8,957.4,2.27134,-3.66223,59.0575
|
||||
2016-02-12 08:34:21,40.3492739,115.7857263,0.6690000000000396,4.24922,133.776,17.7,957.4,2.26504,-4.04384,62.0997
|
||||
2016-02-12 08:34:22,40.3492823,115.7857472,0.6270000000000095,4.12397,132.906,17.7,957.4,2.26494,-3.93005,65.1934
|
||||
2016-02-12 08:34:23,40.3492897,115.7857691,0.38800000000003365,3.87817,129.092,17.7,957.4,2.26202,-3.17112,69.0847
|
||||
2016-02-12 08:34:24,40.3492958,115.7857913,0.535000000000025,3.56279,124.29,17.7,957.4,2.2705,-2.8296,73.4975
|
||||
2016-02-12 08:34:25,40.3493007,115.7858139,0.41500000000002046,3.20321,120.437,17.7,957.4,2.26211,-2.58427,74.5562
|
||||
2016-02-12 08:34:26,40.3493053,115.7858363,0.32600000000002183,2.84368,126.241,17.7,957.4,2.27064,-3.00187,74.5266
|
||||
2016-02-12 08:34:27,40.3493102,115.7858587,0.39800000000002456,2.66869,123.302,17.6,957.4,2.26641,-2.85474,74.2713
|
||||
2016-02-12 08:34:28,40.3493153,115.7858814,0.1560000000000059,2.33673,125.416,17.6,957.4,2.2613,-2.58352,74.0617
|
||||
2016-02-12 08:34:29,40.3493205,115.7859044,0.015000000000043201,2.1419,112.039,17.6,957.4,2.27285,-2.4898,74.4684
|
||||
2016-02-12 08:34:30,40.349325,115.7859273,0.023000000000024556,2.12291,112.287,17.6,957.4,2.26546,-2.37166,74.68
|
||||
2016-02-12 08:34:31,40.3493296,115.7859502,0.09500000000002728,2.11867,112.449,17.5,957.4,2.26334,-2.52349,74.727
|
||||
2016-02-12 08:34:32,40.3493339,115.7859728,0.21500000000003183,2.03503,104.255,17.5,957.4,2.25624,-2.35695,77.5604
|
||||
2016-02-12 08:34:33,40.3493375,115.7859955,0.06800000000004047,2.08166,105.808,17.5,957.4,2.26756,-2.18838,81.7064
|
||||
2016-02-12 08:34:34,40.3493398,115.7860185,0.09100000000000819,2.10121,101.349,17.5,957.4,2.27006,-2.16129,86.1149
|
||||
2016-02-12 08:34:35,40.3493408,115.7860419,0.09800000000001319,2.0652,92.3755,17.5,957.4,2.26534,-1.97706,89.1031
|
||||
2016-02-12 08:34:36,40.3493407,115.7860652,0.28400000000004866,2.03562,89.3814,17.5,957.4,2.26928,-1.69654,91.8991
|
||||
2016-02-12 08:34:37,40.3493397,115.7860884,0.16800000000000637,2.01381,86.7262,17.4,957.4,2.26397,-1.49829,95.0727
|
||||
2016-02-12 08:34:38,40.3493378,115.7861116,0.10599999999999454,1.94986,82.7376,17.4,957.4,2.26526,-1.28459,98.4028
|
||||
2016-02-12 08:34:39,40.3493345,115.7861345,0.2300000000000182,1.98893,75.9616,17.4,957.4,2.27142,-1.40811,102.871
|
||||
2016-02-12 08:34:40,40.3493301,115.786157,0.22400000000004638,1.97218,72.7694,17.4,957.4,2.26352,-1.32349,106.444
|
||||
2016-02-12 08:34:41,40.3493248,115.7861794,0.26300000000003365,1.9978,72.1563,17.4,957.4,2.26984,-1.26987,107.053
|
||||
2016-02-12 08:34:42,40.3493192,115.7862018,0.15399999999999636,1.99455,72.3657,17.4,957.4,2.27778,-1.2788,106.81
|
||||
2016-02-12 08:34:43,40.3493138,115.7862243,0.16800000000000637,2.00894,73.2219,17.4,957.4,2.27545,-1.35789,106.736
|
||||
2016-02-12 08:34:44,40.3493084,115.786247,0.23200000000002774,2.01704,73.3629,17.4,957.4,2.26695,-1.14838,107.083
|
||||
2016-02-12 08:34:45,40.3493031,115.7862696,0.14400000000000546,1.99527,72.7411,17.4,957.4,2.26658,-0.891773,107.534
|
||||
2016-02-12 08:34:46,40.3492978,115.7862919,0.14500000000003865,1.9958,72.9977,17.4,957.4,2.26718,-1.23237,107.164
|
||||
2016-02-12 08:34:47,40.3492925,115.7863145,0.20400000000000773,2.00589,73.2645,17.3,957.4,2.27372,-0.841243,107.309
|
||||
2016-02-12 08:34:48,40.3492871,115.7863372,0.11000000000001364,2.00215,73.3395,17.3,957.4,2.26653,-0.756934,106.873
|
||||
2016-02-12 08:34:49,40.3492815,115.7863599,0.16599999999999682,2.00296,69.5246,17.3,957.4,2.27131,-0.454938,110.252
|
||||
2016-02-12 08:34:50,40.3492749,115.7863818,0.27899999999999636,1.97805,64.6886,17.3,957.4,2.25977,-0.706977,114.756
|
||||
2016-02-12 08:34:51,40.349267,115.7864029,0.23900000000003274,1.9369,61.1256,17.3,957.4,2.27473,-0.520325,118.334
|
||||
2016-02-12 08:34:52,40.3492562,115.7864271,0.37700000000000955,1.9232,56.7933,17.3,957.4,2.27288,-0.892102,122.079
|
||||
2016-02-12 08:34:53,40.3492466,115.7864464,0.3160000000000309,1.9402,53.0758,17.3,957.4,2.26006,-0.668285,125.41
|
||||
2016-02-12 08:34:54,40.3492383,115.7864611,0.4070000000000391,1.94647,49.7541,17.3,957.4,2.26102,-0.793088,129.068
|
||||
2016-02-12 08:34:55,40.3492244,115.7864817,0.46200000000004593,1.96366,44.9617,17.3,957.4,2.26623,-0.715602,133.755
|
||||
2016-02-12 08:34:56,40.3492118,115.7864981,0.43200000000001637,1.96034,44.2948,17.3,957.4,2.26446,-0.690077,134.303
|
||||
2016-02-12 08:34:57,40.3491991,115.7865145,0.34700000000003683,1.97744,45.1225,17.3,957.4,2.26972,-0.643342,134.308
|
||||
2016-02-12 08:34:58,40.3491864,115.7865314,0.37700000000000955,1.76954,51.6213,17.3,957.4,2.26913,-0.884507,134.019
|
||||
2016-02-12 08:34:59,40.3491734,115.7865484,0.3170000000000073,1.64919,64.6824,17.3,957.4,2.26522,-0.561871,134.093
|
||||
2016-02-12 08:35:00,40.3491603,115.786565,0.38900000000001,1.53019,22.4667,17.3,957.4,2.27348,-0.283173,139.001
|
||||
2016-02-12 08:35:01,40.3491468,115.7865794,0.3860000000000241,1.66544,16.1733,17.3,957.4,2.25965,-0.622678,146.88
|
||||
2016-02-12 08:35:02,40.349132,115.7865915,0.49600000000003774,1.90088,25.2598,17.3,957.4,2.27091,-0.675699,152.432
|
||||
2016-02-12 08:35:03,40.3491161,115.7866014,0.660000000000025,1.92716,21.0727,17.3,957.4,2.27679,-0.638564,157.896
|
||||
2016-02-12 08:35:04,40.3490997,115.786609,0.6190000000000282,1.94499,15.5196,17.3,957.4,2.2683,-1.18828,163.563
|
||||
2016-02-12 08:35:05,40.3490826,115.7866145,0.6270000000000095,2.06029,5.61009,17.3,957.4,2.26783,-0.849828,170.567
|
||||
2016-02-12 08:35:06,40.349065,115.7866175,0.7410000000000423,2.25531,18.8411,17.3,957.4,2.27565,-1.02481,175.647
|
||||
2016-02-12 08:35:07,40.3490471,115.7866191,0.5990000000000464,2.6897,27.0446,17.3,957.4,2.26251,-0.938632,175.901
|
||||
2016-02-12 08:35:08,40.3490295,115.7866204,0.8120000000000118,3.5706,22.8119,17.3,957.4,2.26682,-1.4553,175.625
|
||||
2016-02-12 08:35:09,40.3490118,115.7866221,0.5160000000000196,3.58223,23.2989,17.3,957.4,2.26834,-1.51818,175.649
|
||||
2016-02-12 08:35:10,40.3489939,115.7866236,0.6400000000000432,3.47975,23.2924,17.3,957.4,2.26655,-1.19849,175.598
|
||||
2016-02-12 08:35:11,40.3489761,115.7866252,0.5400000000000205,3.19407,20.9252,17.3,957.4,2.26637,-0.986002,176.011
|
||||
2016-02-12 08:35:12,40.3489581,115.7866269,0.5540000000000305,2.9534,17.5397,17.3,957.4,2.2573,-1.64013,176.158
|
||||
2016-02-12 08:35:13,40.3489399,115.7866285,0.7060000000000173,3.15458,18.2456,17.3,957.4,2.26939,-0.792553,175.629
|
||||
2016-02-12 08:35:14,40.3489218,115.7866301,0.6890000000000214,3.20536,19.0764,17.3,957.4,2.2633,-1.16851,175.798
|
||||
2016-02-12 08:35:15,40.3489038,115.7866319,0.8970000000000482,3.5823,22.3026,17.3,957.4,2.25549,-0.91357,175.775
|
||||
2016-02-12 08:35:16,40.3488859,115.7866335,0.7669999999999959,3.65156,26.1432,17.3,957.4,2.27439,-1.38495,178.614
|
||||
2016-02-12 08:35:17,40.3488681,115.7866336,0.7250000000000227,3.69689,28.3517,17.2,957.4,2.2692,-1.36653,-178.318
|
||||
2016-02-12 08:35:18,40.3488503,115.7866322,0.8000000000000114,3.65114,28.7382,17.2,957.4,2.26636,-1.90382,-175.758
|
||||
2016-02-12 08:35:19,40.3488325,115.7866297,0.7480000000000473,3.6946,29.076,17.2,957.4,2.27162,-1.64705,-173.079
|
||||
2016-02-12 08:35:20,40.3488148,115.7866265,0.7590000000000146,3.86282,32.9325,17.2,957.4,2.26822,-1.50341,-170.331
|
||||
2016-02-12 08:35:21,40.3487974,115.7866223,0.57000000000005,3.78317,34.2873,17.2,957.4,2.259,-1.77715,-168.664
|
||||
2016-02-12 08:35:22,40.3487801,115.7866172,0.8000000000000114,3.73202,35.6475,17.2,957.4,2.26842,-1.80386,-166.087
|
||||
2016-02-12 08:35:23,40.348763,115.7866112,0.5540000000000305,3.64181,38.9923,17.2,957.4,2.26291,-1.70703,-162.255
|
||||
2016-02-12 08:35:24,40.348746,115.7866039,0.7320000000000277,3.81269,41.8128,17.2,957.4,2.26664,-1.71048,-159.878
|
||||
2016-02-12 08:35:25,40.3487291,115.7865957,0.7630000000000337,3.48729,42.0299,17.2,957.4,2.26374,-1.33685,-159.608
|
||||
2016-02-12 08:35:26,40.3487121,115.7865872,1.0080000000000382,3.75868,43.3489,17.2,957.4,2.26646,-1.55744,-159.91
|
||||
2016-02-12 08:35:27,40.3486957,115.7865788,0.7060000000000173,3.79234,42.8656,17.2,957.4,2.26328,-2.0048,-159.441
|
||||
2016-02-12 08:35:28,40.3486789,115.7865705,0.7350000000000136,4.22793,42.0491,17.2,957.4,2.26176,-1.98951,-159.699
|
||||
2016-02-12 08:35:29,40.3486623,115.7865625,0.7860000000000014,4.27342,41.0633,17.2,957.4,2.2614,-2.18286,-159.799
|
||||
2016-02-12 08:35:30,40.3486421,115.7865527,0.5580000000000496,4.18449,42.0908,17.2,957.4,2.2712,-1.9235,-159.6
|
||||
2016-02-12 08:35:31,40.3486252,115.7865443,0.4430000000000405,4.02941,43.9017,17.2,957.4,2.26236,-1.78458,-159.343
|
||||
2016-02-12 08:35:32,40.3486085,115.7865356,0.4370000000000118,4.02369,47.8521,17.2,957.4,2.26458,-1.82948,-155.262
|
||||
2016-02-12 08:35:33,40.3485927,115.7865251,0.6200000000000045,4.1616,52.7125,17.2,957.4,2.26249,-2.21001,-149.328
|
||||
2016-02-12 08:35:34,40.3485776,115.7865124,0.5920000000000414,4.1949,56.7896,17.2,957.4,2.26734,-1.6575,-143.271
|
||||
2016-02-12 08:35:35,40.3485635,115.7864976,0.6930000000000405,4.27443,63.2883,17.2,957.4,2.27079,-1.35784,-137.443
|
||||
2016-02-12 08:35:36,40.3485507,115.7864814,1.0370000000000346,4.18031,70.4075,17.2,957.4,2.27161,-2.25059,-133.151
|
||||
2016-02-12 08:35:37,40.3485392,115.7864642,1.143000000000029,4.11209,74.1468,17.1,957.4,2.26222,-2.27961,-128.075
|
||||
2016-02-12 08:35:38,40.3485291,115.7864455,1.0510000000000446,4.69329,81.6319,17.1,957.4,2.26785,-2.59902,-121.299
|
||||
2016-02-12 08:35:39,40.3485208,115.7864255,1.2220000000000368,5.02418,87.2004,17.1,957.4,2.26188,-3.57525,-114.178
|
||||
2016-02-12 08:35:40,40.3485143,115.7864042,1.0570000000000164,5.29014,89.51,17.1,957.4,2.25589,-3.38018,-109.557
|
||||
2016-02-12 08:35:41,40.3485083,115.7863817,1.0950000000000273,5.35193,86.5112,17.1,957.4,2.26569,-3.63266,-110.057
|
||||
2016-02-12 08:35:42,40.3485019,115.7863589,0.9279999999999973,5.48287,85.2166,17.1,957.4,2.25651,-3.05251,-109.671
|
||||
2016-02-12 08:35:43,40.3484959,115.7863364,1.3120000000000118,5.33864,85.4933,17.1,957.4,2.2527,-3.46027,-109.986
|
||||
2016-02-12 08:35:44,40.3484896,115.7863142,0.9790000000000418,5.04344,87.0444,17.1,957.4,2.25495,-2.7903,-109.977
|
||||
2016-02-12 08:35:45,40.3484838,115.7862919,0.8730000000000473,5.04691,88.3346,17.1,957.4,2.25385,-2.69067,-109.498
|
||||
2016-02-12 08:35:46,40.3484784,115.7862698,0.7189999999999941,4.85317,86.7704,17.1,957.4,2.25565,-3.02553,-109.5
|
||||
2016-02-12 08:35:47,40.3484727,115.7862479,0.4759999999999991,4.84407,85.5268,17.1,957.4,2.26485,-2.80897,-109.877
|
||||
2016-02-12 08:35:48,40.3484667,115.786226,0.7650000000000432,4.92592,82.6714,17.1,957.4,2.26122,-3.19976,-110.092
|
||||
2016-02-12 08:35:49,40.3484607,115.7862036,0.9590000000000032,4.84169,80.0226,17.1,957.4,2.26519,-3.37566,-109.883
|
||||
2016-02-12 08:35:50,40.3484548,115.7861811,0.9000000000000341,4.92234,79.5242,17.0,957.4,2.25928,-3.44132,-109.953
|
||||
2016-02-12 08:35:51,40.3484488,115.7861583,0.7050000000000409,4.87384,79.8274,17.0,957.4,2.26367,-2.44092,-108.984
|
||||
2016-02-12 08:35:52,40.3484431,115.7861355,1.3520000000000323,4.80479,85.2199,17.0,957.4,2.26016,-2.95146,-104.259
|
||||
2016-02-12 08:35:53,40.3484387,115.7861128,1.2720000000000482,4.68873,88.9,17.0,957.4,2.26904,-3.00915,-99.8611
|
||||
2016-02-12 08:35:54,40.3484358,115.7860899,1.1779999999999973,4.69722,95.5627,17.0,957.4,2.26347,-3.11661,-95.6539
|
||||
2016-02-12 08:35:55,40.3484345,115.7860661,1.05600000000004,4.66468,100.449,17.0,957.4,2.26009,-2.20477,-92.2146
|
||||
2016-02-12 08:35:56,40.3484343,115.7860428,1.3660000000000423,4.72368,105.041,17.0,957.4,2.25921,-2.67096,-88.8258
|
||||
2016-02-12 08:35:57,40.3484347,115.7860198,1.8100000000000023,4.68402,107.835,17.0,957.4,2.25358,-3.15406,-84.749
|
||||
2016-02-12 08:35:58,40.3484367,115.7859975,2.1230000000000473,4.85389,110.385,17.0,957.4,2.26588,-4.38063,-81.1136
|
||||
2016-02-12 08:35:59,40.3484402,115.7859746,1.6659999999999968,5.12074,113.453,17.0,957.4,2.25073,-3.78266,-75.9072
|
||||
2016-02-12 08:36:00,40.3484451,115.7859519,1.5710000000000264,5.51489,114.162,16.9,957.4,2.25645,-3.45966,-73.0747
|
||||
2016-02-12 08:36:01,40.3484506,115.7859298,1.41700000000003,5.60618,114.705,16.9,957.4,2.26324,-3.852,-72.6888
|
||||
2016-02-12 08:36:02,40.3484562,115.7859079,1.4260000000000446,5.9885,113.608,16.9,957.4,2.25939,-4.08462,-72.4005
|
||||
2016-02-12 08:36:03,40.3484618,115.7858859,1.2960000000000491,6.06681,114.146,16.9,957.4,2.26444,-4.33265,-72.7478
|
||||
2016-02-12 08:36:04,40.3484672,115.7858634,1.875,6.16502,113.964,16.9,957.4,2.25122,-4.29353,-72.6671
|
||||
2016-02-12 08:36:05,40.3484723,115.7858407,1.288000000000011,5.83346,114.53,16.9,957.4,2.25211,-4.46077,-72.5697
|
||||
2016-02-12 08:36:06,40.3484779,115.785818,1.2210000000000036,5.89907,113.53,16.9,957.4,2.25987,-4.01207,-72.7203
|
||||
2016-02-12 08:36:07,40.3484833,115.7857951,1.188000000000045,5.87513,112.884,16.9,957.4,2.25998,-3.94424,-72.7445
|
||||
2016-02-12 08:36:08,40.3484887,115.7857725,1.0230000000000246,5.86465,112.761,16.9,957.4,2.25653,-3.75708,-72.6379
|
||||
2016-02-12 08:36:09,40.3484941,115.78575,1.3140000000000214,5.86195,113.211,16.9,957.4,2.26881,-3.84116,-72.7711
|
||||
2016-02-12 08:36:10,40.3484998,115.7857277,1.1030000000000086,5.76417,116.283,16.8,957.4,2.26094,-3.59034,-70.8237
|
||||
2016-02-12 08:36:11,40.3485062,115.785706,1.30600000000004,5.63247,121.963,16.8,957.4,2.26475,-4.2956,-65.0574
|
||||
2016-02-12 08:36:12,40.348514,115.7856854,1.295000000000016,5.60765,127.444,16.8,957.4,2.27279,-4.16794,-59.1853
|
||||
2016-02-12 08:36:13,40.3485233,115.7856661,1.3950000000000387,5.85706,131.455,16.8,957.4,2.24993,-4.41538,-54.0778
|
||||
2016-02-12 08:36:14,40.3485344,115.7856481,1.2980000000000018,5.88649,137.158,16.8,957.4,2.25646,-4.22068,-48.5817
|
||||
2016-02-12 08:36:15,40.3485493,115.7856278,1.413000000000011,5.83127,140.639,16.8,957.4,2.263,-3.64977,-43.7228
|
||||
2016-02-12 08:36:16,40.3485596,115.7856156,1.6090000000000373,5.7416,144.321,16.8,957.4,2.25908,-4.5659,-39.0127
|
||||
2016-02-12 08:36:17,40.3485769,115.7855999,1.2909999999999968,5.95436,149.405,16.7,957.4,2.25161,-3.4582,-31.6394
|
||||
2016-02-12 08:36:18,40.3485927,115.7855881,1.032000000000039,5.88089,152.516,16.7,957.4,2.26682,-3.81466,-26.593
|
||||
2016-02-12 08:36:19,40.3486084,115.7855776,1.410000000000025,5.92927,151.424,16.7,957.4,2.26897,-4.88384,-25.4304
|
||||
2016-02-12 08:36:20,40.3486242,115.7855675,1.4120000000000346,6.06628,150.826,16.7,957.4,2.25404,-4.74525,-25.5442
|
||||
2016-02-12 08:36:21,40.3486401,115.7855574,1.1860000000000355,6.09935,150.336,16.7,957.4,2.25539,-4.647,-25.6555
|
||||
2016-02-12 08:36:22,40.3486565,115.7855475,0.9850000000000136,6.11633,148.496,16.7,957.4,2.26913,-4.63177,-25.7067
|
||||
2016-02-12 08:36:23,40.3486729,115.7855371,0.7600000000000477,5.92629,148.812,16.6,957.4,2.26475,-4.46693,-26.0157
|
||||
2016-02-12 08:36:24,40.3486891,115.7855269,0.7820000000000391,6.07324,148.379,16.6,957.4,2.25667,-4.99766,-25.7595
|
||||
2016-02-12 08:36:25,40.3487052,115.7855169,1.1040000000000418,5.99085,149.5,16.6,957.4,2.25532,-4.78653,-25.4645
|
||||
2016-02-12 08:36:26,40.3487213,115.7855067,0.8840000000000146,6.04219,151.354,16.6,957.4,2.26913,-4.636,-25.7042
|
||||
2016-02-12 08:36:27,40.3487372,115.7854966,0.8730000000000473,5.95603,152.786,16.6,957.4,2.26901,-4.5439,-24.9907
|
||||
2016-02-12 08:36:28,40.348753,115.7854872,0.6620000000000346,6.0444,155.92,16.6,957.4,2.25882,-5.09607,-21.8508
|
||||
2016-02-12 08:36:29,40.3487698,115.7854789,0.7740000000000009,6.23573,157.69,16.6,957.4,2.27421,-4.48643,-17.9076
|
||||
2016-02-12 08:36:30,40.3487874,115.7854718,0.9569999999999936,6.31058,159.216,16.5,957.4,2.25334,-4.42131,-14.4395
|
||||
2016-02-12 08:36:31,40.3488052,115.7854663,0.8700000000000045,6.16818,160.728,16.5,957.4,2.25037,-4.45109,-11.6332
|
||||
2016-02-12 08:36:32,40.3488228,115.785462,0.5060000000000286,6.10734,161.71,16.5,957.4,2.25493,-4.35846,-8.40024
|
||||
2016-02-12 08:36:33,40.3488399,115.7854588,0.799000000000035,6.11898,164.494,16.4,957.4,2.24844,-5.41057,-6.17409
|
||||
2016-02-12 08:36:34,40.3488573,115.7854564,0.742999999999995,6.07141,167.215,16.4,957.4,2.26561,-5.26034,-2.7436
|
||||
2016-02-12 08:36:35,40.3488754,115.7854551,0.6370000000000005,6.0496,171.946,16.4,957.4,2.25455,-4.44777,1.35836
|
||||
2016-02-12 08:36:36,40.3488935,115.785456,0.7320000000000277,5.83808,173.643,16.4,957.4,2.26214,-4.58309,4.52072
|
||||
2016-02-12 08:36:37,40.3489117,115.7854579,0.8480000000000132,5.70385,172.17,16.4,957.4,2.26151,-3.90147,5.07055
|
||||
2016-02-12 08:36:38,40.3489293,115.78546,0.9800000000000182,5.51454,171.791,16.4,957.4,2.25964,-4.5457,5.24711
|
||||
2016-02-12 08:36:39,40.348947,115.7854622,1.1539999999999964,5.33505,173.485,16.4,957.4,2.26292,-4.35318,5.33673
|
||||
2016-02-12 08:36:40,40.3489653,115.7854648,1.0,5.29204,174.852,16.3,957.4,2.25233,-3.87292,4.70492
|
||||
2016-02-12 08:36:41,40.3489836,115.7854674,1.3559999999999945,5.29741,176.258,16.3,957.4,2.25662,-3.21208,5.21414
|
||||
2016-02-12 08:36:42,40.3490015,115.7854698,1.3730000000000473,5.14176,174.023,16.3,957.4,2.25812,-3.51608,5.02582
|
||||
2016-02-12 08:36:43,40.3490189,115.7854713,1.516999999999996,5.12388,174.751,16.2,957.4,2.25788,-4.56814,5.03848
|
||||
2016-02-12 08:36:44,40.3490365,115.7854732,1.4870000000000232,5.10234,176.725,16.2,957.4,2.26351,-3.96444,5.46284
|
||||
2016-02-12 08:36:45,40.3490545,115.7854759,1.3980000000000246,5.09201,176.966,16.2,957.4,2.2583,-3.79534,5.24914
|
||||
2016-02-12 08:36:46,40.3490724,115.7854788,1.6370000000000005,5.11211,176.889,16.2,957.4,2.2544,-3.72235,5.29693
|
||||
2016-02-12 08:36:47,40.3490906,115.785481,1.1280000000000427,4.8991,175.319,16.2,957.4,2.26159,-3.32421,5.01752
|
||||
2016-02-12 08:36:48,40.3491085,115.7854831,0.8170000000000073,4.10668,170.376,16.2,957.4,2.25495,10.1467,5.26258
|
||||
2016-02-12 08:36:49,40.3491158,115.7854839,0.9000000000000341,2.6314,163.343,16.2,957.4,2.26207,0.106344,5.66132
|
||||
2016-02-12 08:36:50,40.349117,115.785484,0.38100000000002865,1.58396,131.609,16.1,957.4,2.26455,-0.625125,5.82946
|
||||
2016-02-12 08:36:51,40.3491165,115.7854841,0.313000000000045,0.960517,119.397,16.1,957.4,2.25937,-1.82616,5.43775
|
||||
2016-02-12 08:36:52,40.3491162,115.7854839,0.5020000000000095,0.136019,56.7013,16.1,957.4,2.26542,-1.73079,5.51902
|
||||
2016-02-12 08:36:53,40.3491157,115.7854835,0.3410000000000082,0.0603939,21.4057,16.1,957.4,2.25628,-1.86747,5.8482
|
||||
2016-02-12 08:36:54,40.3491154,115.7854833,0.19600000000002638,0.0369274,47.6323,16.1,957.4,2.25712,-1.53655,6.2355
|
||||
2016-02-12 08:36:55,40.3491151,115.7854834,0.2470000000000141,0.0871401,79.15,16.1,957.4,2.26202,-1.14965,6.09218
|
||||
2016-02-12 08:36:56,40.3491149,115.7854834,0.08300000000002683,0.393658,71.1975,16.1,957.4,2.25841,-1.21857,5.48189
|
||||
2016-02-12 08:36:57,40.3491148,115.7854834,0.32900000000000773,0.586761,66.23,16.1,957.4,2.27044,-1.31146,5.43551
|
||||
2016-02-12 08:36:58,40.3491145,115.7854834,0.28600000000000136,0.596112,53.5956,16.1,957.4,2.25902,-1.45305,5.54127
|
||||
2016-02-12 08:36:59,40.3491142,115.7854835,0.21200000000004593,0.25565,52.3429,16.1,957.4,2.25571,-1.29331,5.77698
|
||||
2016-02-12 08:37:00,40.3491142,115.7854836,0.25200000000000955,0.0781401,60.0922,16.1,957.4,2.26602,-1.42537,5.60733
|
||||
2016-02-12 08:37:01,40.3491142,115.7854835,0.16100000000000136,0.0230935,155.25,16.1,957.4,2.25023,-1.26584,5.76965
|
||||
2016-02-12 08:37:02,40.349114,115.7854833,0.10099999999999909,0.0296167,138.082,16.1,957.4,2.26859,-0.900714,6.01267
|
||||
2016-02-12 08:37:03,40.349114,115.7854834,0.15500000000002956,0.430488,50.8187,16.1,957.4,2.25236,-1.35083,5.15359
|
||||
2016-02-12 08:37:04,40.3491135,115.7854833,0.48500000000001364,0.980068,84.1409,16.1,957.4,2.26295,-2.14462,6.25571
|
||||
2016-02-12 08:37:05,40.3491133,115.7854835,0.3830000000000382,1.27921,103.371,16.1,957.4,2.25558,-2.13589,6.19036
|
||||
2016-02-12 08:37:06,40.3491131,115.7854836,0.4159999999999968,1.00135,137.031,16.1,957.4,2.26097,-2.52534,5.96649
|
||||
2016-02-12 08:37:07,40.3491134,115.7854836,0.2950000000000159,0.717434,155.678,16.1,957.4,2.26059,-1.63997,5.78432
|
||||
2016-02-12 08:37:08,40.3491138,115.7854835,0.21200000000004593,0.303557,137.275,16.1,957.4,2.25044,-0.967678,5.61883
|
||||
2016-02-12 08:37:09,40.3491137,115.7854835,0.21100000000001273,0.285436,130.174,16.1,957.4,2.25547,-0.834362,5.65676
|
||||
2016-02-12 08:37:10,40.3491134,115.7854835,0.1500000000000341,0.083626,87.1777,16.1,957.4,2.26308,-0.800933,5.2812
|
||||
2016-02-12 08:37:11,40.3491131,115.7854835,0.16700000000003,0.10367,42.9916,16.1,957.4,2.25831,-1.52589,4.76376
|
||||
2016-02-12 08:37:12,40.3491127,115.7854833,0.5340000000000487,0.113468,112.581,16.1,957.4,2.26024,-1.83201,5.26747
|
||||
2016-02-12 08:37:13,40.3491126,115.7854835,0.5110000000000241,0.0976797,81.7381,16.1,957.4,2.2573,-1.50989,5.33285
|
||||
2016-02-12 08:37:14,40.3491124,115.7854837,0.5590000000000259,1.8663,90.5845,16.1,957.4,2.26294,-1.44776,5.84654
|
||||
2016-02-12 08:37:15,40.3491125,115.7854838,1.865000000000009,2.56762,89.8643,16.1,957.4,2.26043,-0.567501,6.1396
|
||||
2016-02-12 08:37:16,40.3491122,115.7854834,4.349000000000046,0.917837,91.5823,16.1,957.6,2.26137,-1.97875,5.82784
|
||||
2016-02-12 08:37:17,40.3491111,115.7854803,5.638000000000034,0.716475,48.2852,16.1,957.6,2.26394,-2.59172,42.491
|
||||
2016-02-12 08:37:18,40.349111,115.7854795,5.432000000000016,0.760453,50.6434,16.1,957.6,2.25914,-6.46643,44.7529
|
||||
2016-02-12 08:37:19,40.3491155,115.785485,5.338999999999999,0.642827,152.992,16.2,957.6,2.25408,-8.15347,44.0491
|
||||
2016-02-12 08:37:20,40.3491267,115.7854981,5.7379999999999995,1.76091,82.2608,16.2,957.6,2.25425,2.78277,45.1
|
||||
2016-02-12 08:37:21,40.3491335,115.7855064,5.6650000000000205,0.881555,11.1025,16.2,957.6,2.26243,3.05661,47.7378
|
||||
2016-02-12 08:37:23,40.3491349,115.7855084,5.471000000000004,0.876476,69.6464,16.2,957.6,2.25292,-11.4077,48.1101
|
||||
2016-02-12 08:37:23,40.3491427,115.7855204,5.588999999999999,1.84099,115.054,16.2,957.6,2.25092,-3.99469,47.4194
|
||||
2016-02-12 08:37:25,40.3491553,115.7855389,5.801000000000045,2.39834,107.803,16.2,957.6,2.25989,-2.57408,47.3755
|
||||
2016-02-12 08:37:26,40.3491683,115.7855575,6.093000000000018,3.37656,124.034,16.2,957.4,2.26015,-2.74762,47.3582
|
||||
2016-02-12 08:37:27,40.3491811,115.785576,6.409000000000049,3.88895,134.223,16.2,957.4,2.25817,-3.03106,47.53
|
||||
2016-02-12 08:37:28,40.3491935,115.7855944,6.5540000000000305,3.92564,147.649,16.2,957.4,2.25108,-3.07153,47.3604
|
||||
2016-02-12 08:37:29,40.3492054,115.7856118,6.573000000000036,4.2193,150.06,16.2,957.4,2.2539,-3.572,47.243
|
||||
2016-02-12 08:37:30,40.3492176,115.7856288,6.5160000000000196,4.64874,149.884,16.2,957.4,2.24965,-3.67273,47.4047
|
||||
2016-02-12 08:37:31,40.3492301,115.7856461,6.145000000000039,4.72077,150.734,16.2,957.4,2.2622,-3.50582,47.5138
|
||||
2016-02-12 08:37:32,40.3492422,115.7856636,6.133000000000038,4.53941,145.099,16.2,957.4,2.25039,-3.6977,50.4106
|
||||
2016-02-12 08:37:33,40.3492531,115.7856817,6.093999999999994,4.65501,139.49,16.2,957.4,2.25039,-4.10876,54.9473
|
||||
2016-02-12 08:37:34,40.3492629,115.785701,6.29400000000004,4.75953,137.043,16.2,957.4,2.26519,-3.8511,58.421
|
||||
2016-02-12 08:37:35,40.349272,115.7857212,6.16700000000003,4.68933,133.723,16.2,957.4,2.25982,-2.84437,61.008
|
||||
2016-02-12 08:37:36,40.3492803,115.7857422,5.970000000000027,3.71239,129.472,16.2,957.6,2.25764,-2.38204,64.3574
|
||||
2016-02-12 08:37:37,40.3492877,115.7857639,5.673000000000002,3.24695,125.042,16.2,957.6,2.25472,-2.49819,68.7895
|
||||
2016-02-12 08:37:38,40.3492935,115.7857858,6.0020000000000095,3.11102,121.256,16.2,957.4,2.26319,-2.82953,72.6732
|
||||
2016-02-12 08:37:39,40.3492985,115.7858079,5.843000000000018,3.55585,127.703,16.2,957.6,2.26381,-3.01438,73.9861
|
||||
2016-02-12 08:37:40,40.3493033,115.78583,5.6860000000000355,3.66656,127.519,16.2,957.6,2.26546,-3.47219,74.2353
|
||||
2016-02-12 08:37:41,40.3493081,115.7858522,5.900000000000034,3.7344,126.386,16.2,957.6,2.26321,-3.65925,74.02
|
||||
2016-02-12 08:37:42,40.3493131,115.7858748,5.757000000000005,3.59181,124.73,16.2,957.6,2.2622,-3.15687,74.24
|
||||
2016-02-12 08:37:43,40.3493182,115.7858977,5.725999999999999,3.52774,122.094,16.2,957.6,2.25546,-3.02432,74.6855
|
||||
2016-02-12 08:37:44,40.349323,115.7859204,5.878000000000043,3.64108,121.595,16.2,957.6,2.25546,-3.44193,74.1486
|
||||
2016-02-12 08:37:45,40.3493279,115.7859431,5.875,4.10979,123.849,16.2,957.6,2.25546,-3.16246,74.2353
|
||||
2016-02-12 08:37:46,40.3493334,115.7859706,5.775000000000034,4.2231,121.878,16.2,957.6,2.25546,-2.85934,77.3826
|
||||
2016-02-12 08:37:47,40.3493371,115.7859941,5.854000000000042,3.38294,116.494,16.2,957.6,2.25546,-1.69435,81.6517
|
||||
2016-02-12 08:37:48,40.349339,115.7860126,5.703000000000031,3.05009,112.988,16.2,957.6,2.25773,-2.59874,84.8266
|
||||
2016-02-12 08:37:49,40.3493403,115.7860404,5.862000000000023,2.45333,104.114,16.2,957.6,2.25121,-2.6631,88.8626
|
||||
2016-02-12 08:37:50,40.3493403,115.7860637,5.515000000000043,2.71353,107.91,16.2,957.6,2.26119,-2.36504,91.8881
|
||||
2016-02-12 08:37:51,40.3493394,115.7860872,5.501000000000033,2.80362,106.534,16.2,957.6,2.272,-2.08923,94.6365
|
||||
2016-02-12 08:37:52,40.3493377,115.7861101,5.75,2.76796,106.874,16.2,957.6,2.26665,-1.91928,98.7676
|
||||
2016-02-12 08:37:53,40.3493346,115.7861332,5.528999999999996,2.30693,90.5027,16.1,957.6,2.27118,-1.50633,102.875
|
||||
2016-02-12 08:37:54,40.3493303,115.7861556,5.57000000000005,2.15987,80.27,16.1,957.6,2.26014,-2.41663,106.747
|
||||
2016-02-12 08:37:55,40.3493251,115.7861782,5.718000000000018,1.98563,72.6664,16.1,957.6,2.26723,-2.04302,107.311
|
||||
2016-02-12 08:37:56,40.3493196,115.7862007,5.552999999999997,2.00151,72.8042,16.1,957.6,2.26513,-2.02087,106.976
|
||||
2016-02-12 08:37:57,40.3493142,115.7862233,5.675000000000011,2.00659,73.3209,16.1,957.6,2.27117,-2.0311,106.897
|
||||
2016-02-12 08:37:58,40.349309,115.786246,5.585000000000036,1.99442,73.2617,16.1,957.6,2.26669,-1.85794,107.051
|
||||
2016-02-12 08:37:59,40.3493037,115.7862686,5.536000000000001,2.00573,73.0795,16.1,957.6,2.26195,-1.67994,106.978
|
||||
2016-02-12 08:38:00,40.3492984,115.7862912,5.532000000000039,2.00435,72.5278,16.1,957.6,2.26446,-1.44107,107.109
|
||||
2016-02-12 08:38:01,40.3492929,115.7863138,5.579000000000008,2.01129,72.6281,16.1,957.6,2.26836,-1.59664,107.211
|
||||
2016-02-12 08:38:02,40.3492874,115.7863364,5.55800000000005,1.98886,72.6863,16.1,957.6,2.2662,-1.67881,106.822
|
||||
2016-02-12 08:38:03,40.3492821,115.7863585,5.632000000000005,1.97927,70.5809,16.1,957.6,2.26912,-1.75376,109.576
|
||||
2016-02-12 08:38:04,40.349276,115.7863801,5.837000000000046,1.97986,64.8357,16.1,957.6,2.27006,-1.97127,114.942
|
||||
2016-02-12 08:38:05,40.3492681,115.7864007,5.823000000000036,1.97842,61.166,16.1,957.6,2.26009,-1.6458,118.698
|
||||
2016-02-12 08:38:06,40.3492592,115.7864208,5.7900000000000205,1.99769,57.8575,16.1,957.6,2.26133,-1.79207,121.582
|
||||
2016-02-12 08:38:07,40.3492495,115.7864403,5.853000000000009,2.00282,54.7015,16.1,957.6,2.26969,-1.57073,124.545
|
||||
2016-02-12 08:38:08,40.349239,115.786459,5.771000000000015,1.9989,50.599,16.1,957.6,2.25815,-1.77694,129.001
|
||||
2016-02-12 08:38:09,40.3492274,115.7864766,5.78000000000003,1.98813,46.3829,16.1,957.6,2.26562,-1.72888,133.366
|
||||
2016-02-12 08:38:10,40.349215,115.7864933,5.866000000000042,1.99488,45.6054,16.1,957.6,2.26581,-2.17574,134.303
|
||||
2016-02-12 08:38:11,40.3492022,115.7865101,5.807000000000016,1.98864,45.893,16.1,957.6,2.2618,-1.81056,134.52
|
||||
2016-02-12 08:38:12,40.3491897,115.7865271,5.6410000000000196,1.99804,46.0968,16.1,957.6,2.26713,-1.72293,134.343
|
||||
2016-02-12 08:38:13,40.3491772,115.7865441,5.715000000000032,2.01774,45.7766,16.2,957.6,2.26881,-1.66066,134.316
|
||||
2016-02-12 08:38:14,40.3491645,115.7865609,5.787000000000035,2.01669,41.1983,16.2,957.6,2.26173,-1.78608,137.866
|
||||
2016-02-12 08:38:15,40.3491509,115.7865759,5.875,1.98778,34.7049,16.2,957.6,2.25985,-1.48482,144.993
|
||||
2016-02-12 08:38:16,40.3491362,115.7865886,5.813000000000045,1.97874,28.2621,16.2,957.6,2.26267,-1.65466,150.873
|
||||
2016-02-12 08:38:17,40.3491205,115.7865992,6.025000000000034,2.26713,25.0355,16.2,957.4,2.2593,-1.70951,156.254
|
||||
2016-02-12 08:38:18,40.3491039,115.7866075,6.006000000000029,2.47573,18.7115,16.2,957.4,2.2718,-1.63082,161.557
|
||||
2016-02-12 08:38:19,40.349087,115.7866139,5.956999999999994,2.56727,8.6849,16.2,957.6,2.26504,-1.88711,168.928
|
||||
2016-02-12 08:38:20,40.3490696,115.7866172,6.229000000000042,2.75719,0.527441,16.2,957.4,2.26636,-1.99268,174.757
|
||||
2016-02-12 08:38:21,40.3490516,115.7866186,6.1370000000000005,3.24292,2.39415,16.2,957.4,2.26015,-2.09729,175.563
|
||||
2016-02-12 08:38:22,40.3490337,115.7866199,6.097000000000037,3.42724,3.36974,16.2,957.4,2.26113,-2.11005,175.792
|
||||
2016-02-12 08:38:23,40.3490159,115.7866215,6.021000000000015,3.56703,2.19504,16.2,957.4,2.26318,-2.58626,176.337
|
||||
2016-02-12 08:38:24,40.3489978,115.7866231,6.031000000000006,2.98014,0.730891,16.2,957.4,2.2592,-2.18878,176.036
|
||||
2016-02-12 08:38:25,40.3489799,115.7866245,6.050000000000011,2.8769,1.18416,16.2,957.4,2.25769,-2.45616,175.722
|
||||
2016-02-12 08:38:26,40.348962,115.7866262,6.004000000000019,2.97136,0.474847,16.2,957.4,2.25814,-2.30747,176.164
|
||||
2016-02-12 08:38:27,40.348944,115.7866277,6.004000000000019,3.28611,0.877931,16.2,957.4,2.269,-2.20202,176.154
|
||||
2016-02-12 08:38:28,40.3489259,115.7866295,5.921000000000049,3.28373,1.71368,16.2,957.6,2.26907,-2.02838,175.744
|
||||
2016-02-12 08:38:29,40.3489042,115.786632,5.921000000000049,3.18593,0.556809,16.2,957.6,2.26987,-1.73959,176.318
|
||||
2016-02-12 08:38:30,40.3488863,115.7866332,5.9430000000000405,3.17403,2.17954,16.2,957.6,2.26099,-1.88535,178.499
|
||||
2016-02-12 08:38:31,40.3488686,115.7866333,6.1370000000000005,3.17068,5.38026,16.2,957.4,2.25922,-2.30753,-178.254
|
||||
2016-02-12 08:38:32,40.3488508,115.786632,6.119000000000028,2.97947,7.49582,16.2,957.4,2.26828,-1.80747,-175.54
|
||||
2016-02-12 08:38:33,40.348833,115.7866299,6.079000000000008,2.76366,9.47064,16.2,957.4,2.2712,-2.05905,-172.77
|
||||
2016-02-12 08:38:34,40.3488154,115.7866269,6.048000000000002,2.80114,12.3371,16.2,957.4,2.26121,-2.1765,-171.028
|
||||
2016-02-12 08:38:35,40.3487978,115.7866228,5.961000000000013,2.38589,13.145,16.2,957.6,2.25807,-1.81427,-168.985
|
||||
2016-02-12 08:38:36,40.3487802,115.7866181,5.861000000000047,2.69336,16.9089,16.2,957.6,2.26228,-1.41431,-166.704
|
||||
2016-02-12 08:38:37,40.3487628,115.7866124,6.129000000000019,3.06392,23.064,16.2,957.4,2.26631,-1.84058,-163.542
|
||||
2016-02-12 08:38:38,40.3487457,115.7866051,6.288000000000011,3.4603,28.1621,16.2,957.4,2.26631,-1.43673,-160.633
|
||||
2016-02-12 08:38:39,40.3487288,115.7865968,6.198000000000036,3.43651,28.6348,16.2,957.4,2.26631,-1.62867,-159.419
|
||||
2016-02-12 08:38:40,40.348712,115.7865883,5.991000000000042,2.75678,26.6074,16.2,957.6,2.26457,-0.846973,-159.113
|
||||
2016-02-12 08:38:41,40.3486952,115.7865797,6.177000000000021,2.54112,26.0398,16.2,957.4,2.26195,-1.4228,-160.025
|
||||
2016-02-12 08:38:42,40.3486785,115.7865715,6.125,2.77843,30.673,16.2,957.4,2.27229,-1.28339,-158.953
|
||||
2016-02-12 08:38:43,40.3486617,115.7865631,5.989000000000033,2.89037,31.4589,16.2,957.6,2.26166,-1.72081,-159.983
|
||||
2016-02-12 08:38:44,40.3486446,115.7865549,6.1059999999999945,3.4807,38.7283,16.2,957.4,2.2613,-0.817808,-159.645
|
||||
2016-02-12 08:38:45,40.3486278,115.7865468,6.263000000000034,3.21584,39.218,16.2,957.4,2.25894,-1.0708,-159.434
|
||||
2016-02-12 08:38:46,40.3486114,115.7865384,6.3799999999999955,3.20978,42.7982,16.3,957.4,2.24972,-1.54731,-156.522
|
||||
2016-02-12 08:38:47,40.3485958,115.7865289,6.196000000000026,3.13252,47.861,16.3,957.4,2.25409,-2.09613,-150.524
|
||||
2016-02-12 08:38:48,40.3485808,115.7865168,6.437000000000012,3.67395,54.5351,16.3,957.4,2.2518,-2.04851,-144.217
|
||||
2016-02-12 08:38:49,40.348567,115.7865025,6.314999999999998,3.8837,60.8641,16.3,957.4,2.2588,-2.03042,-138.723
|
||||
2016-02-12 08:38:50,40.3485541,115.7864867,6.55600000000004,4.24575,66.9797,16.3,957.4,2.25364,-2.63416,-133.698
|
||||
2016-02-12 08:38:51,40.3485426,115.7864697,6.711000000000013,4.50037,69.3373,16.3,957.4,2.26174,-3.24088,-128.675
|
||||
2016-02-12 08:38:52,40.3485322,115.7864511,6.847000000000037,4.77316,73.4454,16.3,957.4,2.26407,-3.37251,-123.113
|
||||
2016-02-12 08:38:53,40.3485233,115.786431,6.723000000000013,5.25047,74.79,16.3,957.4,2.2571,-3.46872,-116.851
|
||||
2016-02-12 08:38:54,40.3485158,115.7864097,6.77800000000002,5.24579,78.8962,16.3,957.4,2.26562,-2.75706,-110.608
|
||||
2016-02-12 08:38:55,40.3485097,115.7863879,6.963000000000022,5.36328,78.6499,16.3,957.4,2.263,-3.80411,-109.608
|
||||
2016-02-12 08:38:56,40.3485038,115.7863656,6.785000000000025,5.18284,77.9227,16.3,957.4,2.24412,-3.57529,-109.918
|
||||
2016-02-12 08:38:57,40.3484977,115.7863432,6.955000000000041,5.49547,77.0321,16.3,957.4,2.25077,-3.53406,-110.109
|
||||
2016-02-12 08:38:58,40.3484917,115.7863208,6.76400000000001,5.50554,76.9921,16.3,957.4,2.25282,-3.11968,-109.696
|
||||
2016-02-12 08:38:59,40.3484855,115.7862986,7.1860000000000355,5.67789,76.452,16.3,957.4,2.25288,-3.84746,-109.962
|
||||
2016-02-12 08:39:00,40.3484793,115.7862769,7.2760000000000105,5.58245,78.3723,16.3,957.4,2.25248,-4.04632,-109.703
|
||||
2016-02-12 08:39:01,40.3484733,115.7862548,7.010000000000048,5.50281,81.2427,16.3,957.4,2.25681,-3.7117,-109.605
|
||||
2016-02-12 08:39:02,40.3484675,115.7862318,6.583000000000027,5.4681,80.5694,16.3,957.4,2.25373,-2.64838,-110.026
|
||||
2016-02-12 08:39:03,40.3484615,115.7862087,6.435000000000002,5.24186,80.0162,16.3,957.4,2.26402,-2.47181,-110.066
|
||||
2016-02-12 08:39:04,40.3484553,115.7861861,6.744000000000028,4.78997,78.1177,16.3,957.4,2.25976,-3.00221,-110.003
|
||||
2016-02-12 08:39:05,40.3484491,115.7861637,6.635000000000048,4.66952,79.3902,16.3,957.4,2.25965,-2.87573,-109.383
|
||||
2016-02-12 08:39:06,40.3484432,115.7861416,6.846000000000004,4.6271,83.315,16.2,957.4,2.25455,-2.74948,-106.217
|
||||
2016-02-12 08:39:07,40.3484387,115.7861197,7.13900000000001,4.8648,90.499,16.2,957.4,2.25685,-3.80011,-100.807
|
||||
2016-02-12 08:39:08,40.348436,115.7860971,7.078000000000031,5.13107,93.7453,16.2,957.4,2.25624,-3.61118,-96.6337
|
||||
2016-02-12 08:39:09,40.3484346,115.7860739,6.874000000000024,5.30274,96.2317,16.2,957.4,2.25331,-3.42438,-93.2439
|
||||
2016-02-12 08:39:10,40.3484343,115.7860502,6.77800000000002,5.30424,98.1641,16.2,957.4,2.2591,-3.43879,-89.3696
|
||||
2016-02-12 08:39:11,40.3484346,115.7860267,6.948000000000036,5.36689,101.168,16.2,957.4,2.26334,-3.13154,-85.9754
|
||||
2016-02-12 08:39:12,40.3484361,115.7860039,6.992999999999995,5.36225,105.602,16.2,957.4,2.26374,-3.71979,-81.9432
|
||||
2016-02-12 08:39:13,40.3484392,115.7859812,6.986000000000047,5.58163,111.056,16.2,957.4,2.27043,-3.86594,-76.8084
|
||||
2016-02-12 08:39:14,40.3484453,115.7859542,7.071000000000026,5.59064,113.182,16.2,957.4,2.26144,-3.68614,-72.9019
|
||||
2016-02-12 08:39:15,40.3484508,115.7859315,6.8180000000000405,5.49402,114.441,16.2,957.4,2.26241,-3.7118,-72.5991
|
||||
2016-02-12 08:39:16,40.3484563,115.7859087,6.723000000000013,5.288,114.232,16.2,957.4,2.2694,-3.66929,-72.6022
|
||||
2016-02-12 08:39:17,40.3484619,115.785886,6.861000000000047,5.0759,114.328,16.2,957.4,2.27796,-3.37821,-72.5735
|
||||
2016-02-12 08:39:18,40.3484673,115.7858638,6.777000000000044,4.85226,114.202,16.2,957.4,2.2586,-3.65391,-72.617
|
||||
2016-02-12 08:39:19,40.3484724,115.7858417,6.831000000000017,4.95735,114.591,16.2,957.4,2.26003,-3.9336,-72.5228
|
||||
2016-02-12 08:39:20,40.3484777,115.7858195,6.79400000000004,5.19502,114.403,16.2,957.4,2.26938,-3.7664,-72.49
|
||||
2016-02-12 08:39:21,40.348483,115.7857968,6.8910000000000196,5.40131,114.221,16.2,957.4,2.26292,-3.57338,-72.637
|
||||
2016-02-12 08:39:22,40.3484882,115.785774,6.80800000000005,5.49249,113.991,16.2,957.4,2.25832,-3.62734,-72.9702
|
||||
2016-02-12 08:39:23,40.3484933,115.7857514,6.855000000000018,5.30347,113.598,16.2,957.4,2.26829,-3.95096,-72.8731
|
||||
2016-02-12 08:39:24,40.3484986,115.7857289,7.079000000000008,5.39845,115.741,16.2,957.4,2.26638,-3.72339,-70.6984
|
||||
2016-02-12 08:39:25,40.3485052,115.7857071,6.997000000000014,5.59344,120.748,16.2,957.4,2.25883,-3.71214,-65.1554
|
||||
2016-02-12 08:39:26,40.3485135,115.7856866,6.882000000000005,5.57928,126.78,16.2,957.4,2.27045,-3.56952,-59.0728
|
||||
2016-02-12 08:39:27,40.3485232,115.7856676,7.5540000000000305,5.53672,130.762,16.2,957.4,2.26442,-4.75008,-53.7121
|
||||
2016-02-12 08:39:28,40.3485341,115.7856494,7.011000000000024,5.59819,132.478,16.2,957.4,2.27287,-4.20102,-49.5037
|
||||
2016-02-12 08:39:29,40.3485461,115.7856316,6.975999999999999,5.6987,136.872,16.1,957.4,2.27001,-3.76073,-44.3638
|
||||
2016-02-12 08:39:30,40.3485592,115.7856157,6.80600000000004,5.66211,141.975,16.1,957.4,2.25656,-4.27646,-39.2167
|
||||
2016-02-12 08:39:31,40.3485731,115.7856017,6.959000000000003,5.84401,147.888,16.1,957.4,2.26741,-4.22084,-33.5571
|
||||
2016-02-12 08:39:32,40.348588,115.7855893,6.881000000000029,5.84942,153.684,16.1,957.4,2.26479,-4.14474,-27.6361
|
||||
2016-02-12 08:39:33,40.348604,115.7855792,7.069000000000017,5.77161,153.734,16.1,957.4,2.25661,-4.20067,-25.4868
|
||||
2016-02-12 08:39:34,40.3486205,115.7855695,6.948000000000036,5.68293,151.175,16.1,957.4,2.26653,-4.28749,-25.5627
|
||||
2016-02-12 08:39:35,40.3486369,115.7855593,6.831999999999994,5.59499,149.507,16.1,957.4,2.26059,-3.60083,-25.7786
|
||||
2016-02-12 08:39:36,40.3486532,115.785549,6.910000000000025,5.34526,148.245,16.1,957.4,2.25904,-3.79517,-25.4626
|
||||
2016-02-12 08:39:37,40.3486694,115.7855391,6.90300000000002,5.2722,148.018,16.1,957.4,2.24588,-4.47698,-26.0671
|
||||
2016-02-12 08:39:38,40.3486856,115.7855286,6.662000000000035,5.58787,149.531,16.1,957.4,2.26247,-3.74773,-25.3043
|
||||
2016-02-12 08:39:39,40.348702,115.7855183,6.588000000000022,5.58677,149.414,16.1,957.4,2.2546,-3.41345,-25.3933
|
||||
2016-02-12 08:39:40,40.3487186,115.7855084,6.819000000000017,5.39394,150.467,16.1,957.4,2.26018,-3.45871,-25.8609
|
||||
2016-02-12 08:39:41,40.3487348,115.785498,6.869000000000028,5.13558,149.501,16.1,957.4,2.25899,-3.54752,-25.3197
|
||||
2016-02-12 08:39:42,40.3487508,115.7854881,7.076999999999998,5.12994,151.902,16.0,957.4,2.2604,-3.27375,-23.0354
|
||||
2016-02-12 08:39:43,40.348767,115.7854795,7.107000000000028,5.24712,156.111,16.0,957.4,2.25978,-4.17538,-19.0296
|
||||
2016-02-12 08:39:44,40.3487838,115.7854726,7.342000000000041,5.14911,158.067,16.0,957.4,2.26895,-3.91688,-14.9458
|
||||
2016-02-12 08:39:45,40.3488012,115.7854673,7.123000000000047,5.12797,160.258,16.0,957.4,2.25685,-3.97569,-12.4655
|
||||
2016-02-12 08:39:46,40.3488187,115.7854626,7.0470000000000255,5.25077,161.458,16.0,957.4,2.2575,-3.93482,-9.47375
|
||||
2016-02-12 08:39:47,40.3488361,115.7854589,6.831000000000017,5.34247,163.638,16.0,957.4,2.2747,-3.97845,-6.77187
|
||||
2016-02-12 08:39:48,40.3488535,115.7854563,6.91700000000003,5.35046,168.762,16.0,957.4,2.2638,-4.43388,-3.3938
|
||||
2016-02-12 08:39:49,40.3488712,115.7854556,7.05800000000005,5.36638,172.552,16.0,957.4,2.24989,-4.01909,0.499612
|
||||
2016-02-12 08:39:50,40.3488894,115.7854567,6.927999999999997,5.18338,176.719,16.0,957.4,2.25543,-2.9186,4.27917
|
||||
2016-02-12 08:39:51,40.3489075,115.7854591,6.824000000000012,4.74102,174.68,16.0,957.4,2.26294,-3.10757,5.12577
|
||||
2016-02-12 08:39:52,40.3489253,115.7854614,6.846000000000004,4.61108,173.089,16.0,957.4,2.26023,-3.50273,5.24859
|
||||
2016-02-12 08:39:53,40.3489432,115.7854633,6.754000000000019,4.60728,171.857,16.0,957.4,2.24849,-3.4541,5.02424
|
||||
2016-02-12 08:39:54,40.348961,115.7854649,6.802000000000021,4.80725,173.53,16.0,957.4,2.24994,-3.31187,5.23906
|
||||
2016-02-12 08:39:55,40.3489786,115.7854669,6.7690000000000055,4.84929,175.141,16.0,957.4,2.26774,-3.80584,4.93247
|
||||
2016-02-12 08:39:56,40.3489961,115.7854684,6.984000000000037,4.82154,177.285,16.0,957.4,2.25052,-4.3044,6.02525
|
||||
2016-02-12 08:39:57,40.3490136,115.7854712,7.0750000000000455,4.97235,176.681,16.0,957.4,2.26103,-4.44092,4.73105
|
||||
2016-02-12 08:39:58,40.3490352,115.7854737,7.221000000000004,5.17525,176.871,16.0,957.4,2.24897,-4.50248,5.03347
|
||||
2016-02-12 08:39:59,40.3490496,115.7854757,7.2830000000000155,5.31328,175.68,15.9,957.4,2.2604,-4.74577,5.08683
|
||||
2016-02-12 08:40:00,40.3490715,115.7854787,6.9150000000000205,5.36721,173.39,15.9,957.4,2.25116,-3.63533,5.45957
|
||||
2016-02-12 08:40:01,40.3490892,115.7854806,7.02800000000002,5.49745,171.838,15.9,957.4,2.24886,-5.20438,5.31366
|
||||
2016-02-12 08:40:02,40.3491065,115.7854825,7.034000000000049,4.96294,169.028,15.9,957.4,2.25694,8.55605,4.86752
|
||||
2016-02-12 08:40:03,40.3491136,115.7854833,7.170000000000016,3.77454,166.109,15.9,957.4,2.2609,-2.98443,5.3243
|
||||
2016-02-12 08:40:04,40.349114,115.7854833,6.729000000000042,2.51774,161.1,15.9,957.4,2.25686,-2.8505,5.30476
|
||||
2016-02-12 08:40:05,40.3491134,115.7854828,6.692000000000007,2.18392,157.804,15.9,957.4,2.255,-3.45238,5.31065
|
||||
2016-02-12 08:40:06,40.3491132,115.7854827,6.604000000000042,1.60109,157.059,15.9,957.4,2.25357,-2.80026,5.5161
|
||||
2016-02-12 08:40:07,40.3491134,115.7854827,6.537000000000035,2.12836,156.218,15.9,957.4,2.25833,-2.45733,5.39147
|
||||
2016-02-12 08:40:08,40.3491137,115.7854829,6.403999999999996,2.12331,158.872,15.9,957.4,2.25188,-2.14385,5.2223
|
||||
2016-02-12 08:40:09,40.3491142,115.7854831,6.421000000000049,1.8301,160.043,15.9,957.4,2.25406,-2.07424,4.97781
|
||||
2016-02-12 08:40:10,40.3491147,115.785483,6.369000000000028,1.81565,164.795,15.9,957.4,2.25066,-2.04528,5.5712
|
||||
2016-02-12 08:40:11,40.349115,115.785483,6.395000000000039,1.20916,168.073,15.9,957.4,2.25142,-2.12568,5.6004
|
||||
2016-02-12 08:40:12,40.3491152,115.785483,6.479000000000042,0.923121,169.145,15.9,957.4,2.25772,-2.39996,5.49499
|
||||
2016-02-12 08:40:13,40.3491156,115.7854834,6.295000000000016,0.112622,176.925,15.9,957.4,2.2419,-0.992479,5.52595
|
||||
2016-02-12 08:40:14,40.3491156,115.7854835,5.983000000000004,0.104385,137.479,15.9,957.6,2.24902,-2.09931,4.83266
|
||||
2016-02-12 08:40:15,40.3491156,115.7854831,6.322000000000003,0.0972996,135.981,15.9,957.4,2.25507,-2.44217,5.66033
|
||||
2016-02-12 08:40:16,40.3491157,115.785483,6.293000000000006,0.399473,144.672,15.9,957.4,2.24642,-1.95636,5.97701
|
||||
2016-02-12 08:40:17,40.3491159,115.7854832,6.54200000000003,0.419162,134.864,15.9,957.4,2.25464,-2.12978,4.8956
|
||||
2016-02-12 08:40:18,40.3491159,115.7854832,6.412000000000035,0.0974758,162.941,15.9,957.4,2.25094,-2.48262,5.51442
|
||||
2016-02-12 08:40:19,40.349116,115.7854833,6.161000000000001,0.109608,160.361,15.9,957.4,2.25091,-1.41836,5.54454
|
||||
2016-02-12 08:40:20,40.3491161,115.7854831,6.199000000000012,0.0171219,103.985,15.9,957.4,2.24909,-1.11168,5.4494
|
||||
2016-02-12 08:40:21,40.349116,115.7854829,6.218000000000018,0.0288269,128.137,15.9,957.4,2.25383,-1.09086,5.29294
|
||||
2016-02-12 08:40:22,40.349116,115.7854829,6.281000000000006,0.0365605,136.308,15.9,957.4,2.25341,-1.30006,5.35584
|
||||
2016-02-12 08:40:23,40.349116,115.7854829,6.138000000000034,0.300536,142.642,15.9,957.4,2.24777,-1.31523,5.10688
|
||||
2016-02-12 08:40:24,40.3491159,115.7854831,6.272000000000048,0.362359,104.119,15.9,957.4,2.2515,-1.26287,5.34579
|
||||
2016-02-12 08:40:25,40.3491158,115.7854836,6.330000000000041,0.57932,67.1366,15.9,957.4,2.25599,-1.2537,5.10477
|
||||
2016-02-12 08:40:26,40.349116,115.7854837,6.2590000000000146,0.293271,61.2633,15.9,957.4,2.25032,-1.35474,4.62019
|
||||
2016-02-12 08:40:27,40.3491159,115.7854839,6.537000000000035,1.43408,87.8411,15.9,957.4,2.24641,-1.43189,4.81176
|
||||
2016-02-12 08:40:28,40.3491158,115.7854845,7.9360000000000355,2.6621,90.2237,15.9,957.4,2.25034,-1.61754,4.72443
|
||||
2016-02-12 08:40:29,40.349116,115.7854846,10.175000000000011,1.31573,89.1393,16.0,957.6,2.24564,-1.20717,4.9345
|
||||
2016-02-12 08:40:30,40.3491154,115.7854833,11.519000000000005,0.487783,84.9451,16.0,957.6,2.23793,1.226,32.3446
|
||||
2016-02-12 08:40:31,40.3491146,115.7854801,11.199000000000012,0.888404,113.077,16.0,957.6,2.24013,-0.910583,49.0509
|
||||
2016-02-12 08:40:32,40.3491176,115.7854848,11.338000000000022,1.21106,161.686,16.0,957.6,2.24855,-8.71139,49.124
|
||||
2016-02-12 08:40:33,40.349127,115.7854984,11.410000000000025,1.15788,160.732,16.0,957.6,2.24702,2.53409,49.1037
|
||||
2016-02-12 08:40:34,40.3491327,115.785507,11.30600000000004,1.57306,101.28,16.0,957.6,2.23904,3.0513,47.7375
|
||||
2016-02-12 08:40:35,40.3491341,115.7855092,11.441000000000031,1.65924,120.583,16.0,957.6,2.23278,-11.256,47.7951
|
||||
2016-02-12 08:40:36,40.3491421,115.7855205,11.745000000000005,2.09369,165.994,16.0,957.6,2.25014,-5.38932,47.3602
|
||||
2016-02-12 08:40:37,40.3491545,115.7855384,11.939999999999998,2.19563,170.682,16.0,957.6,2.25111,-3.70094,47.3836
|
||||
2016-02-12 08:40:38,40.3491676,115.7855572,12.343999999999994,2.72588,175.629,16.0,957.4,2.24778,-3.39111,47.5684
|
||||
2016-02-12 08:40:39,40.3491805,115.7855757,12.349000000000046,3.77058,170.431,16.0,957.4,2.23919,-2.92421,47.3679
|
||||
2016-02-12 08:40:40,40.3491932,115.7855932,12.30400000000003,4.32723,164.713,16.0,957.4,2.24849,-3.69776,47.2196
|
||||
2016-02-12 08:40:41,40.3492058,115.7856107,12.257000000000005,4.97482,149.719,16.0,957.4,2.23899,-4.2628,46.9518
|
||||
2016-02-12 08:40:42,40.3492184,115.7856285,12.064999999999998,5.01454,148.244,16.0,957.4,2.24654,-3.29362,47.9102
|
||||
2016-02-12 08:40:43,40.3492309,115.7856463,11.847000000000037,4.75528,148.534,16.0,957.6,2.2461,-2.87623,47.2497
|
||||
2016-02-12 08:40:44,40.3492429,115.7856634,11.926000000000045,4.45324,145.688,16.0,957.6,2.24807,-3.24547,50.6289
|
||||
2016-02-12 08:40:45,40.3492538,115.7856819,11.890000000000043,4.37085,139.85,16.0,957.6,2.24435,-2.29374,55.1856
|
||||
2016-02-12 08:40:46,40.3492636,115.7857015,11.858000000000004,4.45991,136.808,16.0,957.6,2.23915,-2.55544,58.6457
|
||||
2016-02-12 08:40:47,40.3492726,115.7857216,11.834000000000003,4.44533,135.258,16.0,957.6,2.25354,-2.42264,61.5678
|
||||
2016-02-12 08:40:48,40.3492809,115.7857425,11.573000000000036,4.17545,135.351,16.0,957.6,2.2454,-2.13607,64.4989
|
||||
2016-02-12 08:40:49,40.3492883,115.7857639,11.456000000000017,3.50478,129.539,16.0,957.6,2.23091,-1.86106,68.0806
|
||||
2016-02-12 08:40:50,40.3492945,115.7857856,11.674000000000035,3.04936,123.522,16.0,957.6,2.24878,-2.12556,72.5683
|
||||
2016-02-12 08:40:51,40.3492995,115.7858078,11.607000000000028,2.59356,118.042,16.0,957.6,2.24837,-2.43165,74.4098
|
||||
2016-02-12 08:40:52,40.349304,115.7858305,11.578000000000031,2.53347,125.879,16.0,957.6,2.24868,-2.31417,74.0187
|
||||
2016-02-12 08:40:54,40.3493085,115.7858534,11.64100000000002,2.66835,133.005,16.0,957.6,2.25345,-2.44876,73.9301
|
||||
2016-02-12 08:40:55,40.3493133,115.7858761,11.571000000000026,2.67835,132.957,16.0,957.6,2.24898,-2.21838,74.0699
|
||||
2016-02-12 08:40:56,40.3493182,115.7858987,11.422000000000025,2.77779,137.606,16.0,957.6,2.24687,-2.05583,74.0956
|
||||
2016-02-12 08:40:57,40.3493232,115.7859214,11.383000000000038,2.65526,125.47,16.0,957.6,2.25382,-2.1098,73.9022
|
||||
2016-02-12 08:40:58,40.3493284,115.7859437,11.393000000000029,2.57385,121.79,16.0,957.6,2.25263,-2.48862,74.428
|
||||
2016-02-12 08:40:59,40.3493328,115.7859659,11.778999999999996,2.60091,118.797,16.0,957.6,2.24775,-2.6614,76.9316
|
||||
2016-02-12 08:41:00,40.3493363,115.7859886,11.846000000000004,2.80531,113.214,16.0,957.6,2.25006,-2.35789,81.2004
|
||||
2016-02-12 08:41:01,40.3493386,115.7860116,11.75200000000001,3.46673,114.085,16.0,957.6,2.2521,-2.83111,84.5953
|
||||
2016-02-12 08:41:02,40.3493399,115.7860352,11.573000000000036,3.45173,111.082,16.0,957.6,2.24462,-2.19087,88.0719
|
||||
2016-02-12 08:41:03,40.3493402,115.7860585,11.668000000000006,3.17838,106.4,16.0,957.6,2.23597,-2.32365,91.6851
|
||||
2016-02-12 08:41:04,40.3493394,115.7860821,11.456999999999994,2.92245,104.091,16.0,957.6,2.25143,-1.39402,94.5082
|
||||
2016-02-12 08:41:05,40.3493376,115.7861056,11.509000000000015,2.69757,107.508,16.0,957.6,2.24626,-1.14534,97.7257
|
||||
2016-02-12 08:41:06,40.349335,115.786129,11.551000000000045,2.14221,105.163,16.0,957.6,2.24878,-1.12488,101.724
|
||||
2016-02-12 08:41:07,40.3493308,115.7861515,11.583000000000027,1.70315,97.0246,16.0,957.6,2.24965,-1.2808,105.324
|
||||
2016-02-12 08:41:08,40.3493256,115.7861737,11.629999999999995,1.73263,82.1027,16.0,957.6,2.24099,-1.10128,106.318
|
||||
2016-02-12 08:41:09,40.3493203,115.786196,11.79000000000002,1.88123,77.929,16.0,957.6,2.25065,-0.890223,106.882
|
||||
2016-02-12 08:41:10,40.3493151,115.7862185,11.629999999999995,2.02174,72.4556,16.0,957.6,2.24402,-1.06764,106.757
|
||||
2016-02-12 08:41:11,40.3493095,115.7862408,11.56800000000004,1.92404,75.0366,16.0,957.6,2.25262,-1.81658,106.7
|
||||
2016-02-12 08:41:12,40.3493042,115.7862632,11.723000000000013,1.96386,76.6269,16.1,957.6,2.24628,-2.08718,106.681
|
||||
2016-02-12 08:41:13,40.3492993,115.7862859,11.694000000000017,1.97875,76.6054,16.1,957.6,2.24301,-1.62918,106.355
|
||||
2016-02-12 08:41:14,40.3492944,115.7863083,11.80800000000005,2.00308,72.8553,16.1,957.6,2.24278,-1.89877,107.29
|
||||
2016-02-12 08:41:15,40.3492889,115.7863306,11.740000000000009,2.16399,82.2897,16.1,957.6,2.24469,-1.95941,107.744
|
||||
2016-02-12 08:41:16,40.349282,115.7863579,11.583000000000027,2.06573,79.7034,16.1,957.6,2.24813,-1.54695,110.369
|
||||
2016-02-12 08:41:17,40.3492752,115.7863801,11.729000000000042,1.96264,82.2375,16.1,957.6,2.24149,-1.13449,115.571
|
||||
2016-02-12 08:41:18,40.349269,115.7863971,11.831999999999994,1.75547,77.7442,16.1,957.6,2.2488,-0.483331,119.086
|
||||
2016-02-12 08:41:19,40.3492581,115.7864217,11.521000000000015,1.54649,80.1118,16.1,957.6,2.24992,-0.430019,121.889
|
||||
2016-02-12 08:41:20,40.3492484,115.7864414,11.724000000000046,1.33618,90.5358,16.1,957.6,2.24614,-0.160765,124.514
|
||||
2016-02-12 08:41:21,40.3492379,115.7864599,11.656000000000006,1.45789,75.7241,16.1,957.6,2.24723,-0.584383,129.253
|
||||
2016-02-12 08:41:22,40.3492265,115.7864776,11.586000000000013,1.52183,64.4082,16.1,957.6,2.2461,-0.1055,133.895
|
||||
2016-02-12 08:41:23,40.3492142,115.7864946,11.551000000000045,1.41725,53.5575,16.1,957.6,2.24917,-0.06657,133.811
|
||||
2016-02-12 08:41:24,40.3492018,115.786511,11.67900000000003,1.96325,45.1024,16.1,957.6,2.24306,-0.546846,133.629
|
||||
2016-02-12 08:41:25,40.3491892,115.7865273,11.583000000000027,1.89267,44.8856,16.1,957.6,2.25231,-0.522077,133.854
|
||||
2016-02-12 08:41:26,40.3491766,115.786544,11.583000000000027,1.88507,45.3735,16.1,957.6,2.25312,-0.6115120000000001,134.018
|
||||
2016-02-12 08:41:27,40.349164,115.7865603,11.56800000000004,1.90691,44.2069,16.1,957.6,2.24901,-0.704764,137.995
|
||||
2016-02-12 08:41:28,40.3491509,115.7865759,11.733000000000004,1.89895,34.7881,16.1,957.6,2.2386,0.216962,144.763
|
||||
2016-02-12 08:41:29,40.3491363,115.7865883,12.136000000000024,1.57389,21.6126,16.1,957.4,2.25495,-0.546539,150.518
|
||||
2016-02-12 08:41:30,40.3491207,115.7865983,12.105999999999995,1.35377,6.14142,16.1,957.4,2.25067,-0.483964,155.45
|
||||
2016-02-12 08:41:31,40.3491041,115.7866067,11.953000000000031,1.48538,7.6301,16.1,957.6,2.25743,0.186806,161.625
|
||||
2016-02-12 08:41:32,40.349087,115.7866129,12.30800000000005,1.43146,25.3402,16.1,957.4,2.25932,-1.17416,169.539
|
||||
2016-02-12 08:41:33,40.3490693,115.7866169,12.262,1.67297,11.2128,16.1,957.4,2.24673,-0.856737,175.334
|
||||
2016-02-12 08:41:34,40.3490516,115.7866188,12.27600000000001,1.88287,5.05985,16.1,957.4,2.23945,-1.40952,175.648
|
||||
2016-02-12 08:41:35,40.349034,115.7866202,12.26600000000002,2.07013,7.49265,16.2,957.4,2.25005,-1.36203,175.821
|
||||
2016-02-12 08:41:36,40.3490162,115.7866215,12.102000000000032,2.16284,5.99243,16.2,957.4,2.24761,-1.28881,175.884
|
||||
2016-02-12 08:41:37,40.3489984,115.786623,12.342000000000041,2.62051,11.9578,16.2,957.4,2.2545,-1.72705,176.135
|
||||
2016-02-12 08:41:38,40.3489803,115.7866248,12.266999999999996,2.80948,9.05194,16.2,957.4,2.24654,-1.73863,175.918
|
||||
2016-02-12 08:41:39,40.3489624,115.7866263,12.15500000000003,2.79103,5.93186,16.2,957.4,2.24162,-1.93178,175.373
|
||||
2016-02-12 08:41:40,40.3489442,115.7866283,12.194000000000017,2.98124,7.96713,16.2,957.4,2.23972,-1.38345,175.634
|
||||
2016-02-12 08:41:41,40.3489261,115.7866302,12.290999999999997,2.76977,6.80938,16.2,957.4,2.23927,-1.48399,175.852
|
||||
2016-02-12 08:41:42,40.348908,115.7866319,12.145000000000039,2.88923,11.1758,16.2,957.4,2.24577,-1.11357,175.647
|
||||
2016-02-12 08:41:43,40.34889,115.7866334,12.115000000000009,2.73138,15.2008,16.2,957.4,2.2535,-1.25101,178.042
|
||||
2016-02-12 08:41:44,40.3488722,115.7866338,12.066000000000031,2.96751,22.2279,16.2,957.4,2.24615,-1.35349,-178.621
|
||||
2016-02-12 08:41:45,40.3488544,115.7866327,12.17100000000005,3.10965,27.6051,16.2,957.4,2.25523,-1.46058,-175.496
|
||||
2016-02-12 08:41:46,40.3488367,115.7866304,12.064000000000021,2.92633,28.5539,16.2,957.4,2.24891,-1.73632,-173.742
|
||||
2016-02-12 08:41:47,40.3488192,115.7866273,12.215000000000032,2.91376,27.4537,16.2,957.4,2.25825,-2.3877,-171.862
|
||||
2016-02-12 08:41:48,40.3488017,115.7866234,12.088999999999999,3.08075,27.0256,16.2,957.4,2.24402,-1.70543,-170.067
|
||||
2016-02-12 08:41:49,40.3487842,115.7866186,12.199000000000012,3.22416,29.298,16.3,957.4,2.26024,-1.40647,-166.772
|
||||
2016-02-12 08:41:50,40.3487669,115.7866126,12.105999999999995,3.04744,30.399,16.3,957.4,2.26284,-1.90689,-163.401
|
||||
2016-02-12 08:41:51,40.34875,115.7866053,12.230999999999995,3.03816,32.1537,16.3,957.4,2.25274,-2.12911,-160.487
|
||||
2016-02-12 08:41:52,40.3487332,115.7865977,12.277000000000044,3.16828,36.9141,16.3,957.4,2.24902,-2.42806,-158.691
|
||||
2016-02-12 08:41:53,40.3487164,115.7865891,12.119000000000028,3.51183,36.1496,16.3,957.4,2.25103,-2.7569,-159.977
|
||||
2016-02-12 08:41:54,40.3486995,115.7865807,12.200000000000045,3.67923,34.0202,16.3,957.4,2.25266,-2.03718,-159.794
|
||||
2016-02-12 08:41:55,40.3486826,115.7865724,12.234000000000037,3.99466,32.2461,16.3,957.4,2.25307,-2.50656,-159.647
|
||||
2016-02-12 08:41:56,40.3486656,115.7865644,12.124000000000024,3.87245,32.552,16.3,957.4,2.25371,-2.16503,-159.25
|
||||
2016-02-12 08:41:57,40.3486488,115.7865561,12.090000000000032,4.08144,33.5234,16.3,957.4,2.24347,-2.05622,-159.351
|
||||
2016-02-12 08:41:58,40.3486319,115.7865478,12.209000000000003,3.86491,36.1923,16.3,957.4,2.24522,-1.63832,-159.568
|
||||
2016-02-12 08:41:59,40.348615,115.7865392,12.052000000000021,3.72907,37.497,16.3,957.4,2.24749,-1.51081,-158.014
|
||||
2016-02-12 08:42:00,40.3485989,115.7865303,12.175000000000011,3.44141,42.7996,16.3,957.4,2.24907,-1.9916,-152.302
|
||||
2016-02-12 08:42:01,40.3485838,115.7865188,12.503000000000043,3.81648,51.5505,16.3,957.4,2.24449,-2.30392,-145.352
|
||||
2016-02-12 08:42:02,40.3485697,115.7865051,12.378000000000043,4.09751,56.1889,16.3,957.4,2.24475,-3.35498,-140.287
|
||||
2016-02-12 08:42:03,40.348554,115.7864864,12.354000000000042,4.45431,63.4819,16.3,957.4,2.25055,-2.78677,-133.264
|
||||
2016-02-12 08:42:04,40.3485421,115.7864686,12.239000000000033,4.57333,67.1847,16.3,957.4,2.25146,-2.48321,-128.375
|
||||
2016-02-12 08:42:05,40.3485336,115.7864536,12.379999999999995,4.48019,71.2003,16.3,957.4,2.24778,-2.34836,-123.58
|
||||
2016-02-12 08:42:06,40.348523,115.7864295,12.398000000000025,4.48081,77.9232,16.3,957.4,2.24845,-2.83183,-116.547
|
||||
2016-02-12 08:42:07,40.3485159,115.7864082,12.41500000000002,4.49352,80.9574,16.3,957.4,2.25643,-2.8192,-110.823
|
||||
2016-02-12 08:42:08,40.3485099,115.7863862,12.507000000000005,4.71163,79.3181,16.3,957.4,2.25778,-3.08629,-109.918
|
||||
2016-02-12 08:42:09,40.348504,115.7863639,12.674000000000035,4.83949,77.9107,16.3,957.4,2.24253,-2.90956,-109.746
|
||||
2016-02-12 08:42:10,40.348498,115.7863416,12.966000000000008,5.05952,75.906,16.3,957.4,2.24613,-2.97764,-110.009
|
||||
2016-02-12 08:42:11,40.3484919,115.7863189,12.289000000000044,4.99548,74.3886,16.3,957.4,2.24257,-2.69509,-109.879
|
||||
2016-02-12 08:42:12,40.3484856,115.7862963,12.305000000000007,4.86027,75.585,16.3,957.4,2.24794,-2.39785,-109.656
|
||||
2016-02-12 08:42:13,40.3484795,115.786274,12.30600000000004,4.63504,75.6022,16.3,957.4,2.25423,-2.3631,-109.497
|
||||
2016-02-12 08:42:14,40.3484733,115.7862519,12.319000000000017,4.62832,77.8567,16.3,957.4,2.24955,-2.26553,-109.463
|
||||
2016-02-12 08:42:15,40.3484674,115.7862297,12.467000000000041,4.51333,78.3431,16.3,957.4,2.24946,-2.37776,-110.25
|
||||
2016-02-12 08:42:16,40.3484612,115.7862076,12.367000000000019,4.41789,79.0752,16.3,957.4,2.25625,-2.54596,-110.091
|
||||
2016-02-12 08:42:17,40.3484548,115.7861855,12.536000000000001,4.47514,81.1023,16.3,957.4,2.24774,-2.51819,-109.937
|
||||
2016-02-12 08:42:18,40.3484486,115.7861638,12.861000000000047,4.55631,82.4619,16.3,957.4,2.24383,-2.94212,-109.487
|
||||
2016-02-12 08:42:19,40.3484429,115.7861421,12.637,4.85356,87.9828,16.3,957.4,2.25176,-3.28014,-106.279
|
||||
2016-02-12 08:42:20,40.3484385,115.78612,12.87700000000001,5.05794,92.5173,16.3,957.4,2.25419,-3.86801,-101.332
|
||||
2016-02-12 08:42:21,40.3484355,115.7860973,12.540999999999997,5.22229,96.0485,16.3,957.4,2.26133,-3.48247,-96.9957
|
||||
2016-02-12 08:42:22,40.3484339,115.786074,12.859000000000037,5.29359,99.1907,16.3,957.4,2.25554,-3.74325,-93.293
|
||||
2016-02-12 08:42:23,40.3484335,115.7860508,12.79400000000004,5.51611,102.248,16.3,957.4,2.25186,-3.60425,-89.6605
|
||||
2016-02-12 08:42:24,40.3484341,115.7860275,12.837000000000046,5.91545,104.712,16.3,957.4,2.26173,-4.32424,-86.4569
|
||||
2016-02-12 08:42:25,40.3484362,115.7860043,12.738,5.97849,108.205,16.3,957.4,2.25334,-4.14827,-81.8178
|
||||
2016-02-12 08:42:26,40.3484397,115.7859813,12.995000000000005,5.95724,109.774,16.3,957.4,2.26212,-3.98325,-77.084
|
||||
2016-02-12 08:42:27,40.3484442,115.7859587,13.02600000000001,5.88203,112.074,16.3,957.4,2.26216,-3.93422,-73.0804
|
||||
2016-02-12 08:42:28,40.3484496,115.7859361,12.66700000000003,5.89762,112.488,16.3,957.4,2.25082,-3.98341,-72.3455
|
||||
2016-02-12 08:42:29,40.3484555,115.7859136,13.284000000000049,5.88128,111.104,16.3,957.4,2.25202,-4.08997,-73.1645
|
||||
2016-02-12 08:42:30,40.3484603,115.7858911,13.001000000000033,5.88179,112.502,16.3,957.4,2.25494,-4.32926,-72.1189
|
||||
2016-02-12 08:42:31,40.3484659,115.785869,12.974000000000046,5.89474,113.961,16.3,957.4,2.2582,-4.21522,-72.1881
|
||||
2016-02-12 08:42:32,40.3484716,115.7858467,13.950000000000045,5.89713,112.242,16.3,957.4,2.25086,-4.48302,-72.4782
|
||||
2016-02-12 08:42:33,40.348477,115.785824,12.950000000000045,5.83897,111.227,16.3,957.4,2.25219,-3.79417,-72.9491
|
||||
2016-02-12 08:42:34,40.3484819,115.7858011,13.550000000000011,5.70104,110.514,16.3,957.4,2.25342,-4.27485,-72.8564
|
||||
2016-02-12 08:42:35,40.3484869,115.7857784,13.302999999999997,5.65056,112.288,16.2,957.4,2.25351,-4.61801,-72.341
|
||||
2016-02-12 08:42:36,40.3484922,115.7857551,12.936000000000035,5.64385,114.009,16.2,957.4,2.25632,-3.38854,-72.9091
|
||||
2016-02-12 08:42:37,40.348498,115.7857323,12.79000000000002,5.63199,115.023,16.2,957.4,2.25322,-3.08252,-71.8235
|
||||
2016-02-12 08:42:38,40.3485042,115.7857104,12.858000000000004,5.36999,121.775,16.2,957.4,2.25358,-3.24984,-65.9687
|
||||
2016-02-12 08:42:39,40.3485119,115.78569,12.900000000000034,5.15898,126.711,16.2,957.4,2.26002,-3.20228,-60.2979
|
||||
2016-02-12 08:42:40,40.348521,115.7856707,13.067000000000007,5.14989,132.227,16.2,957.4,2.25982,-3.62738,-54.9838
|
||||
2016-02-12 08:42:41,40.3485316,115.7856526,13.647000000000048,5.22281,136.78,16.2,957.4,2.26323,-4.14084,-49.9837
|
||||
2016-02-12 08:42:42,40.3485433,115.7856361,13.871000000000038,5.67075,139.828,16.2,957.4,2.25176,-4.97773,-45.5356
|
||||
2016-02-12 08:42:43,40.3485561,115.7856201,12.992000000000019,5.82711,142.777,16.2,957.4,2.26061,-4.46511,-40.6979
|
||||
2016-02-12 08:42:44,40.3485702,115.7856055,12.66700000000003,5.82758,147.255,16.2,957.4,2.26035,-3.8026,-34.6946
|
||||
2016-02-12 08:42:45,40.3485853,115.785593,12.718999999999994,5.62707,149.964,16.2,957.4,2.24634,-4.16698,-28.7379
|
||||
2016-02-12 08:42:46,40.3486012,115.7855819,12.712000000000046,5.40462,152.98,16.2,957.4,2.25537,-3.65881,-25.8209
|
||||
2016-02-12 08:42:47,40.3486175,115.7855715,12.614000000000033,5.40776,154.124,16.2,957.4,2.24852,-3.58218,-25.511
|
||||
2016-02-12 08:42:48,40.348634,115.7855611,12.459000000000003,5.20386,155.024,16.2,957.4,2.25714,-3.00576,-25.8377
|
||||
2016-02-12 08:42:49,40.3486501,115.7855508,12.467000000000041,5.17858,156.915,16.2,957.4,2.26037,-3.62848,-25.7774
|
||||
2016-02-12 08:42:50,40.3486696,115.7855384,12.532000000000039,5.27778,157.696,16.2,957.4,2.25554,-3.50805,-25.7918
|
||||
2016-02-12 08:42:51,40.3486825,115.7855303,12.616000000000042,5.37135,158.794,16.2,957.4,2.25686,-3.56731,-25.5099
|
||||
2016-02-12 08:42:52,40.3487016,115.7855185,12.533000000000015,5.46752,156.941,16.2,957.4,2.25935,-3.98915,-25.6481
|
||||
2016-02-12 08:42:53,40.3487176,115.7855085,12.420000000000016,5.58741,155.836,16.2,957.4,2.26681,-4.31272,-25.4288
|
||||
2016-02-12 08:42:54,40.3487336,115.7854986,12.732000000000028,5.78335,155.823,16.2,957.4,2.25226,-4.67656,-25.4368
|
||||
2016-02-12 08:42:55,40.3487496,115.785489,12.701999999999998,5.92013,158.796,16.1,957.4,2.25212,-4.80537,-23.1046
|
||||
2016-02-12 08:42:56,40.348766,115.78548,12.518000000000029,5.9415,162.588,16.1,957.4,2.24934,-4.43397,-19.1058
|
||||
2016-02-12 08:42:57,40.348783,115.7854727,12.524000000000001,5.72876,166.81,16.1,957.4,2.25649,-4.15858,-15.3635
|
||||
2016-02-12 08:42:58,40.3488007,115.7854667,12.446000000000026,5.43129,168.769,16.1,957.4,2.25655,-3.56822,-12.2917
|
||||
2016-02-12 08:42:59,40.3488186,115.7854623,12.591000000000008,5.38271,169.969,16.1,957.4,2.25312,-3.63613,-9.66924
|
||||
2016-02-12 08:43:00,40.3488359,115.7854588,12.819000000000017,5.39655,172.218,16.1,957.4,2.25178,-4.54191,-6.79331
|
||||
2016-02-12 08:43:01,40.3488535,115.7854562,12.608000000000004,5.58193,174.925,16.1,957.4,2.25727,-4.15431,-3.67836
|
||||
2016-02-12 08:43:02,40.3488709,115.7854557,12.687000000000012,5.78932,176.998,16.1,957.4,2.24647,-4.26279,0.864772
|
||||
2016-02-12 08:43:03,40.3488887,115.7854572,12.700000000000045,5.80957,176.641,16.1,957.4,2.24594,-4.19075,4.37661
|
||||
2016-02-12 08:43:04,40.3489065,115.7854597,12.713999999999999,5.79471,176.923,16.1,957.4,2.25494,-4.33135,5.51263
|
||||
2016-02-12 08:43:05,40.3489243,115.7854622,12.805000000000007,5.76599,174.367,16.1,957.4,2.25948,-4.54469,5.39902
|
||||
2016-02-12 08:43:06,40.3489424,115.7854643,12.515000000000043,5.66438,170.978,16.1,957.4,2.24338,-3.95422,5.18129
|
||||
2016-02-12 08:43:07,40.3489605,115.7854662,12.734000000000037,5.51594,168.338,16.1,957.4,2.25627,-3.94131,5.09212
|
||||
2016-02-12 08:43:08,40.3489785,115.7854682,12.634000000000015,5.26331,167.49,16.0,957.4,2.25091,-3.82072,5.21668
|
||||
2016-02-12 08:43:09,40.3489964,115.7854699,11.968000000000018,5.3484,169.105,16.0,957.6,2.253,-3.64647,5.05816
|
||||
2016-02-12 08:43:10,40.3490145,115.7854713,12.449000000000012,5.24774,170.138,16.0,957.4,2.25451,-3.90744,4.97669
|
||||
2016-02-12 08:43:11,40.3490323,115.7854731,12.962000000000046,5.00866,172.165,16.0,957.4,2.25349,-4.42495,5.03938
|
||||
2016-02-12 08:43:12,40.3490499,115.7854749,12.913000000000011,5.17607,173.761,16.0,957.4,2.25456,-3.96802,4.88758
|
||||
2016-02-12 08:43:13,40.3490678,115.7854774,12.772000000000048,5.17531,173.632,16.0,957.4,2.24864,-4.1331,5.31392
|
||||
2016-02-12 08:43:14,40.3490859,115.7854799,12.660000000000025,5.33766,172.301,16.0,957.4,2.24415,-3.63245,5.13994
|
||||
2016-02-12 08:43:15,40.3491037,115.7854819,12.688000000000045,5.06418,172.85,16.0,957.4,2.25334,3.77341,5.43588
|
||||
2016-02-12 08:43:16,40.3491139,115.7854834,12.534000000000049,3.21808,163.848,16.0,957.4,2.25053,-3.0238,4.27478
|
||||
2016-02-12 08:43:17,40.3491151,115.7854831,12.472000000000037,2.30783,161.518,16.0,957.4,2.25659,-2.5161,4.4831
|
||||
2016-02-12 08:43:18,40.3491145,115.7854826,12.115000000000009,1.10665,156.651,16.0,957.4,2.25019,-3.33712,3.89
|
||||
2016-02-12 08:43:19,40.3491143,115.7854821,12.076000000000022,0.729867,160.824,16.0,957.4,2.25651,-2.90574,4.24684
|
||||
2016-02-12 08:43:20,40.3491143,115.785482,12.163000000000011,0.916749,168.514,16.0,957.4,2.24599,-2.40328,5.06117
|
||||
2016-02-12 08:43:21,40.3491144,115.7854825,12.129999999999995,0.736194,174.551,16.0,957.4,2.24683,-2.259,4.69336
|
||||
2016-02-12 08:43:22,40.3491147,115.7854827,12.256000000000029,0.721629,173.06,16.0,957.4,2.24472,-2.30074,3.95699
|
||||
2016-02-12 08:43:23,40.3491147,115.7854826,12.312000000000012,1.02892,170.476,16.0,957.4,2.25346,-2.14544,4.63103
|
||||
2016-02-12 08:43:24,40.3491148,115.7854826,12.391999999999996,1.71353,162.162,16.0,957.4,2.24199,-2.70262,4.38941
|
||||
2016-02-12 08:43:25,40.349115,115.7854826,12.213999999999999,1.90056,157.922,15.9,957.4,2.25142,-2.56983,4.16209
|
||||
2016-02-12 08:43:26,40.3491151,115.785483,12.249000000000024,1.89172,162.111,15.9,957.4,2.24987,-2.37081,4.33851
|
||||
2016-02-12 08:43:27,40.3491153,115.7854835,12.331000000000017,2.02974,161.345,15.9,957.4,2.25097,-2.36842,4.13939
|
||||
2016-02-12 08:43:28,40.3491156,115.7854835,12.210000000000036,2.11439,163.381,15.9,957.4,2.24901,-2.72557,4.44417
|
||||
2016-02-12 08:43:29,40.3491158,115.7854836,12.192000000000007,1.61638,157.271,15.9,957.4,2.2473,-2.35678,4.22427
|
||||
2016-02-12 08:43:30,40.3491159,115.7854837,12.297000000000025,1.69106,156.577,15.9,957.4,2.24907,-2.72985,4.52237
|
||||
2016-02-12 08:43:31,40.3491159,115.785484,12.105999999999995,1.27958,154.251,16.0,957.4,2.24912,-2.4493,4.27238
|
||||
2016-02-12 08:43:32,40.3491158,115.7854842,12.254000000000019,1.37727,157.977,16.0,957.4,2.25123,-3.27265,4.57478
|
||||
2016-02-12 08:43:33,40.3491159,115.7854845,12.354000000000042,1.21603,157.19,16.0,957.4,2.25817,-3.16157,4.23416
|
||||
2016-02-12 08:43:34,40.3491161,115.7854845,12.486000000000047,1.52134,161.142,16.0,957.4,2.24876,-2.97854,4.09014
|
||||
2016-02-12 08:43:35,40.3491163,115.7854843,12.413000000000011,1.62379,159.631,16.0,957.4,2.24613,-2.711,4.14956
|
||||
2016-02-12 08:43:36,40.3491166,115.7854841,12.363,1.91354,161.307,16.0,957.4,2.24829,-2.66831,4.3683
|
||||
2016-02-12 08:43:37,40.3491167,115.7854843,12.518000000000029,2.54485,128.325,16.0,957.4,2.24011,-2.99271,4.54588
|
||||
2016-02-12 08:43:38,40.3491171,115.7854846,13.846000000000004,3.16679,122.058,16.0,957.4,2.24456,-2.10601,4.30452
|
||||
2016-02-12 08:43:39,40.3491173,115.7854846,16.337000000000046,1.4036,139.974,16.0,957.4,2.25051,-2.11919,4.49659
|
||||
2016-02-12 08:43:40,40.3491161,115.7854819,17.247000000000014,0.665258,149.372,16.0,957.4,2.25517,-2.29389,44.7974
|
||||
2016-02-12 08:43:41,40.3491153,115.7854797,17.309000000000026,0.15585,172.788,16.0,957.4,2.25108,-6.16028,50.6785
|
||||
2016-02-12 08:43:42,40.3491182,115.7854836,17.16300000000001,1.79734,144.454,16.0,957.4,2.24024,-9.94753,51.2855
|
||||
2016-02-12 08:43:43,40.3491268,115.7854975,17.49200000000002,2.21825,152.062,16.0,957.4,2.26809,1.95625,50.9678
|
||||
2016-02-12 08:43:44,40.3491325,115.7855068,17.25600000000003,2.33955,175.315,16.0,957.4,2.26068,1.88367,47.4681
|
||||
2016-02-12 08:43:45,40.3491338,115.7855087,17.298000000000002,2.15619,172.161,16.0,957.4,2.23978,-10.4233,47.4627
|
||||
2016-02-12 08:43:46,40.3491408,115.7855185,17.400000000000034,3.19316,157.115,16.0,957.4,2.24856,-5.19856,47.9142
|
||||
2016-02-12 08:43:47,40.349153,115.7855364,18.277000000000044,3.51508,159.235,16.0,957.4,2.25081,-4.18958,47.1094
|
||||
2016-02-12 08:43:48,40.349166,115.7855553,18.075000000000045,4.85972,155.95,16.0,957.4,2.2575,-3.81284,46.5688
|
||||
2016-02-12 08:43:49,40.349179,115.7855737,18.112000000000023,5.30648,153.064,16.0,957.4,2.25406,-4.46158,47.5726
|
||||
2016-02-12 08:43:50,40.3491919,115.7855918,18.206000000000017,5.51106,149.706,16.0,957.4,2.25075,-3.58214,47.8376
|
||||
2016-02-12 08:43:51,40.3492042,115.7856099,18.246000000000038,5.69668,149.791,16.0,957.4,2.25386,-4.1528,47.7509
|
||||
2016-02-12 08:43:52,40.3492167,115.7856276,17.972000000000037,5.65832,151.933,16.0,957.4,2.25417,-3.96174,47.7473
|
||||
2016-02-12 08:43:53,40.349229,115.7856451,17.373000000000047,5.52845,152.475,16.0,957.4,2.25498,-3.35287,47.6634
|
||||
2016-02-12 08:43:54,40.3492411,115.7856621,17.248000000000047,5.19567,150.868,16.0,957.4,2.25428,-3.57325,50.2049
|
||||
2016-02-12 08:43:55,40.3492522,115.7856796,17.732000000000028,4.85903,145.241,16.0,957.4,2.25249,-4.60038,54.3005
|
||||
2016-02-12 08:43:56,40.3492618,115.7856982,17.80200000000002,4.89722,141.182,16.0,957.4,2.25698,-4.268,57.7334
|
||||
2016-02-12 08:43:57,40.3492709,115.7857178,17.648000000000025,5.00458,138.678,16.0,957.4,2.25503,-4.03971,60.8126
|
||||
2016-02-12 08:43:58,40.3492792,115.7857383,17.670000000000016,5.06119,134.395,16.0,957.4,2.25882,-4.43467,64.1499
|
||||
2016-02-12 08:43:59,40.3492867,115.7857602,17.617999999999995,4.97336,132.843,16.0,957.4,2.25522,-3.4857,67.6561
|
||||
2016-02-12 08:44:00,40.3492929,115.7857822,17.749000000000024,4.76678,129.392,16.0,957.4,2.25682,-3.13844,72.5023
|
||||
2016-02-12 08:44:01,40.3492981,115.7858047,17.458000000000027,4.70141,129.949,16.0,957.4,2.25787,-3.00207,73.9701
|
||||
2016-02-12 08:44:02,40.3493031,115.7858268,17.75600000000003,4.64818,127.629,16.0,957.4,2.25249,-4.01299,74.3217
|
||||
2016-02-12 08:44:03,40.3493081,115.7858497,17.591000000000008,4.78015,127.062,16.0,957.4,2.26045,-3.45797,73.9458
|
||||
2016-02-12 08:44:04,40.349313,115.7858725,17.576000000000022,4.80095,126.743,16.0,957.4,2.25445,-3.48772,74.0784
|
||||
2016-02-12 08:44:05,40.3493179,115.7858957,17.460000000000036,4.46846,127.093,16.0,957.4,2.25907,-2.84138,74.6726
|
||||
2016-02-12 08:44:06,40.3493226,115.7859187,17.524,4.40493,128.632,16.0,957.4,2.2653,-2.7391,74.8234
|
||||
2016-02-12 08:44:07,40.3493273,115.7859409,17.883000000000038,4.20996,130.254,16.0,957.4,2.25721,-3.75628,74.3471
|
||||
2016-02-12 08:44:08,40.3493319,115.7859629,18.008000000000038,4.54446,126.276,16.0,957.4,2.24421,-4.13393,75.9746
|
||||
2016-02-12 08:44:09,40.349336,115.7859856,17.601,4.60809,119.994,16.0,957.4,2.25283,-3.65654,80.2085
|
||||
2016-02-12 08:44:10,40.3493389,115.7860089,17.54600000000005,4.59821,116.256,16.0,957.4,2.25826,-3.52848,84.5632
|
||||
2016-02-12 08:44:11,40.3493404,115.7860326,17.343999999999994,4.60662,111.882,16.0,957.4,2.26272,-2.83632,87.8923
|
||||
2016-02-12 08:44:12,40.3493407,115.786056,17.586000000000013,4.26527,108.906,16.0,957.4,2.26141,-2.64772,91.0007
|
||||
2016-02-12 08:44:13,40.34934,115.7860794,17.64300000000003,4.15369,106.719,16.0,957.4,2.25931,-2.97149,93.9182
|
||||
2016-02-12 08:44:14,40.3493383,115.7861028,17.40500000000003,4.06511,102.898,15.9,957.4,2.25609,-2.43398,97.4505
|
||||
2016-02-12 08:44:15,40.3493351,115.7861305,17.194000000000017,4.10584,100.818,15.9,957.4,2.25337,-2.45874,102.19
|
||||
2016-02-12 08:44:16,40.3493312,115.7861533,17.29600000000005,3.5283,94.6166,15.9,957.4,2.25653,-2.18443,106.176
|
||||
2016-02-12 08:44:17,40.349326,115.7861758,17.158000000000015,3.14686,91.3884,15.9,957.4,2.26545,-1.54058,107.424
|
||||
2016-02-12 08:44:18,40.3493205,115.7861983,17.150000000000034,2.79263,94.4733,15.9,957.4,2.25802,-2.1108,106.841
|
||||
2016-02-12 08:44:19,40.349315,115.7862206,17.38900000000001,2.88462,94.7514,15.9,957.4,2.25069,-2.22205,107.513
|
||||
2016-02-12 08:44:20,40.3493094,115.7862432,17.067000000000007,2.76044,95.8841,15.9,957.4,2.2528,-1.19721,107.134
|
||||
2016-02-12 08:44:21,40.3493041,115.7862659,17.110000000000014,2.41459,96.6922,15.9,957.4,2.26201,-1.52747,106.883
|
||||
2016-02-12 08:44:22,40.3492987,115.7862882,17.20500000000004,2.22398,91.4351,15.9,957.4,2.25557,-2.13694,106.758
|
||||
2016-02-12 08:44:23,40.3492936,115.7863106,17.402000000000044,2.22706,93.5224,15.9,957.4,2.25374,-2.07331,107.193
|
||||
2016-02-12 08:44:24,40.3492882,115.786333,17.234000000000037,2.23447,93.8195,16.0,957.4,2.25663,-1.73543,107.136
|
||||
2016-02-12 08:44:25,40.3492828,115.7863553,17.341000000000008,2.369,89.0479,16.0,957.4,2.25711,-2.02998,109.231
|
||||
2016-02-12 08:44:26,40.3492764,115.786377,17.462000000000046,2.58026,83.2347,16.0,957.4,2.26416,-1.803,114.054
|
||||
2016-02-12 08:44:27,40.3492688,115.7863981,17.28200000000004,2.71047,83.2986,16.0,957.4,2.25945,-2.07605,117.681
|
||||
2016-02-12 08:44:28,40.3492603,115.7864185,17.536,2.62323,77.0265,16.0,957.4,2.25908,-1.97262,120.92
|
||||
2016-02-12 08:44:29,40.3492507,115.786438,17.591000000000008,2.65832,73.4184,16.0,957.4,2.25308,-1.5131,124.759
|
||||
2016-02-12 08:44:30,40.3492401,115.7864565,17.560000000000002,2.7659,69.8955,16.0,957.4,2.26277,-2.36132,129.088
|
||||
2016-02-12 08:44:31,40.3492287,115.7864741,17.32800000000003,2.67548,62.8153,16.0,957.4,2.25068,-2.22984,133.597
|
||||
2016-02-12 08:44:32,40.3492162,115.7864915,17.398000000000025,2.61916,62.5652,16.0,957.4,2.2546,-1.56645,134.004
|
||||
2016-02-12 08:44:33,40.3492038,115.7865082,17.36500000000001,2.38325,63.9515,16.0,957.4,2.25572,-2.22344,134.419
|
||||
2016-02-12 08:44:34,40.3491913,115.7865247,17.396000000000015,2.4148,60.3102,16.0,957.4,2.26123,-1.89722,134.63
|
||||
2016-02-12 08:44:35,40.3491787,115.7865415,17.450000000000045,2.71965,65.9189,16.0,957.4,2.24881,-1.83215,134.581
|
||||
2016-02-12 08:44:36,40.3491659,115.7865581,17.43300000000005,2.8837,66.0174,16.0,957.4,2.26288,-1.92149,136.984
|
||||
2016-02-12 08:44:37,40.3491527,115.7865736,17.39300000000003,2.99332,47.2055,16.0,957.4,2.25737,-1.88269,144.475
|
||||
2016-02-12 08:44:39,40.3491381,115.7865865,17.437000000000012,2.60973,38.2454,16.0,957.4,2.26084,-1.88556,150.27
|
||||
2016-02-12 08:44:39,40.3491223,115.7865974,17.41500000000002,2.0888,24.9333,16.0,957.4,2.26337,-1.17669,155.997
|
||||
2016-02-12 08:44:40,40.3491058,115.7866061,17.382000000000005,2.04768,20.821,16.0,957.4,2.26543,-0.978078,161.237
|
||||
2016-02-12 08:44:42,40.3490888,115.7866126,17.50400000000002,2.02731,13.2953,16.0,957.4,2.25556,-1.22699,167.844
|
||||
2016-02-12 08:44:43,40.3490714,115.7866164,17.65500000000003,1.96942,5.29049,16.0,957.4,2.25431,-0.927087,174.693
|
||||
2016-02-12 08:44:44,40.3490538,115.7866182,17.715000000000032,1.96387,3.9822,16.0,957.4,2.25309,-1.37721,176.111
|
||||
2016-02-12 08:44:45,40.349036,115.7866197,17.62900000000002,1.97297,4.3481,16.0,957.4,2.26046,-1.17786,176.094
|
||||
2016-02-12 08:44:46,40.3490182,115.7866212,17.64500000000004,1.97891,4.29482,16.0,957.4,2.25826,-1.22428,175.974
|
||||
2016-02-12 08:44:47,40.3490003,115.7866228,17.56400000000002,1.92987,5.27529,16.0,957.4,2.25967,-1.38526,175.432
|
||||
2016-02-12 08:44:48,40.3489822,115.7866244,17.507000000000005,2.11482,2.04059,16.1,957.4,2.25986,-1.25446,175.941
|
||||
2016-02-12 08:44:49,40.348964,115.786626,17.494000000000028,2.2084,4.66539,16.1,957.4,2.25505,-1.25606,175.786
|
||||
2016-02-12 08:44:50,40.3489461,115.7866279,17.533000000000015,1.98396,12.7157,16.1,957.4,2.25133,-1.10287,176.088
|
||||
2016-02-12 08:44:51,40.3489282,115.7866296,17.510000000000048,2.00829,16.0908,16.1,957.4,2.26272,-1.05535,176.051
|
||||
2016-02-12 08:44:52,40.3489102,115.7866312,17.38900000000001,2.00029,6.62347,16.1,957.4,2.26306,-1.15381,175.658
|
||||
2016-02-12 08:44:53,40.3488923,115.7866329,17.53200000000004,2.00055,5.94855,16.1,957.4,2.26199,-1.15091,177.283
|
||||
2016-02-12 08:44:54,40.3488743,115.7866336,17.375,1.84412,12.2126,16.1,957.4,2.25823,-0.905536,-179.37
|
||||
2016-02-12 08:44:55,40.3488565,115.7866328,17.537000000000035,1.81085,14.9492,16.1,957.4,2.25636,-0.861661,-176.408
|
||||
2016-02-12 08:44:56,40.3488387,115.7866309,17.624000000000024,1.84405,14.0908,16.1,957.4,2.25948,-1.10296,-174.525
|
||||
2016-02-12 08:44:57,40.348821,115.7866281,17.559000000000026,1.93804,11.6952,16.1,957.4,2.26045,-0.947733,-172.353
|
||||
2016-02-12 08:44:58,40.3488035,115.7866242,17.694000000000017,2.00184,14.1004,16.1,957.4,2.25602,-1.26374,-169.775
|
||||
2016-02-12 08:44:59,40.348786,115.7866195,17.708000000000027,2.03629,21.9379,16.1,957.4,2.2498,-1.05616,-166.958
|
||||
2016-02-12 08:45:00,40.3487688,115.7866138,17.646000000000015,2.05078,24.9459,16.1,957.4,2.25291,-1.05454,-163.484
|
||||
2016-02-12 08:45:01,40.3487517,115.7866069,17.65900000000005,1.95402,30.7938,16.2,957.4,2.26536,-0.651226,-160.678
|
||||
2016-02-12 08:45:02,40.3487347,115.786599,17.588000000000022,1.7691,34.7243,16.2,957.4,2.24918,-0.857132,-159.513
|
||||
2016-02-12 08:45:03,40.348718,115.7865908,17.598000000000013,1.67192,47.1372,16.2,957.4,2.26345,-1.21446,-159.669
|
||||
2016-02-12 08:45:04,40.3487011,115.7865824,17.69500000000005,1.68668,54.3084,16.2,957.4,2.25893,-0.823606,-159.862
|
||||
2016-02-12 08:45:05,40.3486843,115.7865742,17.79200000000003,1.74508,47.7868,16.2,957.4,2.25838,-0.884091,-159.588
|
||||
2016-02-12 08:45:06,40.3486642,115.7865643,17.739000000000033,1.86477,34.9555,16.2,957.4,2.25685,-1.2888,-159.629
|
||||
2016-02-12 08:45:07,40.3486473,115.7865559,17.80200000000002,1.9374,38.5253,16.2,957.4,2.26043,-0.978609,-159.995
|
||||
2016-02-12 08:45:08,40.3486305,115.7865474,17.758000000000038,1.92136,38.8821,16.2,957.4,2.26082,-0.655105,-160.113
|
||||
2016-02-12 08:45:09,40.3486139,115.7865389,17.77000000000004,1.86096,61.4759,16.2,957.4,2.26501,-0.96432,-157.731
|
||||
2016-02-12 08:45:10,40.3485975,115.7865297,17.74000000000001,1.84646,65.4398,16.2,957.4,2.26133,-0.770468,-151.285
|
||||
2016-02-12 08:45:11,40.3485824,115.7865178,17.840000000000032,1.77807,66.8571,16.3,957.4,2.25876,-0.859863,-144.656
|
||||
2016-02-12 08:45:12,40.3485682,115.7865036,17.966000000000008,1.88744,61.4756,16.3,957.4,2.26721,-0.782214,-139.476
|
||||
2016-02-12 08:45:13,40.3485553,115.7864884,17.864000000000033,2.1576,77.0466,16.3,957.4,2.26486,-1.47746,-133.976
|
||||
2016-02-12 08:45:14,40.3485435,115.7864713,18.11700000000002,2.70015,81.7995,16.3,957.4,2.26416,-1.10274,-128.533
|
||||
2016-02-12 08:45:15,40.3485332,115.786453,18.287000000000035,2.69214,88.1674,16.3,957.4,2.26025,-1.94088,-122.849
|
||||
2016-02-12 08:45:16,40.3485245,115.7864337,18.45500000000004,3.78728,89.9081,16.3,957.4,2.25168,-3.04325,-117.301
|
||||
2016-02-12 08:45:17,40.3485173,115.7864131,18.396000000000015,4.16305,89.0334,16.3,957.4,2.25147,-3.31608,-111.595
|
||||
2016-02-12 08:45:18,40.3485111,115.786391,18.156000000000006,4.32816,88.5418,16.4,957.4,2.25652,-3.47936,-110.1
|
||||
2016-02-12 08:45:19,40.3485051,115.7863681,18.30000000000001,4.57069,83.1046,16.4,957.4,2.24823,-2.257,-110.004
|
||||
2016-02-12 08:45:20,40.348499,115.7863454,18.563000000000045,4.63882,82.4551,16.4,957.4,2.25442,-2.3749,-109.707
|
||||
2016-02-12 08:45:21,40.3484927,115.786323,18.28800000000001,4.55507,82.4027,16.4,957.4,2.26189,-2.34621,-109.863
|
||||
2016-02-12 08:45:22,40.3484865,115.7863004,18.225000000000023,4.52532,83.4422,16.4,957.4,2.26022,-2.46761,-109.93
|
||||
2016-02-12 08:45:23,40.3484803,115.7862778,18.161,4.26556,86.1035,16.4,957.4,2.25502,-2.08226,-109.794
|
||||
2016-02-12 08:45:24,40.3484741,115.7862556,18.208000000000027,4.23751,87.8882,16.4,957.4,2.25048,-2.05877,-109.916
|
||||
2016-02-12 08:45:25,40.348468,115.7862333,18.089,4.12006,88.2306,16.4,957.4,2.24768,-2.21128,-109.852
|
||||
2016-02-12 08:45:26,40.3484618,115.7862112,18.319000000000017,3.95742,89.733,16.4,957.4,2.25324,-1.93889,-109.628
|
||||
2016-02-12 08:45:27,40.3484557,115.7861892,18.310000000000002,3.98109,89.0515,16.4,957.4,2.25249,-2.38076,-109.893
|
||||
2016-02-12 08:45:28,40.3484497,115.7861672,18.201999999999998,3.97322,90.7361,16.4,957.4,2.25567,-2.45486,-109.649
|
||||
2016-02-12 08:45:29,40.3484439,115.7861451,18.471000000000004,3.97965,94.0065,16.4,957.4,2.26075,-2.21297,-106.317
|
||||
2016-02-12 08:45:30,40.3484394,115.7861226,18.227000000000032,3.89264,97.9598,16.4,957.4,2.25305,-2.42729,-101.534
|
||||
2016-02-12 08:45:31,40.3484363,115.7860996,18.062000000000012,3.88647,103.014,16.4,957.4,2.26572,-2.24522,-97.1811
|
||||
2016-02-12 08:45:32,40.3484347,115.7860764,18.20500000000004,3.96167,107.487,16.4,957.4,2.25969,-2.48252,-93.2813
|
||||
2016-02-12 08:45:33,40.3484342,115.7860533,18.545000000000016,4.19445,109.054,16.4,957.4,2.26069,-2.93529,-89.8609
|
||||
2016-02-12 08:45:34,40.3484348,115.7860307,18.602000000000032,4.59572,106.289,16.4,957.4,2.25846,-3.74323,-87.0064
|
||||
2016-02-12 08:45:35,40.3484361,115.7860075,18.62700000000001,4.55856,108.097,16.5,957.4,2.25328,-3.77968,-82.5635
|
||||
2016-02-12 08:45:36,40.3484388,115.7859842,18.871000000000038,4.94187,112.091,16.5,957.4,2.25766,-3.47161,-77.7257
|
||||
2016-02-12 08:45:37,40.3484431,115.7859614,18.620000000000005,4.99916,114.421,16.5,957.4,2.24696,-4.25361,-73.8859
|
||||
2016-02-12 08:45:38,40.3484484,115.7859379,18.662000000000035,5.041,118.194,16.5,957.4,2.24988,-2.40826,-72.456
|
||||
2016-02-12 08:45:39,40.3484539,115.7859151,18.590000000000032,4.86597,121.342,16.5,957.4,2.23913,-2.65323,-72.0437
|
||||
2016-02-12 08:45:40,40.3484598,115.7858929,18.458000000000027,4.57349,122.896,16.5,957.4,2.24802,-2.92134,-72.8244
|
||||
2016-02-12 08:45:41,40.3484654,115.7858701,18.50400000000002,4.62556,125.251,16.5,957.4,2.25008,-2.46242,-72.5329
|
||||
2016-02-12 08:45:42,40.3484709,115.7858478,18.313000000000045,4.34028,123.989,16.5,957.4,2.2451,-3.08264,-72.7796
|
||||
2016-02-12 08:45:43,40.3484763,115.7858252,17.950000000000045,4.58716,121.914,16.5,957.4,2.24825,-2.61072,-72.9401
|
||||
2016-02-12 08:45:44,40.3484817,115.7858026,17.908000000000015,4.45187,123.576,16.5,957.4,2.2571,-2.53661,-72.1734
|
||||
2016-02-12 08:45:45,40.3484873,115.7857801,18.42700000000002,4.56513,122.201,16.5,957.4,2.24877,-2.19526,-72.9211
|
||||
2016-02-12 08:45:46,40.3484926,115.7857577,18.236000000000047,4.27496,122.441,16.5,957.4,2.25316,-2.56898,-72.6472
|
||||
2016-02-12 08:45:47,40.3484981,115.7857353,18.04000000000002,4.05575,123.342,16.5,957.4,2.24489,-2.28949,-71.977
|
||||
2016-02-12 08:45:48,40.3485039,115.7857134,18.49200000000002,4.03684,127.936,16.5,957.4,2.25751,-2.46188,-67.2151
|
||||
2016-02-12 08:45:49,40.348511,115.785693,18.735000000000014,4.11837,133.691,16.5,957.4,2.25974,-3.48746,-61.2099
|
||||
2016-02-12 08:45:50,40.3485197,115.7856737,18.715000000000032,4.69469,136.928,16.5,957.4,2.24865,-3.85436,-55.4787
|
||||
2016-02-12 08:45:51,40.3485302,115.785655,18.584000000000003,4.95181,138.959,16.5,957.4,2.25789,-3.25282,-50.9018
|
||||
2016-02-12 08:45:52,40.3485445,115.7856342,19.019000000000005,5.16532,142.967,16.5,957.4,2.25162,-3.51994,-44.9958
|
||||
2016-02-12 08:45:53,40.3485551,115.7856217,18.930000000000007,5.17427,145.326,16.5,957.4,2.25415,-3.20807,-40.945
|
||||
2016-02-12 08:45:54,40.3485718,115.7856049,19.383000000000038,5.22752,149.735,16.5,957.4,2.23934,-4.49393,-34.1759
|
||||
2016-02-12 08:45:55,40.3485865,115.7855928,19.444000000000017,5.46417,153.647,16.5,957.4,2.24285,-3.81437,-28.297
|
||||
2016-02-12 08:45:56,40.3486022,115.7855819,19.15900000000005,5.61763,151.635,16.5,957.4,2.24025,-3.88984,-25.5592
|
||||
2016-02-12 08:45:57,40.3486185,115.7855714,19.13100000000003,5.22492,151.447,16.5,957.4,2.24517,-4.60596,-25.7129
|
||||
2016-02-12 08:45:58,40.348635,115.7855608,18.66500000000002,5.22477,152.331,16.5,957.4,2.25713,-3.54813,-26.0158
|
||||
2016-02-12 08:45:59,40.3486512,115.78555,18.549000000000035,5.00249,156.044,16.5,957.4,2.2507,-3.84303,-26.0043
|
||||
2016-02-12 08:46:00,40.3486673,115.7855394,18.598000000000013,4.90608,159.322,16.5,957.4,2.24678,-3.58029,-25.9375
|
||||
2016-02-12 08:46:01,40.3486837,115.7855294,18.55400000000003,4.89749,161.021,16.5,957.4,2.24366,-3.33764,-25.6277
|
||||
2016-02-12 08:46:02,40.3486999,115.7855194,18.826999999999998,4.74312,160.992,16.5,957.4,2.24832,-3.63823,-25.437
|
||||
2016-02-12 08:46:03,40.3487159,115.7855095,18.734000000000037,5.08126,160.834,16.5,957.4,2.24889,-3.83332,-25.7044
|
||||
2016-02-12 08:46:04,40.348732,115.7854993,18.697000000000003,5.29667,161.546,16.5,957.4,2.24539,-3.81592,-25.3915
|
||||
2016-02-12 08:46:05,40.3487486,115.7854894,18.629999999999995,5.33145,161.929,16.5,957.4,2.24517,-3.09275,-23.3054
|
||||
2016-02-12 08:46:06,40.3487655,115.7854806,18.716000000000008,4.78943,163.905,16.5,957.4,2.25106,-2.56043,-19.1261
|
||||
2016-02-12 08:46:07,40.348782,115.7854734,18.864000000000033,4.65857,165.761,16.5,957.4,2.24854,-3.60127,-15.6055
|
||||
2016-02-12 08:46:08,40.3487988,115.7854675,19.091000000000008,4.77459,166.962,16.5,957.4,2.24936,-4.10476,-12.4039
|
||||
2016-02-12 08:46:09,40.3488161,115.785463,19.161,5.17038,169.08,16.5,957.4,2.26161,-3.88614,-10.0499
|
||||
2016-02-12 08:46:10,40.3488335,115.7854591,19.347000000000037,5.30009,171.894,16.5,957.4,2.25124,-4.02029,-7.24302
|
||||
2016-02-12 08:46:11,40.3488513,115.7854566,19.013000000000034,5.39223,174.83,16.5,957.4,2.24748,-4.18501,-4.02827
|
||||
2016-02-12 08:46:12,40.3488692,115.7854554,18.531000000000006,5.22289,177.792,16.5,957.4,2.24002,-3.68792,-0.138169
|
||||
2016-02-12 08:46:13,40.348887,115.7854559,18.712000000000046,5.29908,176.119,16.5,957.4,2.24991,-4.16261,3.40361
|
||||
2016-02-12 08:46:14,40.3489049,115.7854579,18.876000000000033,5.18513,175.604,16.5,957.4,2.25581,-3.8008,4.82798
|
||||
2016-02-12 08:46:15,40.3489226,115.7854601,18.70300000000003,5.28377,174.7,16.5,957.4,2.25573,-4.42704,5.10422
|
||||
2016-02-12 08:46:16,40.3489402,115.7854625,18.723000000000013,5.07867,173.898,16.5,957.4,2.25527,-4.51496,5.49623
|
||||
2016-02-12 08:46:17,40.3489577,115.7854654,19.19500000000005,5.09618,175.518,16.5,957.4,2.25583,-4.36611,5.46923
|
||||
2016-02-12 08:46:18,40.3489756,115.7854678,18.958000000000027,5.11867,176.609,16.4,957.4,2.25081,-4.54822,5.33563
|
||||
2016-02-12 08:46:19,40.3489936,115.7854701,18.870000000000005,5.4073,176.796,16.4,957.4,2.25643,-4.75882,5.40797
|
||||
2016-02-12 08:46:20,40.3490114,115.7854722,18.610000000000014,5.58391,175.575,16.4,957.4,2.24946,-5.50701,5.03366
|
||||
2016-02-12 08:46:21,40.3490293,115.7854738,18.632000000000005,5.9049,176.505,16.4,957.4,2.25577,-4.83407,5.36974
|
||||
2016-02-12 08:46:22,40.3490473,115.7854761,18.746000000000038,5.91032,176.712,16.4,957.4,2.26172,-4.47638,5.38053
|
||||
2016-02-12 08:46:23,40.3490654,115.7854787,18.749000000000024,5.8128,175.791,16.4,957.4,2.24897,-4.6601,5.14888
|
||||
2016-02-12 08:46:24,40.3490833,115.7854808,18.635000000000048,5.96479,174.72,16.4,957.4,2.26363,-5.30749,5.15921
|
||||
2016-02-12 08:46:25,40.3491012,115.7854828,18.662000000000035,5.69564,172.094,16.4,957.4,2.2571,-0.473702,4.87027
|
||||
2016-02-12 08:46:26,40.3491131,115.7854838,18.741000000000042,3.78275,164.729,16.4,957.4,2.25241,-0.887125,4.75248
|
||||
2016-02-12 08:46:27,40.3491144,115.7854838,18.359000000000037,3.20324,161.213,16.4,957.4,2.25493,-3.26957,5.54402
|
||||
2016-02-12 08:46:28,40.3491142,115.7854837,18.223000000000013,2.40983,155.756,16.3,957.4,2.25721,-3.09028,5.36807
|
||||
2016-02-12 08:46:29,40.3491141,115.7854836,18.116000000000042,2.11352,157.024,16.3,957.4,2.25414,-2.34086,5.54559
|
||||
2016-02-12 08:46:30,40.349114,115.7854839,18.098000000000013,1.80536,155.986,16.3,957.4,2.24952,-2.62845,5.28901
|
||||
2016-02-12 08:46:31,40.3491139,115.7854841,18.104000000000042,1.53764,153.71,16.3,957.4,2.25496,-2.72331,4.91931
|
||||
2016-02-12 08:46:32,40.3491141,115.785484,18.013000000000034,1.25194,135.14,16.3,957.4,2.25252,-2.51398,5.07391
|
||||
2016-02-12 08:46:33,40.3491145,115.7854839,18.096000000000004,0.909581,119.528,16.3,957.4,2.26502,-2.23522,4.9321
|
||||
2016-02-12 08:46:34,40.3491148,115.7854839,18.11500000000001,1.00363,116.368,16.3,957.4,2.25633,-1.86102,4.94461
|
||||
2016-02-12 08:46:35,40.3491147,115.7854839,18.156000000000006,1.29567,116.279,16.3,957.4,2.24816,-3.27238,4.93432
|
||||
2016-02-12 08:46:36,40.3491148,115.7854839,18.29200000000003,1.80343,117.547,16.3,957.4,2.25323,-3.03301,5.0516
|
||||
2016-02-12 08:46:37,40.3491153,115.7854844,18.262,2.20721,129.988,16.3,957.4,2.23802,-2.33693,-3.56446
|
||||
2016-02-12 08:46:38,40.349117,115.7854858,18.539000000000044,2.41458,130.441,16.3,957.4,2.25003,-3.03663,-32.2437
|
||||
2016-02-12 08:46:39,40.3491192,115.7854854,18.525000000000034,2.51248,120.631,16.3,957.4,2.2485,-2.14594,-62.1644
|
||||
2016-02-12 08:46:40,40.3491202,115.7854843,18.311000000000035,2.39774,119.713,16.3,957.4,2.26122,-1.66167,-82.6586
|
||||
2016-02-12 08:46:41,40.3491201,115.7854826,18.40100000000001,2.6669,112.549,16.3,957.4,2.24148,-9.69869,-94.4252
|
||||
2016-02-12 08:46:42,40.349119,115.785474,18.451999999999998,3.58092,114.107,16.3,957.4,2.24515,-8.18198,-94.3401
|
||||
2016-02-12 08:46:43,40.3491177,115.7854533,18.446000000000026,4.36065,111.498,16.3,957.4,2.24984,-6.39408,-94.1511
|
||||
2016-02-12 08:46:44,40.3491158,115.7854241,18.548000000000002,4.83268,107.56,16.2,957.4,2.25608,-5.52217,-93.985
|
||||
2016-02-12 08:46:45,40.3491136,115.7853904,18.562000000000012,5.63733,104.996,16.2,957.4,2.25285,-3.73582,-93.9406
|
||||
2016-02-12 08:46:46,40.3491117,115.7853556,18.718000000000018,5.98308,99.2656,16.2,957.4,2.25147,-1.55215,-94.2177
|
||||
2016-02-12 08:46:47,40.3491101,115.7853231,18.66900000000004,5.99918,99.0378,16.2,957.4,2.25063,-1.4597,-93.9427
|
||||
2016-02-12 08:46:48,40.3491088,115.7852936,18.864000000000033,5.58521,98.8318,16.2,957.4,2.25688,-1.74784,-94.2138
|
||||
2016-02-12 08:46:49,40.3491076,115.7852665,18.647000000000048,5.06914,102.902,16.2,957.4,2.25241,-1.92998,-93.8656
|
||||
2016-02-12 08:46:50,40.3491069,115.7852413,18.53800000000001,4.57836,107.973,16.2,957.4,2.25181,-1.92374,-92.0432
|
||||
2016-02-12 08:46:51,40.3491072,115.785218,18.536,4.43143,115.332,16.2,957.4,2.25353,-2.10617,-85.8288
|
||||
2016-02-12 08:46:52,40.3491093,115.7851963,18.79000000000002,4.43013,123.816,16.2,957.4,2.25323,-2.65061,-78.6565
|
||||
2016-02-12 08:46:53,40.3491127,115.7851749,18.78800000000001,4.58193,121.516,16.2,957.4,2.24977,-2.85184,-78.4132
|
||||
2016-02-12 08:46:54,40.3491161,115.7851532,18.649,4.63984,120.281,16.2,957.4,2.2551,-3.52654,-78.4899
|
||||
2016-02-12 08:46:55,40.3491197,115.7851304,18.391999999999996,4.82902,119.083,16.2,957.4,2.25016,-3.82069,-78.4761
|
||||
2016-02-12 08:46:56,40.3491238,115.7851057,18.468999999999994,4.82599,118.463,16.2,957.4,2.25212,-4.37022,-78.4759
|
||||
2016-02-12 08:46:57,40.3491283,115.7850782,18.476,5.24285,115.506,16.2,957.4,2.2514,-5.29125,-78.4982
|
||||
2016-02-12 08:46:58,40.3491332,115.7850471,18.605999999999995,5.63871,114.027,16.2,957.4,2.25888,-4.89839,-78.6514
|
||||
2016-02-12 08:46:59,40.3491386,115.7850129,18.64300000000003,6.21575,110.912,16.2,957.4,2.23991,-4.85329,-78.5702
|
||||
2016-02-12 08:47:00,40.3491446,115.7849764,18.474000000000046,6.52411,109.303,16.2,957.4,2.25149,-4.43201,-78.5637
|
||||
2016-02-12 08:47:01,40.349151,115.784938,18.80800000000005,6.77679,107.561,16.2,957.4,2.24558,-4.74183,-78.8267
|
||||
2016-02-12 08:47:02,40.3491576,115.7848981,18.956999999999994,7.09753,106.696,16.2,957.4,2.25637,-4.52781,-78.8386
|
||||
2016-02-12 08:47:03,40.3491644,115.7848569,18.587000000000046,7.33927,106.749,16.2,957.4,2.26023,-4.96421,-78.5891
|
||||
2016-02-12 08:47:04,40.3491715,115.784814,18.772000000000048,7.50092,106.624,16.2,957.4,2.25672,-5.29089,-78.658
|
||||
2016-02-12 08:47:05,40.3491788,115.7847687,18.714,7.58947,106.181,16.2,957.4,2.24472,-4.61858,-78.6736
|
||||
2016-02-12 08:47:06,40.3491862,115.7847216,18.861000000000047,7.58499,106.907,16.2,957.4,2.26091,-2.96341,-78.4489
|
||||
2016-02-12 08:47:07,40.3491932,115.7846768,19.076000000000022,7.63955,107.249,16.1,957.4,2.24757,-2.96918,-78.5038
|
||||
2016-02-12 08:47:08,40.3492001,115.7846343,18.950000000000045,7.56674,107.19,16.1,957.4,2.25598,-1.65859,-78.5213
|
||||
2016-02-12 08:47:09,40.3492063,115.7845963,18.875,6.51291,106.081,16.1,957.4,2.25284,0.87737,-78.6438
|
||||
2016-02-12 08:47:10,40.3492115,115.7845658,18.902000000000044,5.94177,109.224,16.1,957.4,2.25329,-0.890602,-75.947
|
||||
2016-02-12 08:47:11,40.3492163,115.7845434,18.91700000000003,3.39562,124.224,16.1,957.4,2.25078,5.36068,-61.0265
|
||||
2016-02-12 08:47:12,40.3492168,115.7845391,18.373000000000047,2.35105,144.149,16.1,957.4,2.25335,0.560695,-45.7276
|
||||
2016-02-12 08:47:13,40.3492155,115.784539,18.483000000000004,0.893619,174.724,16.1,957.4,2.25703,-1.3111,-23.1751
|
||||
2016-02-12 08:47:14,40.3492136,115.7845378,18.56800000000004,0.103818,119.339,16.1,957.4,2.25902,-1.38589,10.1569
|
||||
2016-02-12 08:47:15,40.3492129,115.7845354,18.382000000000005,0.0326814,130.99,16.1,957.4,2.25777,-1.22882,35.8235
|
||||
2016-02-12 08:47:16,40.3492121,115.7845341,18.218999999999994,0.387784,31.1506,16.1,957.4,2.25355,-1.60541,40.0295
|
||||
2016-02-12 08:47:17,40.3492103,115.7845329,18.116000000000042,0.509455,87.3243,16.1,957.4,2.23954,-2.1407,39.683
|
||||
2016-02-12 08:47:18,40.3492105,115.7845333,17.775000000000034,0.381268,100.864,16.1,957.4,2.25254,-1.36341,39.8437
|
||||
2016-02-12 08:47:19,40.3492113,115.7845336,17.50200000000001,0.370224,45.6902,16.1,957.4,2.25597,-1.52545,39.8108
|
||||
2016-02-12 08:47:20,40.3492121,115.7845342,17.311000000000035,0.349136,85.5964,16.1,957.4,2.24971,-0.617308,39.8674
|
||||
2016-02-12 08:47:21,40.349212,115.7845348,16.980000000000018,0.418138,75.5565,16.1,957.4,2.25604,-1.80859,39.3206
|
||||
2016-02-12 08:47:22,40.3492118,115.7845347,16.562000000000012,0.729231,92.0266,16.1,957.4,2.24718,-1.91448,39.8363
|
||||
2016-02-12 08:47:23,40.3492116,115.784534,16.108000000000004,0.752719,77.3196,16.1,957.4,2.2566,-2.7967,40.4346
|
||||
2016-02-12 08:47:24,40.3492117,115.7845338,15.56800000000004,0.876472,112.876,16.2,957.6,2.24441,-2.74975,40.4632
|
||||
2016-02-12 08:47:25,40.3492114,115.7845344,14.550000000000011,0.910692,111.859,16.2,957.6,2.24345,-3.14775,40.1516
|
||||
2016-02-12 08:47:26,40.3492114,115.7845344,13.948000000000036,1.52511,98.9897,16.2,957.4,2.25048,-2.48058,39.9685
|
||||
2016-02-12 08:47:27,40.3492112,115.7845344,13.17100000000005,1.46838,143.535,16.2,957.4,2.25029,1.99029,39.8544
|
||||
2016-02-12 08:47:28,40.3492081,115.7845311,12.235000000000014,1.59666,132.338,16.2,957.4,2.24882,0.489737,39.9297
|
||||
2016-02-12 08:47:29,40.3492023,115.7845249,11.288000000000011,2.02849,101.22,16.2,957.6,2.24682,0.807989,40.035
|
||||
2016-02-12 08:47:30,40.3491942,115.7845164,10.459000000000003,2.41615,81.7842,16.2,957.6,2.24377,-1.64567,39.8959
|
||||
2016-02-12 08:47:31,40.3491866,115.7845083,9.474000000000046,2.36492,77.0593,16.2,957.4,2.25103,-9.16326,40.0477
|
||||
2016-02-12 08:47:32,40.3491841,115.7845057,8.593000000000018,2.04153,85.8787,16.2,957.4,2.25579,-3.23226,40.0162
|
||||
2016-02-12 08:47:33,40.3491831,115.7845048,7.691000000000031,2.01561,84.4265,16.2,957.4,2.24309,2.06283,40.2772
|
||||
2016-02-12 08:47:34,40.3491783,115.7844987,6.6720000000000255,2.23031,79.3183,16.2,957.4,2.24866,3.54644,46.1237
|
||||
2016-02-12 08:47:35,40.3491704,115.7844869,5.802000000000021,2.77827,78.7524,16.2,957.6,2.24481,0.782934,49.3694
|
||||
2016-02-12 08:47:36,40.3491614,115.7844734,5.021000000000015,2.6489,80.7009,16.2,957.6,2.24837,-3.47997,49.2569
|
||||
|
@ -1,95 +0,0 @@
|
||||
# testconfig.yaml
|
||||
|
||||
horizontal_pixels: 500 # Width of the concentration map in pixels
|
||||
vertical_pixels: 100 # Height of the concentration map in pixels
|
||||
num_plumes: 10 # Number of Gaussian plumes
|
||||
groupiness: 0.5 # Groupiness of the plumes (0.0 to 1.0)
|
||||
spread: 0.1 # Spread of the plumes (0.0 to 1.0)
|
||||
wind_reference_height: 10 # Reference height for wind speed calculation (m)
|
||||
windspeed_avg: 5 # Average wind speed at 10m height (m/s)
|
||||
windspeed_rel_std: 0.2 # Relative standard deviation of wind speed (0.0 to inf), recommend 0.2-0.4
|
||||
surface_roughness: 0.1 # Surface roughness length (m)
|
||||
seed: 42 # Random seed for reproducibility
|
||||
simplex_octaves: 4 # Number of octaves for simplex noise (1 to inf, def 1)
|
||||
simplex_persistence: 0.7 # Persistence of simplex noise (0.0 to 1.0, def 0.5) - specifies the amplitude of each octave relative to the one below it
|
||||
simplex_lacunarity: 2.0 # Lacunarity of simplex noise (1.0 to inf, def 2.0) - specifies the frequency of each octave relative to the one below it
|
||||
winddir_avg: 0.0 # Average wind direction in degrees rel to plane (0 is CW)
|
||||
winddir_std: 10 # Standard deviation of wind direction in degrees
|
||||
timestamp: "2022-09-26 02:03:00"
|
||||
flight_time_seconds: 1000
|
||||
sample_frequency: 1
|
||||
start_coords:
|
||||
- 54.87667
|
||||
- 15.41
|
||||
transect_azimuth: 260 # the wind will start off 90 degrees CW to this azimuth, and is modified relative to that by the winddir_avg. 260 is a good value to test N problems
|
||||
sampling_altitude_ato_range:
|
||||
- -10 # negative values should be fine
|
||||
- 100
|
||||
sampling_horizontal_range:
|
||||
- 50
|
||||
- 950
|
||||
scene_altitude_range:
|
||||
- -20
|
||||
- 120
|
||||
scene_horizontal_range:
|
||||
- 0
|
||||
- 1000
|
||||
number_of_transects: 10
|
||||
gases:
|
||||
ch4:
|
||||
- 1.95
|
||||
- 10.0
|
||||
co2:
|
||||
- 380.0
|
||||
- 500.0
|
||||
c2h6:
|
||||
- 0.0
|
||||
- 1.0
|
||||
temperature: 10.0
|
||||
pressure: 1000.0
|
||||
|
||||
output_dir: ./gasflux_reports
|
||||
|
||||
algorithmic_baseline_settings:
|
||||
algorithm: fastchrom
|
||||
|
||||
semivariogram_settings:
|
||||
model: spherical
|
||||
estimator: cressie
|
||||
n_lags: 20
|
||||
bin_func: even
|
||||
fit_method: lm
|
||||
maxlag: 100
|
||||
#fit_sigma: linear
|
||||
tolerance: 10
|
||||
azimuth: 0
|
||||
bandwidth: 20
|
||||
|
||||
ordinary_kriging_settings:
|
||||
min_points: 3
|
||||
max_points: 100
|
||||
grid_resolution: 500
|
||||
min_nodes: 10
|
||||
cut_ground: False
|
||||
y_min: ~
|
||||
|
||||
required_cols:
|
||||
latitude: [-90, 90]
|
||||
longitude: [-180, 180]
|
||||
height_ato: [-100, 500]
|
||||
windspeed: [0, 30]
|
||||
winddir: [0, 360]
|
||||
temperature: [-50, 60]
|
||||
pressure: [900, 1100]
|
||||
|
||||
filters:
|
||||
course_filter:
|
||||
azimuth_filter: 10
|
||||
azimuth_window: 5
|
||||
elevation_filter: 5
|
||||
|
||||
strategies:
|
||||
background: "algorithm"
|
||||
sensor: "insitu"
|
||||
spatial: "curtain"
|
||||
interpolation: "kriging"
|
||||
@ -7,7 +7,6 @@ Jinja2==3.1.6
|
||||
joblib==1.3.2
|
||||
matplotlib==3.10.0
|
||||
molmass==2023.8.30
|
||||
noise==1.2.2
|
||||
numpy==2.1.3
|
||||
pandas==2.2.3
|
||||
plotly==5.20.0
|
||||
@ -18,3 +17,8 @@ scikit-gstat==1.0.19
|
||||
scikit-image==0.24.0
|
||||
scipy==1.15.1
|
||||
simplekml==1.3.6
|
||||
flask
|
||||
werkzeug
|
||||
flask-cors
|
||||
waitress
|
||||
psutil
|
||||
|
||||
30
run_api.py
Normal file
30
run_api.py
Normal file
@ -0,0 +1,30 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add the project root and src directory to PYTHONPATH
|
||||
project_root = Path(__file__).parent.absolute()
|
||||
src_dir = project_root / "src"
|
||||
|
||||
sys.path.append(str(project_root))
|
||||
sys.path.append(str(src_dir))
|
||||
|
||||
# Set environment variables for Flask
|
||||
os.environ['FLASK_APP'] = 'src/gasflux/app.py'
|
||||
os.environ['FLASK_ENV'] = 'development'
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting GasFlux Web API...")
|
||||
|
||||
# Import and run the app
|
||||
try:
|
||||
import gasflux.app as gasflux_app
|
||||
app = gasflux_app.app
|
||||
print("GasFlux app imported successfully")
|
||||
print("Starting Flask development server on http://0.0.0.0:5000")
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
except Exception as e:
|
||||
print(f"Error starting GasFlux app: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
117
server_waitress.py
Normal file
117
server_waitress.py
Normal file
@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GasFlux Web API Server using Waitress WSGI server.
|
||||
|
||||
This is the production server entry point for the GasFlux Web API.
|
||||
Includes full GasFlux processing capabilities with task pool management.
|
||||
|
||||
Features:
|
||||
- File upload and processing (.xlsx, .xls)
|
||||
- Asynchronous task processing with background workers
|
||||
- Task pool management with pagination and filtering
|
||||
- Real-time task status monitoring
|
||||
- Report generation and file download
|
||||
- Health monitoring and statistics
|
||||
|
||||
API Endpoints:
|
||||
- /upload - File upload and processing initiation
|
||||
- /task/<task_id> - Task status management
|
||||
- /tasks - Task pool management (NEW)
|
||||
- /download/<filename> - File download
|
||||
- /reports - Report listing
|
||||
- /health - Health monitoring
|
||||
- /stats - System statistics
|
||||
- /config - Configuration info
|
||||
- / - Web interface
|
||||
|
||||
Usage:
|
||||
python server_waitress.py
|
||||
|
||||
Or package with PyInstaller:
|
||||
pyinstaller --onefile --name GasFluxAPI server_waitress.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add the project root and src directory to PYTHONPATH
|
||||
project_root = Path(__file__).parent.absolute()
|
||||
src_dir = project_root / "src"
|
||||
|
||||
sys.path.insert(0, str(project_root))
|
||||
sys.path.insert(0, str(src_dir))
|
||||
|
||||
# Set environment variables for production
|
||||
os.environ['FLASK_APP'] = 'src/gasflux/app.py'
|
||||
os.environ['FLASK_ENV'] = 'production'
|
||||
|
||||
def main():
|
||||
"""Main entry point for the GasFlux Web API server."""
|
||||
print("Starting GasFlux Web API with Waitress...")
|
||||
|
||||
try:
|
||||
# Import the Flask app
|
||||
import gasflux.app as gasflux_app
|
||||
from src.gasflux.app import Config
|
||||
|
||||
app = gasflux_app.app
|
||||
print("✓ GasFlux app imported successfully")
|
||||
print("✓ Task pool management enabled")
|
||||
print("✓ All blueprints loaded: upload, tasks, task_pool, download, reports, health, stats, config, web")
|
||||
|
||||
# Import waitress
|
||||
from waitress import serve
|
||||
print("✓ Waitress WSGI server imported")
|
||||
|
||||
# Server configuration from environment variables
|
||||
host = Config.HOST
|
||||
port = Config.PORT
|
||||
threads = Config.THREADS
|
||||
connection_limit = Config.CONNECTION_LIMIT
|
||||
channel_timeout = Config.CHANNEL_TIMEOUT
|
||||
|
||||
print(f"Starting Waitress server on {host}:{port}")
|
||||
print(f"- Threads: {threads}")
|
||||
print(f"- Connection limit: {connection_limit}")
|
||||
print(f"- Channel timeout: {channel_timeout}s")
|
||||
print("Press Ctrl+C to stop the server")
|
||||
print("=" * 50)
|
||||
print("Available API endpoints:")
|
||||
print(" POST /upload - Upload files for processing")
|
||||
print(" GET /task/<id> - Get task status")
|
||||
print(" PUT /task/<id> - Update task status")
|
||||
print(" DEL /task/<id> - Delete task")
|
||||
print(" GET /tasks - List tasks (paginated)")
|
||||
print(" GET /tasks/stats - Task pool statistics")
|
||||
print(" GET /tasks/active - Active tasks")
|
||||
print(" GET /tasks/queue - Queued tasks")
|
||||
print(" GET /download/<file> - Download files")
|
||||
print(" GET /reports - List reports")
|
||||
print(" GET /health - Health check")
|
||||
print(" GET /stats - System stats")
|
||||
print(" GET /config - Configuration")
|
||||
print(" GET / - Web interface")
|
||||
print("=" * 50)
|
||||
print(f"Configuration: {Config.to_dict()}")
|
||||
|
||||
# Start the server with valid Waitress parameters
|
||||
serve(
|
||||
app,
|
||||
host=host,
|
||||
port=port,
|
||||
threads=threads,
|
||||
connection_limit=connection_limit,
|
||||
channel_timeout=channel_timeout,
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nServer stopped by user")
|
||||
except Exception as e:
|
||||
print(f"✗ Error starting GasFlux app: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
728
src/gasflux/app.py
Normal file
728
src/gasflux/app.py
Normal file
@ -0,0 +1,728 @@
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import uuid
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from flask import Flask, request, jsonify, send_file, render_template_string, url_for, g
|
||||
from flask_cors import CORS
|
||||
from werkzeug.utils import secure_filename
|
||||
import yaml
|
||||
|
||||
# Shared utilities imported from shared.py
|
||||
try:
|
||||
# Try relative import (when run as part of package)
|
||||
from .shared import task_status, TASK_STATUS_PENDING, TASK_STATUS_PROCESSING, TASK_STATUS_COMPLETED, TASK_STATUS_FAILED
|
||||
except ImportError:
|
||||
# Fallback to absolute import (when run directly)
|
||||
from shared import task_status, TASK_STATUS_PENDING, TASK_STATUS_PROCESSING, TASK_STATUS_COMPLETED, TASK_STATUS_FAILED
|
||||
|
||||
# Blueprints will be imported after app initialization to avoid circular imports
|
||||
|
||||
# Environment-based configuration management
|
||||
class Config:
|
||||
"""Configuration management using environment variables with defaults."""
|
||||
|
||||
# Server configuration
|
||||
HOST = os.getenv('GASFLUX_HOST', '0.0.0.0')
|
||||
PORT = int(os.getenv('GASFLUX_PORT', '5000'))
|
||||
DEBUG = os.getenv('GASFLUX_DEBUG', 'false').lower() in ('true', '1', 'yes', 'on')
|
||||
|
||||
# Directory configuration
|
||||
BASE_DIR = None # Will be set dynamically
|
||||
UPLOAD_FOLDER_NAME = os.getenv('GASFLUX_UPLOAD_FOLDER', 'web_api_data/uploads')
|
||||
OUTPUT_FOLDER_NAME = os.getenv('GASFLUX_OUTPUT_FOLDER', 'web_api_data/outputs')
|
||||
|
||||
# File size limits (in bytes)
|
||||
MAX_CONTENT_LENGTH = int(os.getenv('GASFLUX_MAX_CONTENT_LENGTH', str(100 * 1024 * 1024))) # 100MB
|
||||
|
||||
# Logging configuration
|
||||
LOG_LEVEL = os.getenv('GASFLUX_LOG_LEVEL', 'INFO').upper()
|
||||
LOG_FILE = os.getenv('GASFLUX_LOG_FILE', 'logs/gasflux_api.log')
|
||||
|
||||
# CORS configuration
|
||||
CORS_ORIGINS = os.getenv('GASFLUX_CORS_ORIGINS', '*').split(',')
|
||||
|
||||
# Task management
|
||||
TASK_CLEANUP_INTERVAL = int(os.getenv('GASFLUX_TASK_CLEANUP_INTERVAL', '3600')) # 1 hour in seconds
|
||||
MAX_TASK_AGE = int(os.getenv('GASFLUX_MAX_TASK_AGE', str(24 * 3600))) # 24 hours in seconds
|
||||
|
||||
# Performance tuning
|
||||
THREADS = int(os.getenv('GASFLUX_THREADS', '8')) # Waitress threads
|
||||
CONNECTION_LIMIT = int(os.getenv('GASFLUX_CONNECTION_LIMIT', '100'))
|
||||
CHANNEL_TIMEOUT = int(os.getenv('GASFLUX_CHANNEL_TIMEOUT', '300')) # 5 minutes
|
||||
|
||||
@classmethod
|
||||
def init_base_dir(cls):
|
||||
"""Initialize base directory based on environment."""
|
||||
try:
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Running in PyInstaller bundle
|
||||
cls.BASE_DIR = Path(sys.executable).parent
|
||||
else:
|
||||
# Running in normal Python environment
|
||||
cls.BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
except:
|
||||
# Fallback to current working directory
|
||||
cls.BASE_DIR = Path.cwd()
|
||||
|
||||
# Initialize directories based on config
|
||||
cls.init_directories()
|
||||
|
||||
@classmethod
|
||||
def init_directories(cls, output_dir=None):
|
||||
"""Initialize upload and output directories."""
|
||||
if output_dir:
|
||||
# Use config-based output directory
|
||||
output_base = Path(output_dir)
|
||||
if not output_base.is_absolute():
|
||||
output_base = cls.BASE_DIR / output_base
|
||||
else:
|
||||
# Use default relative paths
|
||||
output_base = cls.BASE_DIR
|
||||
|
||||
# Set upload and output directories relative to output_base
|
||||
cls.UPLOAD_FOLDER = output_base / "uploads"
|
||||
cls.OUTPUT_FOLDER = output_base / "outputs"
|
||||
|
||||
# Create directories
|
||||
cls.UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True)
|
||||
cls.OUTPUT_FOLDER.mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"Directories initialized - Upload: {cls.UPLOAD_FOLDER}, Output: {cls.OUTPUT_FOLDER}")
|
||||
|
||||
@classmethod
|
||||
def update_directories_from_config(cls, config_path=None):
|
||||
"""Update directories based on config file."""
|
||||
if config_path and Path(config_path).exists():
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
output_dir = config.get('output_dir')
|
||||
if output_dir:
|
||||
cls.init_directories(output_dir)
|
||||
logger.info(f"Directories updated from config: {config_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to update directories from config {config_path}: {e}")
|
||||
else:
|
||||
logger.info("Using default directory configuration")
|
||||
|
||||
@classmethod
|
||||
def get_log_level(cls):
|
||||
"""Get logging level from string."""
|
||||
levels = {
|
||||
'DEBUG': logging.DEBUG,
|
||||
'INFO': logging.INFO,
|
||||
'WARNING': logging.WARNING,
|
||||
'ERROR': logging.ERROR,
|
||||
'CRITICAL': logging.CRITICAL
|
||||
}
|
||||
return levels.get(cls.LOG_LEVEL, logging.INFO)
|
||||
|
||||
@classmethod
|
||||
def to_dict(cls):
|
||||
"""Return configuration as dictionary for debugging."""
|
||||
return {
|
||||
'host': cls.HOST,
|
||||
'port': cls.PORT,
|
||||
'debug': cls.DEBUG,
|
||||
'base_dir': str(cls.BASE_DIR) if cls.BASE_DIR else None,
|
||||
'upload_folder': str(cls.UPLOAD_FOLDER) if hasattr(cls, 'UPLOAD_FOLDER') else None,
|
||||
'output_folder': str(cls.OUTPUT_FOLDER) if hasattr(cls, 'OUTPUT_FOLDER') else None,
|
||||
'max_content_length': cls.MAX_CONTENT_LENGTH,
|
||||
'log_level': cls.LOG_LEVEL,
|
||||
'log_file': cls.LOG_FILE,
|
||||
'cors_origins': cls.CORS_ORIGINS,
|
||||
'task_cleanup_interval': cls.TASK_CLEANUP_INTERVAL,
|
||||
'max_task_age': cls.MAX_TASK_AGE,
|
||||
'threads': cls.THREADS,
|
||||
'connection_limit': cls.CONNECTION_LIMIT,
|
||||
'channel_timeout': cls.CHANNEL_TIMEOUT
|
||||
}
|
||||
|
||||
# Initialize logging with environment-based configuration
|
||||
logging.basicConfig(
|
||||
level=Config.get_log_level(),
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(), # Console output
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger("gasflux_api")
|
||||
logger.info("Basic logging initialized")
|
||||
|
||||
|
||||
def log_performance(func):
|
||||
"""Decorator to log function performance."""
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
start_time = time.time()
|
||||
func_name = func.__name__
|
||||
logger.debug(f"PERF: Starting {func_name}")
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
duration = time.time() - start_time
|
||||
logger.info(f"PERF: {func_name} completed in {duration:.3f}s")
|
||||
return result
|
||||
except Exception as e:
|
||||
duration = time.time() - start_time
|
||||
logger.error(f"PERF: {func_name} failed after {duration:.3f}s - Error: {str(e)}")
|
||||
raise
|
||||
return wrapper
|
||||
|
||||
# Task status management
|
||||
# Task status constants and storage moved to shared.py
|
||||
|
||||
def update_task_status(task_id, status, message=None, results=None, error=None):
|
||||
"""Update task status in the global dictionary."""
|
||||
timestamp = time.time()
|
||||
old_status = task_status.get(task_id, {}).get("status", "unknown")
|
||||
|
||||
task_status[task_id] = {
|
||||
"status": status,
|
||||
"message": message,
|
||||
"results": results,
|
||||
"error": error,
|
||||
"updated_at": timestamp
|
||||
}
|
||||
|
||||
# Log detailed status change with context
|
||||
log_msg = f"Task {task_id} status changed: {old_status} -> {status}"
|
||||
if message:
|
||||
log_msg += f" | Message: {message}"
|
||||
if results:
|
||||
log_msg += f" | Results count: {len(results) if isinstance(results, list) else 'N/A'}"
|
||||
if error:
|
||||
log_msg += f" | Error: {error}"
|
||||
|
||||
log_level = logging.ERROR if status == TASK_STATUS_FAILED else logging.INFO
|
||||
logger.log(log_level, log_msg)
|
||||
|
||||
# Update task statistics
|
||||
stats_collector.record_task_status_change(old_status, status)
|
||||
|
||||
|
||||
# Statistics and Monitoring
|
||||
class APIStatsCollector:
|
||||
"""Collect and manage API statistics."""
|
||||
|
||||
def __init__(self):
|
||||
self.start_time = time.time()
|
||||
self.reset_stats()
|
||||
|
||||
def reset_stats(self):
|
||||
"""Reset all statistics."""
|
||||
self.stats = {
|
||||
'requests': {
|
||||
'total': 0,
|
||||
'by_method': {},
|
||||
'by_endpoint': {},
|
||||
'by_status': {},
|
||||
'response_times': [],
|
||||
'errors': 0
|
||||
},
|
||||
'tasks': {
|
||||
'total_created': 0,
|
||||
'total_completed': 0,
|
||||
'total_failed': 0,
|
||||
'by_status': {
|
||||
'pending': 0,
|
||||
'processing': 0,
|
||||
'completed': 0,
|
||||
'failed': 0
|
||||
},
|
||||
'processing_times': []
|
||||
},
|
||||
'performance': {
|
||||
'avg_response_time': 0,
|
||||
'max_response_time': 0,
|
||||
'min_response_time': float('inf'),
|
||||
'uptime_seconds': time.time() - self.start_time
|
||||
}
|
||||
}
|
||||
|
||||
def record_request(self, method, endpoint, status_code, response_time):
|
||||
"""Record an API request."""
|
||||
self.stats['requests']['total'] += 1
|
||||
|
||||
# Method stats
|
||||
if method not in self.stats['requests']['by_method']:
|
||||
self.stats['requests']['by_method'][method] = 0
|
||||
self.stats['requests']['by_method'][method] += 1
|
||||
|
||||
# Endpoint stats
|
||||
if endpoint not in self.stats['requests']['by_endpoint']:
|
||||
self.stats['requests']['by_endpoint'][endpoint] = 0
|
||||
self.stats['requests']['by_endpoint'][endpoint] += 1
|
||||
|
||||
# Status stats
|
||||
status_category = str(status_code // 100 * 100) # 200, 400, 500, etc.
|
||||
if status_category not in self.stats['requests']['by_status']:
|
||||
self.stats['requests']['by_status'][status_category] = 0
|
||||
self.stats['requests']['by_status'][status_category] += 1
|
||||
|
||||
# Response time stats
|
||||
self.stats['requests']['response_times'].append(response_time)
|
||||
|
||||
# Keep only last 1000 response times for memory efficiency
|
||||
if len(self.stats['requests']['response_times']) > 1000:
|
||||
self.stats['requests']['response_times'] = self.stats['requests']['response_times'][-1000:]
|
||||
|
||||
# Error tracking
|
||||
if status_code >= 400:
|
||||
self.stats['requests']['errors'] += 1
|
||||
|
||||
# Update performance stats
|
||||
self._update_performance_stats()
|
||||
|
||||
def record_task_status_change(self, old_status, new_status):
|
||||
"""Record task status changes."""
|
||||
if old_status == "unknown": # New task
|
||||
self.stats['tasks']['total_created'] += 1
|
||||
|
||||
if new_status == TASK_STATUS_COMPLETED:
|
||||
self.stats['tasks']['total_completed'] += 1
|
||||
elif new_status == TASK_STATUS_FAILED:
|
||||
self.stats['tasks']['total_failed'] += 1
|
||||
|
||||
# Update status counts
|
||||
for status in [old_status, new_status]:
|
||||
if status in self.stats['tasks']['by_status']:
|
||||
if status == old_status and old_status != "unknown":
|
||||
self.stats['tasks']['by_status'][old_status] -= 1
|
||||
elif status == new_status:
|
||||
self.stats['tasks']['by_status'][new_status] += 1
|
||||
|
||||
def record_task_completion_time(self, completion_time):
|
||||
"""Record task completion time."""
|
||||
self.stats['tasks']['processing_times'].append(completion_time)
|
||||
|
||||
# Keep only last 100 processing times
|
||||
if len(self.stats['tasks']['processing_times']) > 100:
|
||||
self.stats['tasks']['processing_times'] = self.stats['tasks']['processing_times'][-100:]
|
||||
|
||||
def _update_performance_stats(self):
|
||||
"""Update performance statistics."""
|
||||
response_times = self.stats['requests']['response_times']
|
||||
if response_times:
|
||||
self.stats['performance']['avg_response_time'] = sum(response_times) / len(response_times)
|
||||
self.stats['performance']['max_response_time'] = max(response_times)
|
||||
self.stats['performance']['min_response_time'] = min(response_times)
|
||||
|
||||
self.stats['performance']['uptime_seconds'] = time.time() - self.start_time
|
||||
|
||||
def get_summary(self):
|
||||
"""Get a summary of current statistics."""
|
||||
current_time = time.time()
|
||||
uptime = current_time - self.start_time
|
||||
|
||||
# Calculate rates
|
||||
requests_per_second = self.stats['requests']['total'] / max(uptime, 1)
|
||||
error_rate = (self.stats['requests']['errors'] / max(self.stats['requests']['total'], 1)) * 100
|
||||
|
||||
# Task completion rate
|
||||
total_tasks_processed = self.stats['tasks']['total_completed'] + self.stats['tasks']['total_failed']
|
||||
task_success_rate = (self.stats['tasks']['total_completed'] / max(total_tasks_processed, 1)) * 100
|
||||
|
||||
return {
|
||||
'summary': {
|
||||
'uptime_seconds': uptime,
|
||||
'uptime_formatted': self._format_uptime(uptime),
|
||||
'requests_total': self.stats['requests']['total'],
|
||||
'requests_per_second': round(requests_per_second, 2),
|
||||
'error_rate_percent': round(error_rate, 2),
|
||||
'active_tasks': len([t for t in task_status.values()
|
||||
if t.get('status') in [TASK_STATUS_PENDING, TASK_STATUS_PROCESSING]])
|
||||
},
|
||||
'requests': {
|
||||
'by_method': self.stats['requests']['by_method'],
|
||||
'by_status': self.stats['requests']['by_status'],
|
||||
'top_endpoints': dict(sorted(self.stats['requests']['by_endpoint'].items(),
|
||||
key=lambda x: x[1], reverse=True)[:10])
|
||||
},
|
||||
'tasks': {
|
||||
'total_created': self.stats['tasks']['total_created'],
|
||||
'total_completed': self.stats['tasks']['total_completed'],
|
||||
'total_failed': self.stats['tasks']['total_failed'],
|
||||
'success_rate_percent': round(task_success_rate, 2),
|
||||
'by_status': self.stats['tasks']['by_status']
|
||||
},
|
||||
'performance': {
|
||||
'avg_response_time_ms': round(self.stats['performance']['avg_response_time'] * 1000, 2),
|
||||
'max_response_time_ms': round(self.stats['performance']['max_response_time'] * 1000, 2),
|
||||
'min_response_time_ms': round(self.stats['performance']['min_response_time'] * 1000, 2) if self.stats['performance']['min_response_time'] != float('inf') else 0
|
||||
}
|
||||
}
|
||||
|
||||
def _format_uptime(self, seconds):
|
||||
"""Format uptime in human readable format."""
|
||||
days, remainder = divmod(int(seconds), 86400)
|
||||
hours, remainder = divmod(remainder, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
|
||||
parts = []
|
||||
if days > 0:
|
||||
parts.append(f"{days}d")
|
||||
if hours > 0:
|
||||
parts.append(f"{hours}h")
|
||||
if minutes > 0:
|
||||
parts.append(f"{minutes}m")
|
||||
parts.append(f"{seconds}s")
|
||||
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
# Global statistics collector
|
||||
stats_collector = APIStatsCollector()
|
||||
|
||||
# get_task_status moved to shared.py
|
||||
|
||||
# cleanup_old_tasks moved to shared.py
|
||||
|
||||
def process_data_async(task_id, data_path, config_path, job_output_dir):
|
||||
"""Background task to process data asynchronously."""
|
||||
logger.info(f"Job {task_id}: Background processing started for task {task_id}")
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
update_task_status(task_id, TASK_STATUS_PROCESSING, "Starting data processing...")
|
||||
|
||||
# 1. Load and override config FIRST
|
||||
logger.info(f"Job {task_id}: Loading configuration from {config_path}")
|
||||
config_start = time.time()
|
||||
|
||||
try:
|
||||
with open(config_path, 'r') as f:
|
||||
config = yaml.safe_load(f)
|
||||
logger.info(f"Job {task_id}: Configuration loaded successfully with {len(config)} keys")
|
||||
except Exception as e:
|
||||
logger.error(f"Job {task_id}: Failed to load config from {config_path}: {str(e)}")
|
||||
raise
|
||||
|
||||
# Update directories based on config output_dir
|
||||
Config.update_directories_from_config(config_path)
|
||||
|
||||
# Sync app.config with updated directories
|
||||
app.config['UPLOAD_FOLDER'] = Config.UPLOAD_FOLDER
|
||||
app.config['OUTPUT_FOLDER'] = Config.OUTPUT_FOLDER
|
||||
|
||||
# Update task status file path to new output directory
|
||||
from .shared import set_task_status_file_path, load_task_status_from_file
|
||||
set_task_status_file_path(Config.OUTPUT_FOLDER / "task_status.json")
|
||||
# 立即从新路径加载现有状态,避免后续保存清空文件
|
||||
load_task_status_from_file()
|
||||
|
||||
# Update job directories to be under the correct config-based paths
|
||||
from pathlib import Path
|
||||
job_upload_dir = Path(Config.UPLOAD_FOLDER) / task_id
|
||||
job_output_dir = Path(Config.OUTPUT_FOLDER) / task_id
|
||||
job_upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
job_output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Move uploaded files to the correct config-based directories
|
||||
try:
|
||||
import shutil
|
||||
|
||||
# Move data file to correct uploads directory
|
||||
if data_path.parent != job_upload_dir:
|
||||
new_data_path = job_upload_dir / data_path.name
|
||||
if data_path != new_data_path:
|
||||
shutil.move(str(data_path), str(new_data_path))
|
||||
data_path = new_data_path
|
||||
logger.info(f"Job {task_id}: Moved data file to {data_path}")
|
||||
|
||||
# Move config file to correct uploads directory (if it's a custom config)
|
||||
if config_path.parent != job_upload_dir and config_path.parent != Config.BASE_DIR:
|
||||
new_config_path = job_upload_dir / config_path.name
|
||||
if config_path != new_config_path:
|
||||
shutil.move(str(config_path), str(new_config_path))
|
||||
config_path = new_config_path
|
||||
logger.info(f"Job {task_id}: Moved config file to {config_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Job {task_id}: Failed to move uploaded files to configured directories: {str(e)}")
|
||||
|
||||
logger.debug(f"Job {task_id}: Keeping original output directory: {config.get('output_dir', 'not set')}")
|
||||
logger.debug(f"Job {task_id}: Updated directories - Upload: {Config.UPLOAD_FOLDER}, Output: {Config.OUTPUT_FOLDER}, Job output: {job_output_dir}")
|
||||
|
||||
config_duration = time.time() - config_start
|
||||
logger.info(f"Job {task_id}: Configuration processing completed in {config_duration:.3f}s")
|
||||
|
||||
update_task_status(task_id, TASK_STATUS_PROCESSING, "Configuration loaded, starting preprocessing...")
|
||||
|
||||
# 2. Data Preprocessing (files are already in correct directories)
|
||||
logger.info(f"Job {task_id}: Starting preprocessing phase...")
|
||||
preprocess_start = time.time()
|
||||
|
||||
processed_csv = data_path.parent / f"{data_path.stem}.processed.csv"
|
||||
logger.debug(f"Job {task_id}: Input file: {data_path}, Output file: {processed_csv}")
|
||||
|
||||
process_file(str(data_path), str(processed_csv), str(config_path))
|
||||
|
||||
preprocess_duration = time.time() - preprocess_start
|
||||
logger.info(f"Job {task_id}: Preprocessing completed in {preprocess_duration:.3f}s")
|
||||
|
||||
update_task_status(task_id, TASK_STATUS_PROCESSING, "Preprocessing completed, starting GasFlux analysis...")
|
||||
|
||||
# Write modified config to a temp file
|
||||
final_config_path = data_path.parent / "final_config.yaml"
|
||||
try:
|
||||
with open(final_config_path, 'w') as f:
|
||||
yaml.safe_dump(config, f)
|
||||
logger.info(f"Job {task_id}: Final config written to {final_config_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Job {task_id}: Failed to write final config: {str(e)}")
|
||||
raise
|
||||
|
||||
config_duration = time.time() - config_start
|
||||
logger.info(f"Job {task_id}: Configuration processing completed in {config_duration:.3f}s")
|
||||
|
||||
update_task_status(task_id, TASK_STATUS_PROCESSING, "Configuration loaded, starting GasFlux analysis...")
|
||||
|
||||
# 3. GasFlux Processing
|
||||
logger.info(f"Job {task_id}: Starting GasFlux analysis...")
|
||||
analysis_start = time.time()
|
||||
|
||||
process_main(processed_csv, final_config_path, task_id)
|
||||
|
||||
analysis_duration = time.time() - analysis_start
|
||||
logger.info(f"Job {task_id}: GasFlux analysis completed in {analysis_duration:.3f}s")
|
||||
|
||||
update_task_status(task_id, TASK_STATUS_PROCESSING, "GasFlux analysis completed, generating reports...")
|
||||
|
||||
# Collect results and generate full URLs
|
||||
logger.info(f"Job {task_id}: Collecting generated files from {job_output_dir}")
|
||||
results_start = time.time()
|
||||
results = []
|
||||
|
||||
try:
|
||||
for f in job_output_dir.rglob("*"):
|
||||
if f.is_file():
|
||||
rel_path = f.relative_to(app.config['OUTPUT_FOLDER']).as_posix()
|
||||
file_size = f.stat().st_size
|
||||
results.append({
|
||||
"name": f.name,
|
||||
"rel_path": rel_path,
|
||||
"download_url": f"/download/{rel_path}", # Relative URL that client can use
|
||||
"size": file_size
|
||||
})
|
||||
logger.debug(f"Job {task_id}: Found output file: {f.name} ({file_size} bytes)")
|
||||
|
||||
results_duration = time.time() - results_start
|
||||
logger.info(f"Job {task_id}: Results collection completed in {results_duration:.3f}s - {len(results)} files generated")
|
||||
|
||||
total_size = sum(r.get('size', 0) for r in results)
|
||||
logger.info(f"Job {task_id}: Total output size: {total_size} bytes across {len(results)} files")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Job {task_id}: Failed to collect results: {str(e)}")
|
||||
raise
|
||||
|
||||
total_duration = time.time() - start_time
|
||||
logger.info(f"Job {task_id}: Processing complete. Total duration: {total_duration:.3f}s, {len(results)} files generated.")
|
||||
|
||||
# Record task completion time for statistics
|
||||
stats_collector.record_task_completion_time(total_duration)
|
||||
|
||||
update_task_status(task_id, TASK_STATUS_COMPLETED, "Processing completed successfully", results=results)
|
||||
|
||||
except Exception as e:
|
||||
total_duration = time.time() - start_time
|
||||
logger.error(f"Job {task_id}: Processing failed after {total_duration:.3f}s - Error: {str(e)}", exc_info=True)
|
||||
|
||||
# Record failed task processing time for statistics
|
||||
stats_collector.record_task_completion_time(total_duration)
|
||||
logger.error(f"Job {task_id}: Failed task details - Data: {data_path}, Config: {config_path}, Output: {job_output_dir}")
|
||||
|
||||
# Try to capture any partial results
|
||||
partial_results = []
|
||||
try:
|
||||
for f in job_output_dir.rglob("*"):
|
||||
if f.is_file():
|
||||
rel_path = f.relative_to(app.config['OUTPUT_FOLDER']).as_posix()
|
||||
partial_results.append({
|
||||
"name": f.name,
|
||||
"rel_path": rel_path,
|
||||
"download_url": f"/download/{rel_path}", # Relative URL that client can use
|
||||
"size": f.stat().st_size,
|
||||
"note": "partial_result"
|
||||
})
|
||||
except Exception as collect_error:
|
||||
logger.warning(f"Job {task_id}: Failed to collect partial results: {str(collect_error)}")
|
||||
|
||||
error_msg = f"Processing failed: {str(e)}"
|
||||
if partial_results:
|
||||
error_msg += f" (partial results available: {len(partial_results)} files)"
|
||||
|
||||
update_task_status(task_id, TASK_STATUS_FAILED, error=error_msg, results=partial_results if partial_results else None)
|
||||
|
||||
# Import GasFlux modules
|
||||
logger.info("Importing GasFlux modules...")
|
||||
import_start = time.time()
|
||||
|
||||
try:
|
||||
# Try absolute imports first (more reliable)
|
||||
from src.gasflux.processing_pipelines import process_main
|
||||
from src.gasflux.data_processor import process_file
|
||||
from src.gasflux.reporting import generate_reports
|
||||
import_duration = time.time() - import_start
|
||||
logger.info(f"GasFlux modules imported successfully in {import_duration:.3f}s (absolute import)")
|
||||
except ImportError as e1:
|
||||
logger.warning(f"Absolute import failed, trying relative import: {e1}")
|
||||
try:
|
||||
from .processing_pipelines import process_main
|
||||
from .data_processor import process_file
|
||||
from .reporting import generate_reports
|
||||
import_duration = time.time() - import_start
|
||||
logger.info(f"GasFlux modules imported successfully in {import_duration:.3f}s (relative import)")
|
||||
except ImportError as e2:
|
||||
import_duration = time.time() - import_start
|
||||
logger.error(f"Failed to import GasFlux modules after {import_duration:.3f}s - Absolute error: {e1}, Relative error: {e2}")
|
||||
raise ImportError(f"Cannot import GasFlux modules: {e2}")
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app) # Initialize CORS
|
||||
|
||||
# Enhanced logging configuration after app initialization
|
||||
try:
|
||||
log_file_path = Path(Config.LOG_FILE)
|
||||
log_file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create file handler
|
||||
file_handler = logging.FileHandler(log_file_path, encoding='utf-8')
|
||||
file_handler.setLevel(Config.get_log_level())
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
# Add file handler to logger
|
||||
logger.addHandler(file_handler)
|
||||
logger.info(f"File logging initialized. Log file: {log_file_path.absolute()}")
|
||||
print(f"Log file: {log_file_path.absolute()}") # Also print to console
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to initialize file logging: {e}")
|
||||
logger.warning(f"Failed to initialize file logging: {e}")
|
||||
|
||||
logger.info("Flask application initialized")
|
||||
|
||||
# Request logging middleware
|
||||
@app.before_request
|
||||
def log_request_info():
|
||||
"""Log incoming request details."""
|
||||
g.start_time = time.time()
|
||||
logger.info(f"REQUEST: {request.method} {request.url} - IP: {request.remote_addr} - User-Agent: {request.headers.get('User-Agent', 'Unknown')}")
|
||||
|
||||
@app.after_request
|
||||
def log_response_info(response):
|
||||
"""Log response details."""
|
||||
duration = time.time() - g.start_time
|
||||
logger.info(f"RESPONSE: {request.method} {request.url} - Status: {response.status_code} - Duration: {duration:.3f}s")
|
||||
|
||||
# Record statistics
|
||||
endpoint = request.url_rule.rule if request.url_rule else request.path
|
||||
stats_collector.record_request(request.method, endpoint, response.status_code, duration)
|
||||
|
||||
return response
|
||||
|
||||
# Initialize configuration from environment variables
|
||||
Config.init_base_dir()
|
||||
|
||||
# Apply configuration to app (directories will be created dynamically based on config)
|
||||
# ALLOWED_DATA_EXTENSIONS and ALLOWED_CONFIG_EXTENSIONS moved to shared.py
|
||||
|
||||
app.config['MAX_CONTENT_LENGTH'] = Config.MAX_CONTENT_LENGTH
|
||||
# Don't set UPLOAD_FOLDER and OUTPUT_FOLDER here - they will be set dynamically per request
|
||||
# Set defaults to avoid KeyError if any handler reads before config is applied
|
||||
app.config.setdefault('UPLOAD_FOLDER', None)
|
||||
app.config.setdefault('OUTPUT_FOLDER', None)
|
||||
|
||||
# Log current configuration
|
||||
logger.info(f"Upload folder: {Config.UPLOAD_FOLDER}")
|
||||
logger.info(f"Output folder: {Config.OUTPUT_FOLDER}")
|
||||
logger.info(f"Configuration: {Config.to_dict()}")
|
||||
|
||||
# Ensure directories exist at startup
|
||||
def setup_directories():
|
||||
logger.info("Initializing application directories...")
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Check if directories already exist
|
||||
upload_exists = Config.UPLOAD_FOLDER.exists()
|
||||
output_exists = Config.OUTPUT_FOLDER.exists()
|
||||
|
||||
Config.UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True)
|
||||
Config.OUTPUT_FOLDER.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
duration = time.time() - start_time
|
||||
logger.info(f"Directories initialized in {duration:.3f}s: {Config.UPLOAD_FOLDER} ({'existing' if upload_exists else 'created'}), {Config.OUTPUT_FOLDER} ({'existing' if output_exists else 'created'})")
|
||||
|
||||
# Log directory permissions
|
||||
upload_writable = os.access(Config.UPLOAD_FOLDER, os.W_OK)
|
||||
output_writable = os.access(Config.OUTPUT_FOLDER, os.W_OK)
|
||||
logger.info(f"Directory permissions - Upload writable: {upload_writable}, Output writable: {output_writable}")
|
||||
|
||||
except Exception as e:
|
||||
duration = time.time() - start_time
|
||||
logger.error(f"Failed to create directories after {duration:.3f}s: {e}")
|
||||
raise
|
||||
|
||||
# setup_directories() - commented out to avoid creating directories at startup
|
||||
# Directories will be created dynamically based on config when processing tasks
|
||||
|
||||
# allowed_file moved to shared.py
|
||||
|
||||
# Import blueprints after app initialization to avoid circular imports
|
||||
from .blueprints.health import health_bp
|
||||
from .blueprints.upload import upload_bp
|
||||
from .blueprints.tasks import tasks_bp
|
||||
from .blueprints.task_pool import task_pool_bp
|
||||
from .blueprints.stats import stats_bp
|
||||
from .blueprints.config import config_bp
|
||||
from .blueprints.reports import reports_bp
|
||||
from .blueprints.download import download_bp
|
||||
from .blueprints.web import web_bp
|
||||
|
||||
# Register blueprints
|
||||
app.register_blueprint(health_bp)
|
||||
app.register_blueprint(upload_bp)
|
||||
app.register_blueprint(tasks_bp)
|
||||
app.register_blueprint(task_pool_bp)
|
||||
app.register_blueprint(stats_bp)
|
||||
app.register_blueprint(config_bp)
|
||||
app.register_blueprint(reports_bp)
|
||||
app.register_blueprint(download_bp)
|
||||
app.register_blueprint(web_bp)
|
||||
|
||||
# Load persisted task status after app initialization
|
||||
try:
|
||||
from .shared import (
|
||||
load_task_status_from_file,
|
||||
save_task_status_to_file,
|
||||
set_task_status_file_path,
|
||||
)
|
||||
|
||||
# 只有在 OUTPUT_FOLDER 有效时才启用持久化
|
||||
if hasattr(Config, 'OUTPUT_FOLDER') and Config.OUTPUT_FOLDER:
|
||||
task_status_path = Config.OUTPUT_FOLDER / "task_status.json"
|
||||
set_task_status_file_path(task_status_path)
|
||||
logger.info(f"Task status persistence path set to: {task_status_path}")
|
||||
with app.app_context():
|
||||
load_task_status_from_file()
|
||||
|
||||
import atexit
|
||||
def _save_on_exit():
|
||||
with app.app_context():
|
||||
save_task_status_to_file()
|
||||
atexit.register(_save_on_exit)
|
||||
else:
|
||||
logger.info("Task status persistence will be configured after config is loaded")
|
||||
except Exception as e:
|
||||
print(f"⚠ Failed to setup task persistence: {e}")
|
||||
|
||||
# _get_file_type and _format_response moved to shared.py
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000, debug=False)
|
||||
1
src/gasflux/blueprints/__init__.py
Normal file
1
src/gasflux/blueprints/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# GasFlux API Blueprints
|
||||
67
src/gasflux/blueprints/config.py
Normal file
67
src/gasflux/blueprints/config.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""
|
||||
Configuration Blueprint
|
||||
Provides configuration information and environment variables endpoints.
|
||||
"""
|
||||
|
||||
import os
|
||||
from flask import Blueprint
|
||||
|
||||
from ..app import Config
|
||||
from ..shared import _format_response, log_performance, logger
|
||||
|
||||
# Create blueprint
|
||||
config_bp = Blueprint('config', __name__, url_prefix='/config')
|
||||
|
||||
|
||||
@config_bp.route('', methods=['GET'])
|
||||
@log_performance
|
||||
def get_config():
|
||||
"""Get current configuration (without sensitive information)."""
|
||||
logger.debug("Configuration requested")
|
||||
|
||||
try:
|
||||
config_info = Config.to_dict()
|
||||
|
||||
# Remove potentially sensitive information
|
||||
safe_config = config_info.copy()
|
||||
|
||||
data = {
|
||||
"configuration": safe_config,
|
||||
"environment_variables": {
|
||||
"supported": [
|
||||
"GASFLUX_HOST",
|
||||
"GASFLUX_PORT",
|
||||
"GASFLUX_DEBUG",
|
||||
"GASFLUX_UPLOAD_FOLDER",
|
||||
"GASFLUX_OUTPUT_FOLDER",
|
||||
"GASFLUX_MAX_CONTENT_LENGTH",
|
||||
"GASFLUX_LOG_LEVEL",
|
||||
"GASFLUX_LOG_FILE",
|
||||
"GASFLUX_CORS_ORIGINS",
|
||||
"GASFLUX_TASK_CLEANUP_INTERVAL",
|
||||
"GASFLUX_MAX_TASK_AGE",
|
||||
"GASFLUX_THREADS",
|
||||
"GASFLUX_CONNECTION_LIMIT",
|
||||
"GASFLUX_CHANNEL_TIMEOUT"
|
||||
],
|
||||
"current_values": {
|
||||
key: os.getenv(key, "not set") if key.startswith("GASFLUX_") else "internal"
|
||||
for key in [
|
||||
"GASFLUX_HOST", "GASFLUX_PORT", "GASFLUX_DEBUG",
|
||||
"GASFLUX_UPLOAD_FOLDER", "GASFLUX_OUTPUT_FOLDER",
|
||||
"GASFLUX_MAX_CONTENT_LENGTH", "GASFLUX_LOG_LEVEL",
|
||||
"GASFLUX_LOG_FILE", "GASFLUX_CORS_ORIGINS",
|
||||
"GASFLUX_TASK_CLEANUP_INTERVAL", "GASFLUX_MAX_TASK_AGE",
|
||||
"GASFLUX_THREADS", "GASFLUX_CONNECTION_LIMIT",
|
||||
"GASFLUX_CHANNEL_TIMEOUT"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
return _format_response(200, "配置信息获取成功", data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve configuration: {str(e)}", exc_info=True)
|
||||
return _format_response(500, "获取配置信息失败", {
|
||||
"error_details": str(e)
|
||||
})
|
||||
68
src/gasflux/blueprints/download.py
Normal file
68
src/gasflux/blueprints/download.py
Normal file
@ -0,0 +1,68 @@
|
||||
"""
|
||||
Download Blueprint
|
||||
Handles file download endpoints.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from flask import Blueprint, send_file, current_app
|
||||
|
||||
from ..shared import _format_response, log_performance, logger
|
||||
|
||||
# Create blueprint
|
||||
download_bp = Blueprint('download', __name__, url_prefix='/download')
|
||||
|
||||
|
||||
@download_bp.route('/<path:filename>')
|
||||
@log_performance
|
||||
def download_file(filename):
|
||||
"""Download a processed file."""
|
||||
from flask import request
|
||||
|
||||
logger.info(f"Download request for file: {filename} from IP {request.remote_addr}")
|
||||
|
||||
try:
|
||||
# 支持两种路径格式:
|
||||
# 1. 绝对路径(以 / 开头,如 /full/path/to/file)
|
||||
# 2. 相对路径(task_id/filename)
|
||||
if filename.startswith('/'):
|
||||
# 绝对路径 - 直接使用
|
||||
file_path = Path(filename)
|
||||
else:
|
||||
# 相对路径 - 相对于 OUTPUT_FOLDER
|
||||
output_folder = Path(current_app.config.get('OUTPUT_FOLDER') or '')
|
||||
if not output_folder:
|
||||
logger.error("OUTPUT_FOLDER not configured")
|
||||
return _format_response(500, "服务器配置错误")
|
||||
|
||||
# 解析 task_id/filename 格式
|
||||
parts = filename.split('/', 1)
|
||||
if len(parts) != 2:
|
||||
logger.warning(f"Invalid relative path format: {filename}")
|
||||
return _format_response(400, "无效的文件路径")
|
||||
|
||||
task_id, filename_part = parts
|
||||
file_path = output_folder / task_id / filename_part
|
||||
|
||||
# Security check - ensure file is within output folder
|
||||
file_path = file_path.resolve()
|
||||
output_folder = Path(current_app.config.get('OUTPUT_FOLDER') or '').resolve()
|
||||
if output_folder and not str(file_path).startswith(str(output_folder)):
|
||||
logger.warning(f"Security violation: Attempted to access file outside output folder: {filename}")
|
||||
return _format_response(403, "访问被拒绝")
|
||||
|
||||
if not file_path.exists():
|
||||
logger.warning(f"File not found: {filename}")
|
||||
return _format_response(404, "文件未找到")
|
||||
|
||||
if not file_path.is_file():
|
||||
logger.warning(f"Path is not a file: {filename}")
|
||||
return _format_response(400, "不是文件")
|
||||
|
||||
file_size = file_path.stat().st_size
|
||||
logger.info(f"Serving file: {filename} ({file_size} bytes)")
|
||||
|
||||
return send_file(file_path)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error serving file {filename}: {str(e)}", exc_info=True)
|
||||
return _format_response(500, "内部服务器错误")
|
||||
94
src/gasflux/blueprints/health.py
Normal file
94
src/gasflux/blueprints/health.py
Normal file
@ -0,0 +1,94 @@
|
||||
"""
|
||||
Health Check Blueprint
|
||||
Provides API health monitoring and system status endpoints.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
from flask import Blueprint, request
|
||||
|
||||
from ..app import Config, stats_collector
|
||||
from ..shared import task_status, TASK_STATUS_PENDING, TASK_STATUS_PROCESSING
|
||||
from ..shared import _format_response, log_performance, logger
|
||||
|
||||
# Create blueprint
|
||||
health_bp = Blueprint('health', __name__, url_prefix='/health')
|
||||
|
||||
|
||||
@health_bp.route('', methods=['GET'])
|
||||
@log_performance
|
||||
def health_check():
|
||||
"""API Health Check"""
|
||||
logger.debug("Health check requested")
|
||||
|
||||
try:
|
||||
# Check storage accessibility
|
||||
uploads_writable = os.access(Config.UPLOAD_FOLDER, os.W_OK)
|
||||
outputs_writable = os.access(Config.OUTPUT_FOLDER, os.W_OK)
|
||||
|
||||
# Check active tasks
|
||||
active_tasks = len([t for t in task_status.values() if t.get("status") in [TASK_STATUS_PENDING, TASK_STATUS_PROCESSING]])
|
||||
|
||||
# Get basic stats for health check
|
||||
stats_summary = stats_collector.get_summary()
|
||||
|
||||
health_data = {
|
||||
"status": "healthy",
|
||||
"version": "1.0.0",
|
||||
"timestamp": time.time(),
|
||||
"uptime": stats_summary['summary']['uptime_formatted'],
|
||||
"storage": {
|
||||
"uploads_writable": uploads_writable,
|
||||
"outputs_writable": outputs_writable
|
||||
},
|
||||
"tasks": {
|
||||
"active_count": active_tasks,
|
||||
"total_tracked": len(task_status),
|
||||
"total_processed": stats_summary['tasks']['total_completed'] + stats_summary['tasks']['total_failed'],
|
||||
"success_rate_percent": stats_summary['tasks']['success_rate_percent']
|
||||
},
|
||||
"performance": {
|
||||
"requests_per_second": stats_summary['summary']['requests_per_second'],
|
||||
"avg_response_time_ms": stats_summary['performance']['avg_response_time_ms'],
|
||||
"error_rate_percent": stats_summary['summary']['error_rate_percent']
|
||||
}
|
||||
}
|
||||
|
||||
# Determine health status based on metrics
|
||||
is_healthy = True
|
||||
issues = []
|
||||
|
||||
if not uploads_writable:
|
||||
issues.append("上传文件夹不可写")
|
||||
is_healthy = False
|
||||
if not outputs_writable:
|
||||
issues.append("输出文件夹不可写")
|
||||
is_healthy = False
|
||||
if active_tasks > 20: # High load threshold
|
||||
issues.append(f"活跃任务数量过多 ({active_tasks})")
|
||||
if stats_summary['summary']['error_rate_percent'] > 10: # High error rate
|
||||
issues.append(f"错误率过高 ({stats_summary['summary']['error_rate_percent']:.1f}%)")
|
||||
is_healthy = False
|
||||
|
||||
health_data["status"] = "healthy" if is_healthy else "degraded"
|
||||
if issues:
|
||||
health_data["issues"] = issues
|
||||
|
||||
# Log warnings for potential issues
|
||||
for issue in issues:
|
||||
logger.warning(f"Health check issue: {issue}")
|
||||
|
||||
status_level = logging.DEBUG if is_healthy else logging.WARNING
|
||||
logger.log(status_level, f"Health check: {health_data['status']} (active tasks: {active_tasks})")
|
||||
|
||||
status_code = 200 if is_healthy else 503 # 503 Service Unavailable for degraded
|
||||
return _format_response(status_code, "健康检查完成" if is_healthy else "服务不可用", health_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Health check failed: {str(e)}", exc_info=True)
|
||||
return _format_response(500, "健康检查失败", {
|
||||
"status": "unhealthy",
|
||||
"error": str(e),
|
||||
"timestamp": time.time()
|
||||
})
|
||||
192
src/gasflux/blueprints/reports.py
Normal file
192
src/gasflux/blueprints/reports.py
Normal file
@ -0,0 +1,192 @@
|
||||
"""
|
||||
Reports Blueprint
|
||||
Provides report listing and management endpoints.
|
||||
"""
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
from flask import Blueprint, request, current_app
|
||||
|
||||
|
||||
from ..shared import _get_file_type, _format_response, log_performance, logger, task_status
|
||||
from ..app import Config
|
||||
|
||||
# Create blueprint
|
||||
reports_bp = Blueprint('reports', __name__, url_prefix='/reports')
|
||||
|
||||
|
||||
@reports_bp.route('', methods=['GET'])
|
||||
@log_performance
|
||||
def list_reports():
|
||||
"""List all generated reports with pagination and filtering."""
|
||||
logger.debug("Reports list requested")
|
||||
|
||||
try:
|
||||
# Parse query parameters
|
||||
try:
|
||||
page = int(request.args.get('page', 1))
|
||||
if page < 1:
|
||||
return _format_response(400, "Invalid parameter: page must be >= 1")
|
||||
except (ValueError, TypeError):
|
||||
return _format_response(400, "Invalid parameter: page must be a valid integer")
|
||||
|
||||
try:
|
||||
per_page = int(request.args.get('per_page', 20))
|
||||
if per_page < 1 or per_page > 100:
|
||||
return _format_response(400, "Invalid parameter: per_page must be between 1 and 100")
|
||||
except (ValueError, TypeError):
|
||||
return _format_response(400, "Invalid parameter: per_page must be a valid integer")
|
||||
|
||||
sort_by = request.args.get('sort_by', 'created_at')
|
||||
sort_order = request.args.get('sort_order', 'desc')
|
||||
status_filter = request.args.get('status', None) # 'completed', 'failed', or None for all
|
||||
|
||||
# Validate sort parameters
|
||||
valid_sort_fields = ['created_at', 'task_id', 'file_size', 'processing_time']
|
||||
if sort_by not in valid_sort_fields:
|
||||
return _format_response(400, f"Invalid parameter: sort_by must be one of {valid_sort_fields}")
|
||||
if sort_order not in ['asc', 'desc']:
|
||||
return _format_response(400, "Invalid parameter: sort_order must be 'asc' or 'desc'")
|
||||
|
||||
# Validate status filter
|
||||
valid_statuses = ['completed', 'failed', None]
|
||||
if status_filter is not None and status_filter not in ['completed', 'failed']:
|
||||
return _format_response(400, "Invalid parameter: status must be 'completed', 'failed', or not specified")
|
||||
|
||||
# 兼容缺省:优先 app.config,其次 Config.OUTPUT_FOLDER
|
||||
output_root = current_app.config.get('OUTPUT_FOLDER') or getattr(Config, 'OUTPUT_FOLDER', None)
|
||||
if not output_root:
|
||||
return _format_response(200, "报告列表获取成功", {
|
||||
'reports': [],
|
||||
'pagination': {'page': page, 'per_page': per_page, 'total_reports': 0, 'total_pages': 0, 'has_next': False, 'has_prev': False},
|
||||
'filters': {'sort_by': sort_by, 'sort_order': sort_order, 'status': status_filter}
|
||||
})
|
||||
|
||||
output_folder = Path(output_root)
|
||||
reports = []
|
||||
|
||||
# Scan all task directories
|
||||
if output_folder.exists():
|
||||
for task_dir in output_folder.iterdir():
|
||||
if not task_dir.is_dir():
|
||||
continue
|
||||
task_id = task_dir.name
|
||||
|
||||
# Get task information from global task_status
|
||||
task_info = task_status.get(task_id, {})
|
||||
task_status_value = task_info.get('status')
|
||||
|
||||
# Log task status for debugging
|
||||
logger.debug(f"Task {task_id}: status from memory={task_status_value}, info={task_info}")
|
||||
|
||||
# 直接扫描平铺文件
|
||||
files = [p for p in task_dir.iterdir() if p.is_file()]
|
||||
if not files:
|
||||
# 按需应用状态过滤
|
||||
if status_filter:
|
||||
continue
|
||||
reports.append({
|
||||
'task_id': task_id,
|
||||
'report_name': "N/A",
|
||||
'status': 'failed',
|
||||
'created_at': task_dir.stat().st_mtime,
|
||||
'file_count': 0,
|
||||
'total_size': 0,
|
||||
'processing_time_seconds': None,
|
||||
'main_report': None,
|
||||
'all_files': [],
|
||||
'run_directory': f'{task_id}'
|
||||
})
|
||||
continue
|
||||
|
||||
# 识别主报告与统计
|
||||
total_size = sum(f.stat().st_size for f in files)
|
||||
created_at = max(f.stat().st_mtime for f in files) if files else task_dir.stat().st_mtime
|
||||
|
||||
def file_entry(p):
|
||||
return {
|
||||
'name': p.name,
|
||||
'size': p.stat().st_size,
|
||||
'type': _get_file_type(p.name),
|
||||
# 使用相对路径下载,清晰且安全
|
||||
'download_url': f"/download/{task_id}/{p.name}"
|
||||
}
|
||||
|
||||
all_files = [file_entry(f) for f in files]
|
||||
# 优先 CO2_report,其次任意 *_report_*.html
|
||||
report_html = None
|
||||
for f in files:
|
||||
if f.name.endswith('_report_') and f.suffix == '.html':
|
||||
report_html = file_entry(f)
|
||||
break
|
||||
if not report_html:
|
||||
for f in files:
|
||||
if f.name.endswith('.html'):
|
||||
report_html = file_entry(f)
|
||||
break
|
||||
|
||||
# 任务状态:若有报告或关键产物则视为 completed
|
||||
has_outputs = any(f.name.startswith(('config_', 'output_vars_', 'processed_data_', 'CO2_report_')) for f in files)
|
||||
task_status_value = 'completed' if has_outputs else 'unknown'
|
||||
if status_filter and task_status_value != status_filter:
|
||||
continue
|
||||
|
||||
# Create report entry
|
||||
report_entry = {
|
||||
'task_id': task_id,
|
||||
'report_name': task_id,
|
||||
'status': task_status_value,
|
||||
'created_at': created_at,
|
||||
'file_count': len(files),
|
||||
'total_size': total_size,
|
||||
'processing_time_seconds': None,
|
||||
'main_report': report_html,
|
||||
'all_files': all_files,
|
||||
'run_directory': f'{task_id}'
|
||||
}
|
||||
|
||||
reports.append(report_entry)
|
||||
|
||||
# Sort reports
|
||||
reverse_order = sort_order == 'desc'
|
||||
if sort_by == 'created_at':
|
||||
reports.sort(key=lambda x: x['created_at'], reverse=reverse_order)
|
||||
elif sort_by == 'task_id':
|
||||
reports.sort(key=lambda x: x['task_id'], reverse=reverse_order)
|
||||
elif sort_by == 'file_size':
|
||||
reports.sort(key=lambda x: x['total_size'], reverse=reverse_order)
|
||||
elif sort_by == 'processing_time':
|
||||
reports.sort(key=lambda x: x['processing_time_seconds'] or 0, reverse=reverse_order)
|
||||
|
||||
# Paginate results
|
||||
total_reports = len(reports)
|
||||
start_idx = (page - 1) * per_page
|
||||
end_idx = start_idx + per_page
|
||||
paginated_reports = reports[start_idx:end_idx]
|
||||
|
||||
# Calculate pagination metadata
|
||||
total_pages = (total_reports + per_page - 1) // per_page
|
||||
|
||||
response_data = {
|
||||
'reports': paginated_reports,
|
||||
'pagination': {
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'total_reports': total_reports,
|
||||
'total_pages': total_pages,
|
||||
'has_next': page < total_pages,
|
||||
'has_prev': page > 1
|
||||
},
|
||||
'filters': {
|
||||
'sort_by': sort_by,
|
||||
'sort_order': sort_order,
|
||||
'status': status_filter
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(f"Returning {len(paginated_reports)} reports (page {page}/{total_pages})")
|
||||
return _format_response(200, "报告列表获取成功", response_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing reports: {str(e)}", exc_info=True)
|
||||
return _format_response(500, "内部服务器错误")
|
||||
93
src/gasflux/blueprints/stats.py
Normal file
93
src/gasflux/blueprints/stats.py
Normal file
@ -0,0 +1,93 @@
|
||||
"""
|
||||
Statistics Blueprint
|
||||
Provides API statistics and monitoring endpoints.
|
||||
"""
|
||||
|
||||
import time
|
||||
from flask import Blueprint, current_app
|
||||
|
||||
|
||||
from ..shared import _format_response, log_performance, logger,stats_collector, task_status
|
||||
|
||||
# Create blueprint
|
||||
stats_bp = Blueprint('stats', __name__, url_prefix='/stats')
|
||||
|
||||
|
||||
@stats_bp.route('', methods=['GET'])
|
||||
@log_performance
|
||||
def get_stats():
|
||||
"""Get detailed API statistics and monitoring data."""
|
||||
logger.debug("Statistics requested")
|
||||
|
||||
try:
|
||||
# Get detailed statistics
|
||||
stats_data = stats_collector.get_summary()
|
||||
|
||||
# Add current system information
|
||||
try:
|
||||
import psutil
|
||||
memory = psutil.virtual_memory()
|
||||
disk = psutil.disk_usage(str(current_app.config['OUTPUT_FOLDER']))
|
||||
|
||||
stats_data['system'] = {
|
||||
'memory_usage_percent': memory.percent,
|
||||
'memory_used_gb': round(memory.used / (1024**3), 2),
|
||||
'memory_total_gb': round(memory.total / (1024**3), 2),
|
||||
'disk_usage_percent': disk.percent,
|
||||
'disk_used_gb': round(disk.used / (1024**3), 2),
|
||||
'disk_total_gb': round(disk.total / (1024**3), 2)
|
||||
}
|
||||
except ImportError:
|
||||
# psutil not available
|
||||
stats_data['system'] = {
|
||||
'note': 'System metrics unavailable - install psutil for detailed monitoring'
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to collect system metrics: {e}")
|
||||
stats_data['system'] = {'error': str(e)}
|
||||
|
||||
# Add recent task information
|
||||
recent_tasks = []
|
||||
current_time = time.time()
|
||||
for task_id, task_info in list(task_status.items())[-20:]: # Last 20 tasks
|
||||
age = current_time - task_info.get('updated_at', 0)
|
||||
recent_tasks.append({
|
||||
'task_id': task_id,
|
||||
'status': task_info.get('status'),
|
||||
'age_seconds': round(age, 1),
|
||||
'message': task_info.get('message', '')[:100] # Truncate long messages
|
||||
})
|
||||
|
||||
stats_data['recent_tasks'] = recent_tasks
|
||||
|
||||
return _format_response(200, "统计信息获取成功", stats_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve statistics: {str(e)}", exc_info=True)
|
||||
return _format_response(500, "获取统计信息失败", {
|
||||
"error_details": str(e)
|
||||
})
|
||||
|
||||
|
||||
@stats_bp.route('/reset', methods=['POST'])
|
||||
@log_performance
|
||||
def reset_stats():
|
||||
"""Reset API statistics (admin function)."""
|
||||
logger.warning("Statistics reset requested")
|
||||
|
||||
try:
|
||||
# Reset statistics
|
||||
stats_collector.reset_stats()
|
||||
|
||||
# Log the reset
|
||||
logger.info("API statistics have been reset")
|
||||
|
||||
return _format_response(200, "统计信息重置成功", {
|
||||
"timestamp": time.time()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to reset statistics: {str(e)}", exc_info=True)
|
||||
return _format_response(500, "重置统计信息失败", {
|
||||
"error_details": str(e)
|
||||
})
|
||||
228
src/gasflux/blueprints/task_pool.py
Normal file
228
src/gasflux/blueprints/task_pool.py
Normal file
@ -0,0 +1,228 @@
|
||||
"""
|
||||
Task Pool Blueprint
|
||||
Handles task pool management endpoints: listing tasks with pagination, pool statistics.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request
|
||||
from pathlib import Path
|
||||
|
||||
from ..shared import (
|
||||
get_task_list,
|
||||
get_task_pool_stats,
|
||||
_format_response,
|
||||
log_performance,
|
||||
logger,
|
||||
task_status,
|
||||
TASK_STATUS_PENDING,
|
||||
TASK_STATUS_PROCESSING,
|
||||
TASK_STATUS_COMPLETED,
|
||||
TASK_STATUS_FAILED
|
||||
)
|
||||
|
||||
# Create blueprint
|
||||
task_pool_bp = Blueprint('task_pool', __name__, url_prefix='/tasks')
|
||||
|
||||
|
||||
def _build_simple_downloads_from_results(results: list[dict]) -> dict:
|
||||
"""
|
||||
Build direct download shortcuts for common files, based on task results.
|
||||
This is intentionally minimal and frontend-friendly.
|
||||
"""
|
||||
downloads: dict = {}
|
||||
|
||||
def set_once(key: str, url: str):
|
||||
if key not in downloads and url:
|
||||
downloads[key] = url
|
||||
|
||||
for item in results or []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
rel_path = item.get('rel_path')
|
||||
if not rel_path:
|
||||
continue
|
||||
|
||||
name_l = (item.get('name') or '').lower()
|
||||
url = f"/download/{rel_path}"
|
||||
|
||||
if name_l.endswith('.xlsx'):
|
||||
set_once('data_xlsx', url)
|
||||
elif name_l.endswith('.xls'):
|
||||
set_once('data_xls', url)
|
||||
elif name_l.endswith('ch4_report.html'):
|
||||
set_once('report_ch4', url)
|
||||
elif name_l.endswith('co2_report.html'):
|
||||
set_once('report_co2', url)
|
||||
elif name_l.endswith(('.yaml', '.yml')):
|
||||
set_once('config', url)
|
||||
elif name_l.endswith('.json') and 'output_vars' in name_l:
|
||||
set_once('metadata', url)
|
||||
elif name_l.endswith('.html'):
|
||||
# fallback: any html report
|
||||
set_once('report_html', url)
|
||||
|
||||
return downloads
|
||||
|
||||
|
||||
def _lean_task_summary(task_summary: dict) -> dict:
|
||||
"""Return a minimal task representation for frontend consumption."""
|
||||
task_id = task_summary.get('task_id')
|
||||
status = task_summary.get('status')
|
||||
|
||||
lean = {
|
||||
'task_id': task_id,
|
||||
'status': status,
|
||||
'message': task_summary.get('message'),
|
||||
'updated_at': task_summary.get('updated_at'),
|
||||
}
|
||||
|
||||
if status == TASK_STATUS_COMPLETED and task_id:
|
||||
full_task_info = task_status.get(task_id, {})
|
||||
results = full_task_info.get('results', []) or []
|
||||
downloads = _build_simple_downloads_from_results(results)
|
||||
if downloads:
|
||||
lean['downloads'] = downloads
|
||||
|
||||
return lean
|
||||
|
||||
|
||||
@task_pool_bp.route('', methods=['GET'])
|
||||
@log_performance
|
||||
def list_tasks():
|
||||
"""Get paginated list of tasks with optional filtering."""
|
||||
logger.debug(f"Task list request from IP {request.remote_addr}")
|
||||
|
||||
try:
|
||||
# Parse query parameters
|
||||
status_filter = request.args.get('status')
|
||||
if status_filter:
|
||||
# Support comma-separated status values
|
||||
status_filter = status_filter.split(',')
|
||||
|
||||
page = int(request.args.get('page', 1))
|
||||
page_size = int(request.args.get('page_size', 20))
|
||||
sort_by = request.args.get('sort_by', 'updated_at')
|
||||
sort_order = request.args.get('sort_order', 'desc')
|
||||
|
||||
# Validate parameters
|
||||
if page < 1:
|
||||
return _format_response(400, "页码必须大于0")
|
||||
|
||||
if page_size < 1 or page_size > 100:
|
||||
return _format_response(400, "每页数量必须在1-100之间")
|
||||
|
||||
valid_sort_fields = ['created_at', 'updated_at', 'status']
|
||||
if sort_by not in valid_sort_fields:
|
||||
return _format_response(400, f"排序字段必须是以下之一: {', '.join(valid_sort_fields)}")
|
||||
|
||||
if sort_order.lower() not in ['asc', 'desc']:
|
||||
return _format_response(400, "排序顺序必须是 'asc' 或 'desc'")
|
||||
|
||||
# Get task list
|
||||
result = get_task_list(
|
||||
status_filter=status_filter,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
cleanup=False
|
||||
)
|
||||
|
||||
# Slim response: only task status + downloads (completed only)
|
||||
result['tasks'] = [_lean_task_summary(t) for t in result.get('tasks', [])]
|
||||
|
||||
logger.debug(f"Returning {len(result['tasks'])} tasks (page {page} of {result['total_pages']})")
|
||||
|
||||
return _format_response(200, "任务列表查询成功", result)
|
||||
|
||||
except ValueError as e:
|
||||
logger.warning(f"Invalid parameter in task list request: {str(e)}")
|
||||
return _format_response(400, "参数格式错误")
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing tasks: {str(e)}", exc_info=True)
|
||||
return _format_response(500, "内部服务器错误")
|
||||
|
||||
|
||||
@task_pool_bp.route('/stats', methods=['GET'])
|
||||
@log_performance
|
||||
def get_pool_stats():
|
||||
"""Get task pool statistics."""
|
||||
logger.debug(f"Task pool stats request from IP {request.remote_addr}")
|
||||
|
||||
try:
|
||||
stats = get_task_pool_stats()
|
||||
|
||||
logger.debug(f"Pool stats: {stats['total_tasks']} total tasks, "
|
||||
f"{stats['active_tasks']} active, {stats['queued_tasks']} queued")
|
||||
|
||||
return _format_response(200, "任务池统计信息查询成功", stats)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting pool stats: {str(e)}", exc_info=True)
|
||||
return _format_response(500, "内部服务器错误")
|
||||
|
||||
|
||||
@task_pool_bp.route('/active', methods=['GET'])
|
||||
@log_performance
|
||||
def get_active_tasks():
|
||||
"""Get list of currently active (processing) tasks."""
|
||||
logger.debug(f"Active tasks request from IP {request.remote_addr}")
|
||||
|
||||
try:
|
||||
# Get all processing tasks, no pagination needed for active tasks
|
||||
result = get_task_list(
|
||||
status_filter=TASK_STATUS_PROCESSING,
|
||||
page=1,
|
||||
page_size=1000, # Large page size to get all active tasks
|
||||
sort_by='updated_at',
|
||||
sort_order='asc', # Oldest first
|
||||
cleanup=False
|
||||
)
|
||||
|
||||
active_tasks = result['tasks']
|
||||
|
||||
active_tasks = [_lean_task_summary(t) for t in active_tasks]
|
||||
|
||||
logger.debug(f"Returning {len(active_tasks)} active tasks")
|
||||
|
||||
return _format_response(200, "活跃任务查询成功", {
|
||||
'active_tasks': active_tasks,
|
||||
'count': len(active_tasks)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting active tasks: {str(e)}", exc_info=True)
|
||||
return _format_response(500, "内部服务器错误")
|
||||
|
||||
|
||||
@task_pool_bp.route('/queue', methods=['GET'])
|
||||
@log_performance
|
||||
def get_queued_tasks():
|
||||
"""Get list of queued (pending) tasks."""
|
||||
logger.debug(f"Queued tasks request from IP {request.remote_addr}")
|
||||
|
||||
try:
|
||||
# Get all pending tasks, sorted by creation time
|
||||
result = get_task_list(
|
||||
status_filter=TASK_STATUS_PENDING,
|
||||
page=1,
|
||||
page_size=1000, # Large page size to get all queued tasks
|
||||
sort_by='created_at',
|
||||
sort_order='asc', # Oldest first (FIFO)
|
||||
cleanup=False
|
||||
)
|
||||
|
||||
queued_tasks = result['tasks']
|
||||
|
||||
queued_tasks = [_lean_task_summary(t) for t in queued_tasks]
|
||||
|
||||
logger.debug(f"Returning {len(queued_tasks)} queued tasks")
|
||||
|
||||
return _format_response(200, "队列任务查询成功", {
|
||||
'queued_tasks': queued_tasks,
|
||||
'count': len(queued_tasks),
|
||||
'queue_position_info': "任务按创建时间排序,较早的任务优先处理"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting queued tasks: {str(e)}", exc_info=True)
|
||||
return _format_response(500, "内部服务器错误")
|
||||
220
src/gasflux/blueprints/tasks.py
Normal file
220
src/gasflux/blueprints/tasks.py
Normal file
@ -0,0 +1,220 @@
|
||||
"""
|
||||
Tasks Blueprint
|
||||
Handles task management endpoints: status query, update, and deletion.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request
|
||||
|
||||
from ..shared import (
|
||||
get_task_status,
|
||||
update_task_status,
|
||||
cleanup_old_tasks,
|
||||
_format_response,
|
||||
log_performance,
|
||||
logger,
|
||||
task_status,
|
||||
_build_simple_downloads_from_results,
|
||||
TASK_STATUS_COMPLETED,
|
||||
TASK_STATUS_FAILED,
|
||||
TASK_STATUS_PROCESSING,
|
||||
TASK_STATUS_PENDING,
|
||||
)
|
||||
|
||||
# Create blueprint
|
||||
tasks_bp = Blueprint('tasks', __name__, url_prefix='/task')
|
||||
|
||||
|
||||
@tasks_bp.route('/<task_id>', methods=['GET'])
|
||||
@log_performance
|
||||
def get_task_status_endpoint(task_id):
|
||||
"""Get the status of a processing task."""
|
||||
logger.debug(f"Status request for task {task_id}")
|
||||
|
||||
try:
|
||||
# Note: cleanup_old_tasks() is disabled for individual task queries
|
||||
# to preserve historical task data for task pool management
|
||||
# cleanup_old_tasks()
|
||||
|
||||
task_info = get_task_status(task_id)
|
||||
if task_info.get("status") == "not_found":
|
||||
logger.warning(f"Status request for non-existent task {task_id} from IP {request.remote_addr}")
|
||||
return _format_response(404, "任务未找到")
|
||||
|
||||
data = {
|
||||
"task_id": task_id,
|
||||
"status": task_info["status"],
|
||||
"message": task_info.get("message", ""),
|
||||
"updated_at": task_info.get("updated_at", 0)
|
||||
}
|
||||
|
||||
if task_info["status"] == TASK_STATUS_COMPLETED:
|
||||
results = task_info.get("results", [])
|
||||
data["results"] = results
|
||||
# Add direct download shortcuts for frontend (if available or can be derived)
|
||||
downloads = task_info.get("downloads") or _build_simple_downloads_from_results(results)
|
||||
if downloads:
|
||||
data["downloads"] = downloads
|
||||
logger.debug(f"Task {task_id}: Returning {len(results)} completed results")
|
||||
return _format_response(200, "任务查询成功", data)
|
||||
elif task_info["status"] == TASK_STATUS_FAILED:
|
||||
error_msg = task_info.get("error", "未知错误")
|
||||
data["error"] = error_msg
|
||||
logger.warning(f"Task {task_id}: Returning failure status - {error_msg}")
|
||||
# Return 200 for failed tasks since this is expected behavior, not an HTTP error
|
||||
return _format_response(200, "任务处理失败", data)
|
||||
else:
|
||||
# Processing or pending status
|
||||
return _format_response(200, "任务查询成功", data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving status for task {task_id}: {str(e)}", exc_info=True)
|
||||
return _format_response(500, "内部服务器错误")
|
||||
|
||||
|
||||
@tasks_bp.route('/<task_id>', methods=['PUT'])
|
||||
@log_performance
|
||||
def update_task(task_id):
|
||||
"""Update task status and information."""
|
||||
logger.info(f"Task update request for {task_id} from IP {request.remote_addr}")
|
||||
|
||||
try:
|
||||
# Validate task exists
|
||||
task_info = get_task_status(task_id)
|
||||
if task_info.get("status") == "not_found":
|
||||
logger.warning(f"Update request for non-existent task {task_id}")
|
||||
return _format_response(404, "任务未找到")
|
||||
|
||||
# Parse request data
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return _format_response(400, "请求体必须是 JSON 格式")
|
||||
|
||||
# Validate allowed fields
|
||||
allowed_fields = ['status', 'message', 'priority']
|
||||
valid_statuses = [TASK_STATUS_PENDING, TASK_STATUS_PROCESSING,
|
||||
TASK_STATUS_COMPLETED, TASK_STATUS_FAILED]
|
||||
|
||||
updates = {}
|
||||
for field in allowed_fields:
|
||||
if field in data:
|
||||
if field == 'status' and data[field] not in valid_statuses:
|
||||
return _format_response(400, f"无效状态。必须是以下之一: {', '.join(valid_statuses)}")
|
||||
updates[field] = data[field]
|
||||
|
||||
if not updates:
|
||||
return _format_response(400, "没有有效的字段可更新")
|
||||
|
||||
# Update task status
|
||||
current_status = task_info.get('status')
|
||||
new_status = updates.get('status', current_status)
|
||||
message = updates.get('message', task_info.get('message'))
|
||||
|
||||
# Special handling for status changes
|
||||
if 'status' in updates:
|
||||
if new_status == TASK_STATUS_COMPLETED:
|
||||
# For completed tasks, we might want to add fake results if none exist
|
||||
if not task_info.get('results'):
|
||||
logger.warning(f"Marking task {task_id} as completed but no results found")
|
||||
elif new_status == TASK_STATUS_FAILED:
|
||||
# For failed tasks, error message is required
|
||||
error_msg = updates.get('message', 'Task manually marked as failed')
|
||||
update_task_status(task_id, new_status, error_msg)
|
||||
else:
|
||||
update_task_status(task_id, new_status, message)
|
||||
else:
|
||||
# Only update message
|
||||
update_task_status(task_id, current_status, message)
|
||||
|
||||
# Update priority if provided
|
||||
if 'priority' in updates:
|
||||
task_status[task_id]['priority'] = updates['priority']
|
||||
|
||||
# Get updated task info
|
||||
updated_task = get_task_status(task_id)
|
||||
|
||||
data = {
|
||||
"task_id": task_id,
|
||||
"status": "updated",
|
||||
"task_info": {
|
||||
"status": updated_task.get("status"),
|
||||
"message": updated_task.get("message"),
|
||||
"updated_at": updated_task.get("updated_at", 0),
|
||||
"priority": updated_task.get("priority", "normal")
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(f"Task {task_id} updated: {updates}")
|
||||
return _format_response(200, "任务更新成功", data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating task {task_id}: {str(e)}", exc_info=True)
|
||||
return _format_response(500, "内部服务器错误")
|
||||
|
||||
|
||||
@tasks_bp.route('/<task_id>', methods=['DELETE'])
|
||||
@log_performance
|
||||
def delete_task(task_id):
|
||||
"""Delete a task and its associated files."""
|
||||
logger.info(f"Task deletion request for {task_id} from IP {request.remote_addr}")
|
||||
|
||||
try:
|
||||
# Validate task exists
|
||||
task_info = get_task_status(task_id)
|
||||
if task_info.get("status") == "not_found":
|
||||
logger.warning(f"Delete request for non-existent task {task_id}")
|
||||
return _format_response(404, "任务未找到")
|
||||
|
||||
# Check if task is currently processing
|
||||
if task_info.get("status") in [TASK_STATUS_PROCESSING, TASK_STATUS_PENDING]:
|
||||
return _format_response(409, "无法删除当前正在处理或等待处理的任务", {
|
||||
"task_status": task_info.get("status")
|
||||
})
|
||||
|
||||
# Delete associated files
|
||||
from pathlib import Path
|
||||
from flask import current_app
|
||||
import shutil
|
||||
|
||||
output_folder = Path(current_app.config['OUTPUT_FOLDER'])
|
||||
task_folder = output_folder / task_id
|
||||
|
||||
files_deleted = 0
|
||||
total_size_deleted = 0
|
||||
|
||||
if task_folder.exists():
|
||||
try:
|
||||
# Calculate total size before deletion
|
||||
for file_path in task_folder.rglob('*'):
|
||||
if file_path.is_file():
|
||||
total_size_deleted += file_path.stat().st_size
|
||||
|
||||
# Delete the entire task folder
|
||||
shutil.rmtree(task_folder)
|
||||
files_deleted = 1 # Count as one folder deleted
|
||||
logger.info(f"Deleted task folder: {task_folder}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting task folder {task_folder}: {str(e)}")
|
||||
return _format_response(500, f"删除任务文件失败: {str(e)}")
|
||||
|
||||
# Remove from task status tracking
|
||||
if task_id in task_status:
|
||||
del task_status[task_id]
|
||||
logger.info(f"Removed task {task_id} from status tracking")
|
||||
|
||||
data = {
|
||||
"task_id": task_id,
|
||||
"status": "deleted",
|
||||
"details": {
|
||||
"folders_deleted": files_deleted,
|
||||
"total_size_deleted": total_size_deleted,
|
||||
"task_status": task_info.get("status")
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(f"Task {task_id} deleted successfully")
|
||||
return _format_response(200, "任务及相关文件删除成功", data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting task {task_id}: {str(e)}", exc_info=True)
|
||||
return _format_response(500, "内部服务器错误")
|
||||
134
src/gasflux/blueprints/upload.py
Normal file
134
src/gasflux/blueprints/upload.py
Normal file
@ -0,0 +1,134 @@
|
||||
"""
|
||||
Upload Blueprint
|
||||
Handles file upload and processing initiation endpoints.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from flask import Blueprint, request, current_app
|
||||
from werkzeug.utils import secure_filename
|
||||
import yaml
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
from ..app import process_data_async
|
||||
from ..shared import _format_response, log_performance, logger, ALLOWED_DATA_EXTENSIONS, ALLOWED_CONFIG_EXTENSIONS, allowed_file,update_task_status, TASK_STATUS_PENDING, TASK_STATUS_FAILED
|
||||
|
||||
# Create blueprint
|
||||
upload_bp = Blueprint('upload', __name__, url_prefix='/upload')
|
||||
|
||||
|
||||
@upload_bp.route('', methods=['POST'])
|
||||
@log_performance
|
||||
def upload_file():
|
||||
logger.info("Received upload request")
|
||||
logger.info(f"Request content length: {request.content_length} bytes")
|
||||
|
||||
# Check if data file is present
|
||||
if 'file' not in request.files:
|
||||
logger.warning("Upload failed: No data file part in request")
|
||||
return _format_response(400, "未找到数据文件部分")
|
||||
|
||||
data_file = request.files['file']
|
||||
config_file = request.files.get('config')
|
||||
|
||||
# Log file details
|
||||
logger.info(f"Data file: {data_file.filename} (size: {getattr(data_file, 'content_length', 'unknown')} bytes)")
|
||||
if config_file:
|
||||
logger.info(f"Config file: {config_file.filename} (size: {getattr(config_file, 'content_length', 'unknown')} bytes)")
|
||||
else:
|
||||
logger.info("No custom config file provided, will use default")
|
||||
|
||||
if data_file.filename == '':
|
||||
logger.warning("Upload failed: No data file selected (empty filename)")
|
||||
return _format_response(400, "未选择数据文件")
|
||||
|
||||
if not allowed_file(data_file.filename, ALLOWED_DATA_EXTENSIONS):
|
||||
logger.warning(f"Upload failed: Invalid data file type {data_file.filename} - allowed: {ALLOWED_DATA_EXTENSIONS}")
|
||||
return _format_response(400, "无效的数据文件类型。只允许 .xlsx 和 .xls 格式。")
|
||||
|
||||
# Generate unique job ID
|
||||
job_id = str(uuid.uuid4())
|
||||
logger.info(f"Generated job ID: {job_id}")
|
||||
|
||||
# 1) Parse config content (parse in memory without saving first)
|
||||
if config_file and config_file.filename != '':
|
||||
if not allowed_file(config_file.filename, ALLOWED_CONFIG_EXTENSIONS):
|
||||
return _format_response(400, "无效的配置文件类型。只允许 .yaml 和 .yml 格式。")
|
||||
config_file.stream.seek(0)
|
||||
config_text = config_file.read().decode('utf-8', errors='ignore')
|
||||
try:
|
||||
active_config = yaml.safe_load(config_text)
|
||||
except Exception:
|
||||
return _format_response(400, "配置文件解析失败")
|
||||
# Reset stream for saving
|
||||
config_file.stream = BytesIO(config_text.encode('utf-8'))
|
||||
else:
|
||||
default_config_path = Path(__file__).parent.parent / "gasflux_config.yaml"
|
||||
with open(default_config_path, 'r', encoding='utf-8') as f:
|
||||
active_config = yaml.safe_load(f)
|
||||
|
||||
# 2) Create job directories based on config['output_dir']
|
||||
output_base = Path(active_config['output_dir']).expanduser()
|
||||
job_upload_dir = output_base / "uploads" / job_id
|
||||
job_output_dir = output_base / "outputs" / job_id
|
||||
job_upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
job_output_dir.mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"Job {job_id}: Created directories - Upload: {job_upload_dir}, Output: {job_output_dir}")
|
||||
|
||||
# 3) Save data file to job_upload_dir
|
||||
data_filename = secure_filename(data_file.filename)
|
||||
data_path = job_upload_dir / data_filename
|
||||
try:
|
||||
data_file.seek(0)
|
||||
data_file.save(str(data_path))
|
||||
logger.info(f"Job {job_id}: Data file saved successfully - Path: {data_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Job {job_id}: Failed to save data file {data_filename}: {str(e)}")
|
||||
return _format_response(500, "保存数据文件失败")
|
||||
|
||||
# 4) Save config file to job_upload_dir
|
||||
if config_file and config_file.filename != '':
|
||||
config_filename = secure_filename(config_file.filename)
|
||||
config_path = job_upload_dir / config_filename
|
||||
try:
|
||||
config_file.seek(0)
|
||||
config_file.save(str(config_path))
|
||||
active_config_path = config_path
|
||||
logger.info(f"Job {job_id}: Custom config saved successfully - Path: {config_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Job {job_id}: Failed to save config file {config_filename}: {str(e)}")
|
||||
return _format_response(500, "保存配置文件失败")
|
||||
else:
|
||||
# Copy default config for record keeping
|
||||
config_path = job_upload_dir / "config.yaml"
|
||||
with open(config_path, 'w', encoding='utf-8') as f:
|
||||
yaml.safe_dump(active_config, f, allow_unicode=True)
|
||||
active_config_path = config_path
|
||||
logger.info(f"Job {job_id}: Default config saved for record - Path: {config_path}")
|
||||
|
||||
# Initialize task status
|
||||
update_task_status(job_id, TASK_STATUS_PENDING, "Task queued for processing")
|
||||
logger.info(f"Job {job_id}: Task status initialized as PENDING")
|
||||
|
||||
# Start background processing
|
||||
try:
|
||||
thread = threading.Thread(
|
||||
target=process_data_async,
|
||||
args=(job_id, data_path, active_config_path, job_output_dir)
|
||||
)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
logger.info(f"Job {job_id}: Background processing thread started successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Job {job_id}: Failed to start background processing thread: {str(e)}")
|
||||
update_task_status(job_id, TASK_STATUS_FAILED, error=str(e))
|
||||
return _format_response(500, "启动处理失败")
|
||||
|
||||
logger.info(f"Job {job_id}: Upload process completed successfully, returning job ID to client")
|
||||
return _format_response(202, "任务已接受并加入处理队列", {
|
||||
"status": "accepted",
|
||||
"job_id": job_id,
|
||||
"task_status_url": f"/task/{job_id}"
|
||||
})
|
||||
233
src/gasflux/blueprints/web.py
Normal file
233
src/gasflux/blueprints/web.py
Normal file
@ -0,0 +1,233 @@
|
||||
"""
|
||||
Web Blueprint
|
||||
Provides web interface for the GasFlux API.
|
||||
"""
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
from flask import Blueprint, render_template_string, current_app
|
||||
|
||||
from ..shared import log_performance, logger
|
||||
from ..app import Config
|
||||
|
||||
# Create blueprint
|
||||
web_bp = Blueprint('web', __name__)
|
||||
|
||||
|
||||
@web_bp.route('/')
|
||||
@log_performance
|
||||
def index():
|
||||
logger.debug("Index page requested")
|
||||
|
||||
# 递归查找所有生成的 HTML 报告
|
||||
start_time = time.time()
|
||||
all_reports = []
|
||||
# 优先用 app.config 中的目录,其次回退到 Config.OUTPUT_FOLDER;都不存在则不列出文件
|
||||
output_root = current_app.config.get('OUTPUT_FOLDER') or getattr(Config, 'OUTPUT_FOLDER', None)
|
||||
if not output_root:
|
||||
output_path = None
|
||||
else:
|
||||
output_path = Path(output_root)
|
||||
|
||||
if output_path and output_path.exists():
|
||||
try:
|
||||
for file in output_path.rglob("*.html"):
|
||||
# 获取相对于 OUTPUT_FOLDER 的相对路径,用于下载链接
|
||||
rel_path = file.relative_to(output_path).as_posix()
|
||||
all_reports.append(rel_path)
|
||||
|
||||
scan_duration = time.time() - start_time
|
||||
logger.debug(f"Report scan completed in {scan_duration:.3f}s - found {len(all_reports)} HTML reports")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error scanning for reports: {str(e)}")
|
||||
all_reports = []
|
||||
else:
|
||||
logger.debug("No output directory configured yet, skipping report scan")
|
||||
all_reports = []
|
||||
|
||||
return render_template_string('''
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GasFlux Web API</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 40px; line-height: 1.6; background-color: #f4f7f6; }
|
||||
.container { max-width: 900px; margin: auto; background: white; padding: 30px; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
|
||||
h1 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
|
||||
.upload-section { background: #f8f9fa; padding: 25px; border-radius: 8px; border-left: 5px solid #3498db; margin-bottom: 30px; }
|
||||
.form-group { margin-bottom: 20px; }
|
||||
label { display: block; font-weight: bold; margin-bottom: 8px; color: #34495e; }
|
||||
input[type="file"] { display: block; width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
|
||||
input[type="submit"] { background: #3498db; color: white; border: none; padding: 12px 25px; border-radius: 4px; cursor: pointer; font-size: 16px; transition: background 0.3s; }
|
||||
input[type="submit"]:hover { background: #2980b9; }
|
||||
.results-section { margin-top: 40px; }
|
||||
.report-item { margin-bottom: 15px; padding: 15px; border: 1px solid #eee; border-radius: 6px; display: flex; justify-content: space-between; align-items: center; }
|
||||
.report-info { display: flex; flex-direction: column; }
|
||||
.report-link { font-weight: bold; color: #2980b9; text-decoration: none; font-size: 1.1em; }
|
||||
.report-link:hover { text-decoration: underline; }
|
||||
.report-path { font-size: 0.85em; color: #7f8c8d; margin-top: 4px; }
|
||||
.api-docs { margin-top: 50px; padding: 20px; background: #e8f4f8; border-radius: 8px; font-size: 0.9em; }
|
||||
code { background: #eee; padding: 2px 5px; border-radius: 3px; font-family: monospace; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>GasFlux Web API 控制台</h1>
|
||||
|
||||
<div class="upload-section">
|
||||
<h2>新建处理任务</h2>
|
||||
<form id="uploadForm" enctype=multipart/form-data>
|
||||
<div class="form-group">
|
||||
<label for="data_file">数据文件 (Excel):</label>
|
||||
<input type="file" name="file" id="data_file" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="config_file">配置文件 (YAML) [可选]:</label>
|
||||
<input type="file" name="config" id="config_file">
|
||||
</div>
|
||||
<button type="submit" id="submitBtn">开始上传并分析</button>
|
||||
</form>
|
||||
<div id="taskStatus" style="display: none; margin-top: 20px; padding: 15px; background: #e8f8e8; border-radius: 5px; border: 1px solid #28a745;">
|
||||
<h3>任务状态</h3>
|
||||
<p id="statusMessage">正在上传文件...</p>
|
||||
<div id="progressBar" style="width: 100%; height: 20px; background: #f0f0f0; border-radius: 10px; margin: 10px 0; display: none;">
|
||||
<div id="progressFill" style="height: 100%; background: #28a745; border-radius: 10px; width: 0%; transition: width 0.3s;"></div>
|
||||
</div>
|
||||
<p id="taskId" style="font-size: 0.9em; color: #666;"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="results-section">
|
||||
<h2>已生成的报告</h2>
|
||||
<div id="reports">
|
||||
{% for report in reports %}
|
||||
<div class="report-item">
|
||||
<div class="report-info">
|
||||
<a class="report-link" href="/download/{{ report }}" target="_blank">{{ report.split('/')[-1] }}</a>
|
||||
<span class="report-path">任务 ID: {{ report.split('/')[0] }}</span>
|
||||
</div>
|
||||
<a href="/download/{{ report }}" download class="report-link" style="font-size: 0.9em;">下载</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<p style="color: #95a5a6;">暂无已生成的报告。</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-docs">
|
||||
<h3>API 调用指南 (开发者)</h3>
|
||||
<p><strong>健康检查:</strong> <code>GET /health</code></p>
|
||||
<p><strong>上传分析:</strong> <code>POST /upload</code></p>
|
||||
<p><strong>查询任务状态:</strong> <code>GET /task/<task_id></code></p>
|
||||
<p>参数: <code>file</code> (Excel), <code>config</code> (YAML, 可选)</p>
|
||||
<p>示例: <code>curl -X POST -F "file=@data.xlsx" http://localhost:5000/upload</code></p>
|
||||
<p>状态查询: <code>curl http://localhost:5000/task/your-task-id</code></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('uploadForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const taskStatus = document.getElementById('taskStatus');
|
||||
const statusMessage = document.getElementById('statusMessage');
|
||||
const taskIdElement = document.getElementById('taskId');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const progressFill = document.getElementById('progressFill');
|
||||
|
||||
// Disable form and show status
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '上传中...';
|
||||
taskStatus.style.display = 'block';
|
||||
progressBar.style.display = 'block';
|
||||
progressFill.style.width = '10%';
|
||||
|
||||
try {
|
||||
// Upload file
|
||||
statusMessage.textContent = '正在上传文件...';
|
||||
const response = await fetch('/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const taskId = result.data.job_id;
|
||||
|
||||
statusMessage.textContent = '文件上传成功,开始处理数据...';
|
||||
taskIdElement.textContent = `任务ID: ${taskId}`;
|
||||
progressFill.style.width = '30%';
|
||||
|
||||
// Poll for status
|
||||
let pollCount = 0;
|
||||
const maxPolls = 300; // 5 minutes max (every 1 second)
|
||||
|
||||
const pollStatus = async () => {
|
||||
try {
|
||||
const statusResponse = await fetch(`/task/${taskId}`);
|
||||
const status = await statusResponse.json();
|
||||
|
||||
if (status.data.status === 'completed') {
|
||||
statusMessage.textContent = '处理完成!正在准备下载链接...';
|
||||
progressFill.style.width = '100%';
|
||||
submitBtn.textContent = '处理完成!';
|
||||
submitBtn.disabled = false;
|
||||
|
||||
// Reload page to show new reports
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
|
||||
} else if (status.data.status === 'failed') {
|
||||
statusMessage.textContent = `处理失败: ${status.data.error || '未知错误'}`;
|
||||
progressFill.style.backgroundColor = '#dc3545';
|
||||
progressFill.style.width = '100%';
|
||||
submitBtn.textContent = '处理失败';
|
||||
submitBtn.disabled = false;
|
||||
|
||||
} else {
|
||||
// Still processing
|
||||
statusMessage.textContent = status.data.message || '正在处理中...';
|
||||
const progressPercent = Math.min(30 + (pollCount * 70 / maxPolls), 90);
|
||||
progressFill.style.width = `${progressPercent}%`;
|
||||
|
||||
pollCount++;
|
||||
if (pollCount < maxPolls) {
|
||||
setTimeout(pollStatus, 1000);
|
||||
} else {
|
||||
statusMessage.textContent = '处理超时,请稍后手动检查状态';
|
||||
submitBtn.textContent = '处理超时';
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Status check failed:', error);
|
||||
statusMessage.textContent = '状态检查失败,请手动刷新页面查看结果';
|
||||
submitBtn.textContent = '状态检查失败';
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Start polling
|
||||
setTimeout(pollStatus, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error);
|
||||
statusMessage.textContent = `上传失败: ${error.message}`;
|
||||
progressFill.style.backgroundColor = '#dc3545';
|
||||
progressFill.style.width = '100%';
|
||||
submitBtn.textContent = '上传失败';
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
''', reports=all_reports)
|
||||
@ -29,13 +29,14 @@ from datetime import datetime
|
||||
import sys
|
||||
import os
|
||||
from collections import Counter
|
||||
import yaml
|
||||
|
||||
try:
|
||||
from tqdm import tqdm
|
||||
HAS_TQDM = True
|
||||
except ImportError:
|
||||
HAS_TQDM = False
|
||||
print("⚠️ 未安装tqdm库,将不显示进度条。如需进度条,请运行: pip install tqdm")
|
||||
print("WARNING: 未安装tqdm库,将不显示进度条。如需进度条,请运行: pip install tqdm")
|
||||
|
||||
|
||||
def create_height_bins(heights, bin_size=2.0):
|
||||
@ -82,7 +83,7 @@ def create_height_bins(heights, bin_size=2.0):
|
||||
# 导入qiya模块
|
||||
try:
|
||||
from .qiya import get_pressure_at_location
|
||||
print("✅ 成功导入qiya模块")
|
||||
|
||||
except ImportError as e:
|
||||
print(f"❌ 导入qiya模块失败: {e}")
|
||||
print("请确保GasFlux包结构完整")
|
||||
@ -104,13 +105,13 @@ def load_excel_data(file_path):
|
||||
|
||||
# 读取Excel文件
|
||||
df = pd.read_excel(file_path)
|
||||
print(f"✅ 成功读取数据:{len(df)} 行,{len(df.columns)} 列")
|
||||
print(f"SUCCESS: Successfully loaded data: {len(df)} rows, {len(df.columns)} columns")
|
||||
print(f"列名:{list(df.columns)}")
|
||||
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 读取文件失败: {e}")
|
||||
print(f"ERROR: Failed to read file: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@ -132,11 +133,11 @@ def remove_columns(df, columns_to_remove):
|
||||
missing_columns = [col for col in columns_to_remove if col not in df.columns]
|
||||
|
||||
if missing_columns:
|
||||
print(f"⚠️ 以下列不存在(跳过): {missing_columns}")
|
||||
print(f"以下列不存在(跳过): {missing_columns}")
|
||||
|
||||
if existing_columns:
|
||||
df = df.drop(columns=existing_columns)
|
||||
print(f"✅ 已删除 {len(existing_columns)} 列")
|
||||
print(f"已删除 {len(existing_columns)} 列")
|
||||
|
||||
return df
|
||||
|
||||
@ -158,7 +159,7 @@ def extract_hour_from_filename(filename):
|
||||
if match:
|
||||
return match.group(1)
|
||||
else:
|
||||
print(f"⚠️ 无法从文件名 '{filename}' 中提取小时信息,使用默认值 '00'")
|
||||
print(f"无法从文件名 '{filename}' 中提取小时信息,使用默认值 '00'")
|
||||
return "00"
|
||||
|
||||
|
||||
@ -181,7 +182,7 @@ def fix_time_column(df, filename):
|
||||
|
||||
# 检查时间列是否存在
|
||||
if '时间' not in df.columns:
|
||||
print("❌ 未找到 '时间' 列")
|
||||
print("未找到 '时间' 列")
|
||||
return df
|
||||
|
||||
# 修正时间格式
|
||||
@ -231,18 +232,18 @@ def convert_coordinates(df):
|
||||
"""
|
||||
print("转换经纬度坐标...")
|
||||
|
||||
if '经度' in df.columns:
|
||||
original_lon = df['经度'].head(3).tolist()
|
||||
df['经度'] = df['经度'] / 1e7
|
||||
converted_lon = df['经度'].head(3).tolist()
|
||||
if 'stGPSPositionX' in df.columns:
|
||||
original_lon = df['stGPSPositionX'].head(3).tolist()
|
||||
df['stGPSPositionX'] = df['stGPSPositionX'] / 1e7
|
||||
converted_lon = df['stGPSPositionX'].head(3).tolist()
|
||||
print("经度转换示例:")
|
||||
for orig, conv in zip(original_lon, converted_lon):
|
||||
print(".6f")
|
||||
|
||||
if '纬度' in df.columns:
|
||||
original_lat = df['纬度'].head(3).tolist()
|
||||
df['纬度'] = df['纬度'] / 1e7
|
||||
converted_lat = df['纬度'].head(3).tolist()
|
||||
if 'stGPSPositionY' in df.columns:
|
||||
original_lat = df['stGPSPositionY'].head(3).tolist()
|
||||
df['stGPSPositionY'] = df['stGPSPositionY'] / 1e7
|
||||
converted_lat = df['stGPSPositionY'].head(3).tolist()
|
||||
print("纬度转换示例:")
|
||||
for orig, conv in zip(original_lat, converted_lat):
|
||||
print(".6f")
|
||||
@ -265,42 +266,54 @@ def calculate_pressure(df, max_samples=None, height_tolerance=10.0, height_bin_s
|
||||
"""
|
||||
print("计算气压数据...")
|
||||
|
||||
# 检查必要列是否存在
|
||||
required_cols = ['日期', '时间', '经度', '纬度', '融合高程']
|
||||
# 检查必要列是否存在(支持原始列名和新列名)
|
||||
# 原始数据中的列名
|
||||
original_cols = ['qStrDate', 'qStrTime', 'stGPSPositionX', 'stGPSPositionY', 'fAltitudeFused']
|
||||
# 坐标转换后的列名(经纬度已被除以1e7)
|
||||
converted_cols = ['qStrDate', 'qStrTime', 'stGPSPositionX', 'stGPSPositionY', 'fAltitudeFused']
|
||||
|
||||
# 优先使用转换后的列名(如果存在),否则使用原始列名
|
||||
date_col = 'qStrDate' if 'qStrDate' in df.columns else '日期'
|
||||
time_col = 'qStrTime' if 'qStrTime' in df.columns else '时间'
|
||||
lon_col = 'stGPSPositionX' if 'stGPSPositionX' in df.columns else '经度'
|
||||
lat_col = 'stGPSPositionY' if 'stGPSPositionY' in df.columns else '纬度'
|
||||
height_col = 'fAltitudeFused' if 'fAltitudeFused' in df.columns else '融合高程'
|
||||
|
||||
required_cols = [date_col, time_col, lon_col, lat_col, height_col]
|
||||
missing_cols = [col for col in required_cols if col not in df.columns]
|
||||
|
||||
if missing_cols:
|
||||
print(f"❌ 缺少必要列: {missing_cols}")
|
||||
print(f"缺少必要列: {missing_cols}")
|
||||
return df
|
||||
|
||||
# 检查高度变化范围
|
||||
height_min = df['融合高程'].min()
|
||||
height_max = df['融合高程'].max()
|
||||
height_min = df[height_col].min()
|
||||
height_max = df[height_col].max()
|
||||
height_range = height_max - height_min
|
||||
|
||||
print(f"🏔️ 高度范围: {height_min:.1f} - {height_max:.1f} 米 (变化: {height_range:.1f} 米)")
|
||||
print(f"高度范围: {height_min:.1f} - {height_max:.1f} 米 (变化: {height_range:.1f} 米)")
|
||||
# 创建高度分档
|
||||
height_bins = create_height_bins(df['融合高程'], height_bin_size)
|
||||
print(f"📏 高度分档: {len(height_bins)} 个档位 (间隔: {height_bin_size:.1f} 米)")
|
||||
height_bins = create_height_bins(df[height_col], height_bin_size)
|
||||
print(f"高度分档: {len(height_bins)} 个档位 (间隔: {height_bin_size:.1f} 米)")
|
||||
|
||||
for i, (bin_min, bin_max, bin_center, count) in enumerate(height_bins):
|
||||
print(f" 档位{i+1}: {bin_min:.1f}-{bin_max:.1f}m (中心: {bin_center:.1f}m, 数据: {count}行)")
|
||||
# 决定计算策略
|
||||
if height_range <= height_tolerance:
|
||||
# 高度变化小,只计算一次气压
|
||||
print("🎯 高度变化小,将使用平均高度计算一次气压")
|
||||
print("高度变化小,将使用平均高度计算一次气压")
|
||||
use_single_calculation = True
|
||||
mean_height = df['融合高程'].mean()
|
||||
print(f"📍 使用平均高度: {mean_height:.1f} 米")
|
||||
mean_height = df[height_col].mean()
|
||||
print(f"使用平均高度: {mean_height:.1f} 米")
|
||||
elif len(height_bins) == 1:
|
||||
# 只有一个高度档位,使用档位中心高度
|
||||
print("📦 只有一个高度档位,使用档位中心高度")
|
||||
print("只有一个高度档位,使用档位中心高度")
|
||||
use_single_calculation = True
|
||||
mean_height = height_bins[0][2] # bin_center
|
||||
print(f"📍 使用档位中心高度: {mean_height:.1f} 米")
|
||||
print(f"使用档位中心高度: {mean_height:.1f} 米")
|
||||
else:
|
||||
# 高度变化大,使用分档计算
|
||||
print("🏗️ 使用高度分档策略,减少API调用")
|
||||
print("使用高度分档策略,减少API调用")
|
||||
use_single_calculation = False
|
||||
|
||||
# 确定要处理的行数
|
||||
@ -309,10 +322,10 @@ def calculate_pressure(df, max_samples=None, height_tolerance=10.0, height_bin_s
|
||||
sample_df = df.copy()
|
||||
actual_samples = len(df)
|
||||
if not use_single_calculation:
|
||||
print(f"📊 将计算所有 {len(df)} 行的气压数据")
|
||||
print(f"将计算所有 {len(df)} 行的气压数据")
|
||||
else:
|
||||
# 限制采样数量
|
||||
print(f"⚠️ 数据量较大 ({len(df)} 行),只对前 {max_samples} 行计算气压")
|
||||
print(f"数据量较大 ({len(df)} 行),只对前 {max_samples} 行计算气压")
|
||||
sample_df = df.head(max_samples).copy()
|
||||
actual_samples = max_samples
|
||||
|
||||
@ -325,11 +338,11 @@ def calculate_pressure(df, max_samples=None, height_tolerance=10.0, height_bin_s
|
||||
first_row = sample_df.iloc[0]
|
||||
|
||||
# 转换日期格式 - 只提取日期部分,移除任何时间信息
|
||||
date_str = str(first_row['日期'])
|
||||
date_str = str(first_row[date_col])
|
||||
if ' ' in date_str:
|
||||
date_str = date_str.split(' ')[0]
|
||||
date_str = date_str.split(' ')[0] # 处理 "2026-01-15 00:00:00" 格式
|
||||
elif 'T' in date_str:
|
||||
date_str = date_str.split('T')[0]
|
||||
date_str = date_str.split('T')[0] # 处理ISO格式
|
||||
|
||||
if '/' in date_str:
|
||||
date_str = date_str.replace('/', '-')
|
||||
@ -343,8 +356,15 @@ def calculate_pressure(df, max_samples=None, height_tolerance=10.0, height_bin_s
|
||||
|
||||
# 使用数据的代表性时间(整点小时,众数)
|
||||
time_strings = []
|
||||
for time_val in sample_df['时间']:
|
||||
for time_val in sample_df[time_col]:
|
||||
time_str = str(time_val).strip()
|
||||
|
||||
# 处理时间字符串,提取正确的部分
|
||||
# 如果时间字符串包含多个时间部分(如 "00:00:00 08:37:12"),取最后一个
|
||||
if ' ' in time_str:
|
||||
time_parts = time_str.split()
|
||||
time_str = time_parts[-1] # 取最后一个有效的时间部分
|
||||
|
||||
if ':' in time_str:
|
||||
# 确保是有效的 HH:MM 格式,然后取整点小时
|
||||
parts = time_str.split(':')
|
||||
@ -368,8 +388,8 @@ def calculate_pressure(df, max_samples=None, height_tolerance=10.0, height_bin_s
|
||||
|
||||
print("正在计算平均气压...")
|
||||
pressure = get_pressure_at_location(
|
||||
lat=sample_df['纬度'].mean(),
|
||||
lon=sample_df['经度'].mean(),
|
||||
lat=sample_df[lat_col].mean(),
|
||||
lon=sample_df[lon_col].mean(),
|
||||
altitude=mean_height,
|
||||
date=formatted_date,
|
||||
time=formatted_time
|
||||
@ -377,18 +397,18 @@ def calculate_pressure(df, max_samples=None, height_tolerance=10.0, height_bin_s
|
||||
|
||||
if pressure is not None:
|
||||
pressures = [pressure] * len(sample_df)
|
||||
print("✅ 平均气压计算成功,将应用到所有行")
|
||||
print("平均气压计算成功,将应用到所有行")
|
||||
else:
|
||||
print("❌ 平均气压计算失败")
|
||||
print("平均气压计算失败")
|
||||
pressures = [None] * len(sample_df)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 平均气压计算失败: {e}")
|
||||
print(f"平均气压计算失败: {e}")
|
||||
pressures = [None] * len(sample_df)
|
||||
|
||||
else:
|
||||
# 使用高度分档策略
|
||||
print("🏗️ 开始分档计算气压...")
|
||||
print("开始分档计算气压...")
|
||||
|
||||
# 为每个高度档位计算气压
|
||||
bin_pressures = {} # bin_center -> pressure
|
||||
@ -402,19 +422,19 @@ def calculate_pressure(df, max_samples=None, height_tolerance=10.0, height_bin_s
|
||||
try:
|
||||
# 使用第一行数据作为代表来获取日期和时间
|
||||
# 找到这个档位中的一行数据
|
||||
bin_rows = sample_df[(sample_df['融合高程'] >= bin_min) &
|
||||
(sample_df['融合高程'] <= bin_max)]
|
||||
bin_rows = sample_df[(sample_df[height_col] >= bin_min) &
|
||||
(sample_df[height_col] <= bin_max)]
|
||||
if len(bin_rows) == 0:
|
||||
continue
|
||||
|
||||
first_row = bin_rows.iloc[0]
|
||||
|
||||
# 转换日期格式 - 只提取日期部分,移除任何时间信息
|
||||
date_str = str(first_row['日期'])
|
||||
date_str = str(first_row[date_col])
|
||||
if ' ' in date_str:
|
||||
date_str = date_str.split(' ')[0]
|
||||
date_str = date_str.split(' ')[0] # 处理 "2026-01-15 00:00:00" 格式
|
||||
elif 'T' in date_str:
|
||||
date_str = date_str.split('T')[0]
|
||||
date_str = date_str.split('T')[0] # 处理ISO格式
|
||||
|
||||
if '/' in date_str:
|
||||
date_str = date_str.replace('/', '-')
|
||||
@ -424,14 +444,21 @@ def calculate_pressure(df, max_samples=None, height_tolerance=10.0, height_bin_s
|
||||
year, month, day = date_parts
|
||||
formatted_date = f"{year}-{month.zfill(2)}-{day.zfill(2)}"
|
||||
else:
|
||||
print(f"⚠️ 档位高度 {bin_center:.1f}m 日期格式异常: {date_str}")
|
||||
print(f"档位高度 {bin_center:.1f}m 日期格式异常: {date_str}")
|
||||
bin_pressures[bin_center] = None
|
||||
continue
|
||||
|
||||
# 使用该档位数据的代表性时间(整点小时)
|
||||
time_strings = []
|
||||
for time_val in bin_rows['时间']:
|
||||
for time_val in bin_rows[time_col]:
|
||||
time_str = str(time_val).strip()
|
||||
|
||||
# 处理时间字符串,提取正确的部分
|
||||
# 如果时间字符串包含多个时间部分(如 "00:00:00 08:37:12"),取最后一个
|
||||
if ' ' in time_str:
|
||||
time_parts = time_str.split()
|
||||
time_str = time_parts[-1] # 取最后一个有效的时间部分
|
||||
|
||||
if ':' in time_str:
|
||||
# 确保是有效的 HH:MM 格式,然后取整点小时
|
||||
parts = time_str.split(':')
|
||||
@ -456,8 +483,8 @@ def calculate_pressure(df, max_samples=None, height_tolerance=10.0, height_bin_s
|
||||
print(" 无有效时间数据,使用默认中午12:00")
|
||||
|
||||
# 计算这个档位的气压(使用平均位置和档位中心高度)
|
||||
avg_lat = bin_rows['纬度'].mean()
|
||||
avg_lon = bin_rows['经度'].mean()
|
||||
avg_lat = bin_rows[lat_col].mean()
|
||||
avg_lon = bin_rows[lon_col].mean()
|
||||
|
||||
pressure = get_pressure_at_location(
|
||||
lat=avg_lat,
|
||||
@ -475,14 +502,14 @@ def calculate_pressure(df, max_samples=None, height_tolerance=10.0, height_bin_s
|
||||
iterator.set_description(f"计算档位 (成功: {success_count}/{len(bin_pressures)})")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 计算高度档位 {bin_center:.1f}m 气压失败: {e}")
|
||||
print(f"计算高度档位 {bin_center:.1f}m 气压失败: {e}")
|
||||
bin_pressures[bin_center] = None
|
||||
|
||||
# 为每一行分配对应档位的气压
|
||||
pressures = []
|
||||
for idx, row in sample_df.iterrows():
|
||||
# 找到这个高度对应的档位
|
||||
height = row['融合高程']
|
||||
height = row[height_col]
|
||||
assigned_pressure = None
|
||||
|
||||
for bin_min, bin_max, bin_center, count in height_bins:
|
||||
@ -492,7 +519,7 @@ def calculate_pressure(df, max_samples=None, height_tolerance=10.0, height_bin_s
|
||||
|
||||
pressures.append(assigned_pressure)
|
||||
|
||||
print(f"✅ 完成分档气压计算,共 {len(bin_pressures)} 个档位,{len([p for p in bin_pressures.values() if p is not None])} 个成功")
|
||||
print(f"完成分档气压计算,共 {len(bin_pressures)} 个档位,{len([p for p in bin_pressures.values() if p is not None])} 个成功")
|
||||
|
||||
# 添加气压列
|
||||
df['pressure'] = None # 初始化
|
||||
@ -513,7 +540,7 @@ def calculate_pressure(df, max_samples=None, height_tolerance=10.0, height_bin_s
|
||||
avg_pressure = sum(valid_pressures) / len(valid_pressures)
|
||||
print(f"成功计算 {len(valid_pressures)}/{actual_samples} 个气压值,平均值: {avg_pressure:.1f} hPa")
|
||||
else:
|
||||
print("⚠️ 未能计算出任何气压值")
|
||||
print("未能计算出任何气压值")
|
||||
return df
|
||||
|
||||
|
||||
@ -529,13 +556,13 @@ def adjust_altitude(df):
|
||||
"""
|
||||
print("调整融合高程...")
|
||||
|
||||
if '融合高程' in df.columns:
|
||||
min_altitude = df['融合高程'].min()
|
||||
if 'fAltitudeFused' in df.columns:
|
||||
min_altitude = df['fAltitudeFused'].min()
|
||||
print(".2f")
|
||||
|
||||
original_alt = df['融合高程'].head(3).tolist()
|
||||
df['融合高程'] = df['融合高程'] - min_altitude
|
||||
adjusted_alt = df['融合高程'].head(3).tolist()
|
||||
original_alt = df['fAltitudeFused'].head(3).tolist()
|
||||
df['fAltitudeFused'] = df['fAltitudeFused'] - min_altitude
|
||||
adjusted_alt = df['fAltitudeFused'].head(3).tolist()
|
||||
|
||||
print("高度调整示例:")
|
||||
for orig, adj in zip(original_alt, adjusted_alt):
|
||||
@ -546,7 +573,7 @@ def adjust_altitude(df):
|
||||
|
||||
def merge_timestamp(df):
|
||||
"""
|
||||
融合日期和时间列为时间戳
|
||||
融合日期和时间列为时间戳(修正时间格式)
|
||||
|
||||
Args:
|
||||
df: 输入DataFrame
|
||||
@ -554,57 +581,48 @@ def merge_timestamp(df):
|
||||
Returns:
|
||||
pd.DataFrame: 融合后的DataFrame
|
||||
"""
|
||||
print("融合日期和时间...")
|
||||
print("融合日期和时间(修正时间格式)...")
|
||||
|
||||
if '日期' in df.columns and '时间' in df.columns:
|
||||
if 'qStrDate' in df.columns and 'qStrTime' in df.columns:
|
||||
timestamps = []
|
||||
|
||||
for idx, row in df.iterrows():
|
||||
try:
|
||||
date_str = str(row['日期'])
|
||||
time_str = str(row['时间'])
|
||||
date_str = str(row['qStrDate']).strip()
|
||||
time_str = str(row['qStrTime']).strip()
|
||||
|
||||
# 清理日期字符串 - 移除任何时间部分
|
||||
date_str = date_str.strip()
|
||||
# 处理日期字符串,提取纯日期部分
|
||||
# 如果日期字符串包含时间部分(如 "2026-01-15 00:00:00"),取日期部分
|
||||
if ' ' in date_str:
|
||||
date_str = date_str.split(' ')[0] # 只取日期部分
|
||||
if 'T' in date_str:
|
||||
date_str = date_str.split('T')[0] # 处理ISO格式
|
||||
date_parts = date_str.split()
|
||||
date_str = date_parts[0] # 取第一个部分作为日期
|
||||
|
||||
# 标准化日期格式
|
||||
if '/' in date_str:
|
||||
date_str = date_str.replace('/', '-')
|
||||
# 处理时间字符串,提取正确的部分
|
||||
# 如果时间字符串包含多个时间部分(如 "00:00:00 08:37:12"),取最后一个
|
||||
if ' ' in time_str:
|
||||
time_parts = time_str.split()
|
||||
# 取最后一个有效的时间部分
|
||||
time_str = time_parts[-1]
|
||||
|
||||
# 确保日期格式正确
|
||||
date_parts = date_str.split('-')
|
||||
if len(date_parts) == 3:
|
||||
year, month, day = date_parts
|
||||
date_formatted = f"{year.zfill(4)}-{month.zfill(2)}-{day.zfill(2)}"
|
||||
else:
|
||||
print(f"⚠️ 日期格式异常: '{date_str}',使用当前日期")
|
||||
date_formatted = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
# 时间字符串已经是修正后的格式(如 "08:34:01"),直接使用
|
||||
time_str = time_str.strip()
|
||||
# 确保时间格式正确
|
||||
if ':' in time_str and len(time_str.split(':')) >= 2:
|
||||
time_formatted = time_str
|
||||
# 组合日期和修正后的时间
|
||||
timestamp = f"{date_str} {time_str}"
|
||||
else:
|
||||
print(f"⚠️ 时间格式异常: '{time_str}',使用默认时间")
|
||||
time_formatted = "12:00:00"
|
||||
timestamp = f"{date_str} 12:00:00" # 默认中午时间
|
||||
print(f" 时间格式异常 '{row['qStrTime']}',使用默认时间")
|
||||
|
||||
# 组合时间戳 - 直接连接日期和时间
|
||||
timestamp = f"{date_formatted} {time_formatted}"
|
||||
timestamps.append(timestamp)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 处理第 {idx+1} 行时间戳失败: {e}")
|
||||
print(f"处理第 {idx+1} 行时间戳失败: {e}")
|
||||
timestamps.append(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
df['timestamp'] = timestamps
|
||||
|
||||
print("时间戳融合示例:")
|
||||
for i in range(min(3, len(timestamps))):
|
||||
print(f" {df.loc[i, '日期']} + {df.loc[i, '时间']} → {timestamps[i]}")
|
||||
print(f" {df.loc[i, 'qStrDate']} + {df.loc[i, 'qStrTime']} → {timestamps[i]}")
|
||||
|
||||
return df
|
||||
|
||||
@ -624,82 +642,103 @@ def rename_columns(df):
|
||||
# 定义字段映射
|
||||
column_mapping = {
|
||||
'timestamp': 'timestamp', # 时间戳(已创建)
|
||||
'经度': 'longitude', # 经度 → latitude
|
||||
'纬度': 'latitude', # 纬度 → longitude
|
||||
'融合高程': 'height_ato', # 融合高程 → height_ato
|
||||
'修正风向': 'winddir', # 修正风向 → winddir
|
||||
'修正风速': 'windspeed', # 修正风速 → windspeed
|
||||
'风温': 'temperature', # 风温 → temperature
|
||||
'pressure': 'pressure', # 气压(已计算)
|
||||
'CH4': 'ch4', # CH4保持不变
|
||||
'stGPSPositionX': 'longitude', # 经度 → longitude
|
||||
'stGPSPositionY': 'latitude', # 纬度 → latitude
|
||||
'fAltitudeFused': 'height_ato', # 融合高程 → height_ato
|
||||
'fFixedWindDirection': 'winddir', # 修正风向 → winddir
|
||||
'fFixedWindSpeed': 'windspeed', # 修正风速 → windspeed
|
||||
'fWindTemperature': 'temperature', # 风温 → temperature
|
||||
'CO2': 'CO2', # CO2浓度 → co2
|
||||
'pitch': 'course_elevation', # pitch → course_elevation
|
||||
'yaw': 'course_azimuth' # yaw → course_azimuth
|
||||
}
|
||||
|
||||
# 重命名存在的列
|
||||
columns_to_rename = {}
|
||||
# 先复制字段,再对复制的字段重命名
|
||||
columns_renamed = []
|
||||
for old_name, new_name in column_mapping.items():
|
||||
if old_name in df.columns:
|
||||
columns_to_rename[old_name] = new_name
|
||||
# 复制字段到新名称
|
||||
df[new_name] = df[old_name].copy()
|
||||
columns_renamed.append((old_name, new_name))
|
||||
print(f" 复制并重命名: {old_name} → {new_name}")
|
||||
|
||||
if columns_to_rename:
|
||||
df = df.rename(columns=columns_to_rename)
|
||||
print("字段重命名:")
|
||||
for old, new in columns_to_rename.items():
|
||||
print(f" {old} → {new}")
|
||||
|
||||
# 只保留GasFlux需要的列
|
||||
required_columns = ['timestamp', 'latitude', 'longitude', 'height_ato', 'windspeed', 'winddir', 'temperature', 'pressure', 'ch4', 'course_elevation', 'course_azimuth']
|
||||
existing_required_columns = [col for col in required_columns if col in df.columns]
|
||||
|
||||
if len(existing_required_columns) != len(required_columns):
|
||||
missing = [col for col in required_columns if col not in df.columns]
|
||||
print(f"⚠️ 缺少必需列: {missing}")
|
||||
|
||||
# 移除不需要的列,只保留必需的列
|
||||
df = df[existing_required_columns]
|
||||
print(f"最终保留列: {existing_required_columns}")
|
||||
if columns_renamed:
|
||||
print(f"共处理了 {len(columns_renamed)} 个字段")
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def process_excel_file(file_path):
|
||||
def ensure_float64_types(df, config=None):
|
||||
"""
|
||||
确保数值字段为float64类型,以满足GasFlux处理要求
|
||||
|
||||
Args:
|
||||
df: 输入DataFrame
|
||||
config: 配置字典,包含gases字段
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: 数据类型转换后的DataFrame
|
||||
"""
|
||||
print("确保数值字段类型为float64...")
|
||||
|
||||
# 定义基础需要转换为float64的字段
|
||||
float64_columns = [
|
||||
'longitude', 'latitude', 'height_ato', # 位置和高度
|
||||
'winddir', 'windspeed', 'temperature', # 风和温度
|
||||
'pressure', # 气压
|
||||
'course_elevation', 'course_azimuth' # 姿态角
|
||||
]
|
||||
|
||||
# 如果提供了配置,添加gases中的气体列
|
||||
if config and 'gases' in config:
|
||||
gas_columns = list(config['gases'].keys())
|
||||
float64_columns.extend(gas_columns)
|
||||
print(f"从配置中添加气体列: {gas_columns}")
|
||||
|
||||
converted_count = 0
|
||||
for col in float64_columns:
|
||||
if col in df.columns:
|
||||
try:
|
||||
original_dtype = df[col].dtype
|
||||
df[col] = df[col].astype('float64')
|
||||
new_dtype = df[col].dtype
|
||||
if original_dtype != new_dtype:
|
||||
print(f" 转换: {col} ({original_dtype} → {new_dtype})")
|
||||
converted_count += 1
|
||||
except Exception as e:
|
||||
print(f" 转换失败: {col} - {e}")
|
||||
|
||||
if converted_count > 0:
|
||||
print(f"共转换了 {converted_count} 个字段的数据类型为float64")
|
||||
else:
|
||||
print("所有数值字段已经是float64类型")
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def process_excel_file(file_path, config_path=None):
|
||||
"""
|
||||
处理单个Excel文件的主函数
|
||||
|
||||
Args:
|
||||
file_path: Excel文件路径
|
||||
config_path: 配置文件路径,用于读取gases配置
|
||||
"""
|
||||
print(f"=== 开始处理文件: {file_path} ===\n")
|
||||
|
||||
# 获取文件名(用于时间修正)
|
||||
filename = Path(file_path).name
|
||||
|
||||
# 1. 读取数据
|
||||
df = load_excel_data(file_path)
|
||||
|
||||
# 2. 删除不需要的列
|
||||
columns_to_remove = [
|
||||
'高程', '速度x', '速度y', '速度z',
|
||||
'四元数_q0', '四元数_q1', '四元数_q2', '四元数_q3',
|
||||
'roll', 'H2O', # 保留pitch和yaw,将重命名为course_elevation和course_azimuth
|
||||
'原始风向', '原始风速'
|
||||
]
|
||||
df = remove_columns(df, columns_to_remove)
|
||||
|
||||
# 3. 修正时间格式
|
||||
df = fix_time_column(df, filename)
|
||||
|
||||
# 4. 坐标转换
|
||||
# 2. 坐标转换
|
||||
df = convert_coordinates(df)
|
||||
|
||||
# 5. 计算气压
|
||||
# 4. 计算气压数据
|
||||
df = calculate_pressure(df, max_samples=None, height_tolerance=10.0, height_bin_size=2.0) # 计算所有行,高度容差10米,分档2米
|
||||
|
||||
# 6. 高度调整
|
||||
# 5. 高度调整
|
||||
df = adjust_altitude(df)
|
||||
|
||||
# 7. 时间戳融合
|
||||
# 6. 时间戳融合(保持原有时间格式)
|
||||
df = merge_timestamp(df)
|
||||
|
||||
# 调试:检查当前列
|
||||
@ -707,28 +746,41 @@ def process_excel_file(file_path):
|
||||
if 'timestamp' in df.columns:
|
||||
print(f"timestamp列示例: {df['timestamp'].head(3).tolist()}")
|
||||
|
||||
# 8. 字段重命名
|
||||
# 7. 字段重命名
|
||||
df = rename_columns(df)
|
||||
|
||||
# 8. 确保数值字段类型为float64
|
||||
# 读取配置以获取gases字段
|
||||
config = None
|
||||
if config_path:
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
except Exception as e:
|
||||
print(f"读取配置文件失败: {e}")
|
||||
|
||||
df = ensure_float64_types(df, config)
|
||||
|
||||
# 保存处理结果
|
||||
output_path = Path(file_path).with_suffix('.processed.csv')
|
||||
df.to_csv(output_path, index=False)
|
||||
df.to_csv(output_path, index=False, encoding='utf-8-sig')
|
||||
|
||||
print(f"\n✅ 处理完成!")
|
||||
print(f"📁 输出文件: {output_path}")
|
||||
print(f"📊 最终数据形状: {df.shape[0]} 行 × {df.shape[1]} 列")
|
||||
print(f"📋 最终列名: {list(df.columns)}")
|
||||
print(f"\n处理完成!")
|
||||
print(f"输出文件: {output_path}")
|
||||
print(f"最终数据形状: {df.shape[0]} 行 × {df.shape[1]} 列")
|
||||
print(f"最终列名: {list(df.columns)}")
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def process_file(input_file, output_file=None):
|
||||
def process_file(input_file, output_file=None, config_file=None):
|
||||
"""
|
||||
直接处理Excel文件的函数(不使用命令行参数)
|
||||
|
||||
Args:
|
||||
input_file: 输入Excel文件路径(字符串或Path对象)
|
||||
output_file: 输出CSV文件路径(可选,字符串或Path对象)
|
||||
config_file: 配置文件路径(可选,用于读取gases配置)
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: 处理后的DataFrame
|
||||
@ -744,13 +796,13 @@ def process_file(input_file, output_file=None):
|
||||
raise ValueError(f"输入文件必须是Excel格式 (.xlsx 或 .xls),当前文件: {input_path}")
|
||||
|
||||
# 处理文件
|
||||
df = process_excel_file(str(input_path))
|
||||
df = process_excel_file(str(input_path), config_file)
|
||||
|
||||
# 如果指定了输出路径,额外保存一份
|
||||
if output_file:
|
||||
output_path = Path(output_file)
|
||||
df.to_csv(output_path, index=False)
|
||||
print(f"📁 额外保存到: {output_path}")
|
||||
df.to_csv(output_path, index=False, encoding='utf-8-sig')
|
||||
print(f"额外保存到: {output_path}")
|
||||
|
||||
return df
|
||||
|
||||
@ -769,17 +821,17 @@ def interactive_input():
|
||||
while True:
|
||||
input_file = input("请输入Excel文件路径 (例如: data.xlsx): ").strip()
|
||||
if not input_file:
|
||||
print("❌ 文件路径不能为空,请重新输入")
|
||||
print("文件路径不能为空,请重新输入")
|
||||
continue
|
||||
|
||||
input_path = Path(input_file)
|
||||
if not input_path.exists():
|
||||
print(f"❌ 文件不存在: {input_path}")
|
||||
print(f"文件不存在: {input_path}")
|
||||
print("提示: 请确保文件路径正确,或者将文件放在当前目录下")
|
||||
continue
|
||||
|
||||
if input_path.suffix.lower() not in ['.xlsx', '.xls']:
|
||||
print(f"❌ 文件格式错误: {input_path.suffix}")
|
||||
print(f"文件格式错误: {input_path.suffix}")
|
||||
print("只支持 .xlsx 和 .xls 格式的Excel文件")
|
||||
continue
|
||||
|
||||
@ -791,13 +843,13 @@ def interactive_input():
|
||||
output_file = None
|
||||
print("使用默认输出文件名")
|
||||
|
||||
print(f"\n✅ 输入确认:")
|
||||
print(f"\n输入确认:")
|
||||
print(f" 输入文件: {input_file}")
|
||||
print(f" 输出文件: {output_file or '自动生成'}")
|
||||
|
||||
confirm = input("\n确认开始处理? (y/N): ").strip().lower()
|
||||
if confirm not in ['y', 'yes', '是', '确认']:
|
||||
print("❌ 用户取消操作")
|
||||
print("用户取消操作")
|
||||
return None, None
|
||||
|
||||
return input_file, output_file
|
||||
@ -823,7 +875,7 @@ def main(input_file=None, output_file=None, interactive=False):
|
||||
try:
|
||||
return process_file(input_file, output_file)
|
||||
except Exception as e:
|
||||
print(f"❌ 处理失败: {e}")
|
||||
print(f"处理失败: {e}")
|
||||
raise
|
||||
|
||||
# 否则使用命令行参数
|
||||
@ -881,7 +933,7 @@ def main(input_file=None, output_file=None, interactive=False):
|
||||
if not input_file:
|
||||
parser.print_help()
|
||||
print("\n" + "="*60)
|
||||
print("📖 使用示例:")
|
||||
print("使用示例:")
|
||||
print("="*60)
|
||||
print("1. 命令行模式:")
|
||||
print(" python data_processor.py your_file.xlsx")
|
||||
@ -900,11 +952,11 @@ def main(input_file=None, output_file=None, interactive=False):
|
||||
# 检查输入文件
|
||||
input_path = Path(input_file)
|
||||
if not input_path.exists():
|
||||
print(f"❌ 错误:输入文件不存在: {input_path}")
|
||||
print(f"错误:输入文件不存在: {input_path}")
|
||||
sys.exit(1)
|
||||
|
||||
if input_path.suffix.lower() not in ['.xlsx', '.xls']:
|
||||
print(f"❌ 错误:输入文件必须是Excel格式 (.xlsx 或 .xls)")
|
||||
print(f"错误:输入文件必须是Excel格式 (.xlsx 或 .xls)")
|
||||
sys.exit(1)
|
||||
|
||||
# 处理文件
|
||||
@ -914,16 +966,16 @@ def main(input_file=None, output_file=None, interactive=False):
|
||||
# 如果指定了输出路径,额外保存一份
|
||||
if output_file:
|
||||
output_path = Path(output_file)
|
||||
df.to_csv(output_path, index=False)
|
||||
print(f"📁 额外保存到: {output_path}")
|
||||
df.to_csv(output_path, index=False, encoding='utf-8-sig')
|
||||
print(f"额外保存到: {output_path}")
|
||||
|
||||
return df
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n⚠️ 用户中断处理")
|
||||
print("\n用户中断处理")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"\n❌ 处理失败: {e}")
|
||||
print(f"\n处理失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
@ -192,8 +192,13 @@ class SpiralSpatialProcessingStrategy(SpatialProcessingStrategy):
|
||||
self.data_processor.circle_center_x,
|
||||
self.data_processor.circle_center_y,
|
||||
) = processing.circle_deviation(self.data_processor.df, x_col="utm_easting", y_col="utm_northing")
|
||||
|
||||
# 使用配置中第一个气体的归一化列名
|
||||
primary_gas = self.data_processor.gases[0]
|
||||
y_col = f"{primary_gas}_normalised"
|
||||
|
||||
self.data_processor.df = processing.recentre_azimuth(
|
||||
self.data_processor.df, r=self.data_processor.circle_radius
|
||||
self.data_processor.df, r=self.data_processor.circle_radius, y=y_col
|
||||
)
|
||||
self.data_processor.df["x"] = self.data_processor.df["circumference_distance"]
|
||||
for gas_name in self.data_processor.gases:
|
||||
@ -304,10 +309,11 @@ class DataProcessor:
|
||||
].std()
|
||||
|
||||
|
||||
def process_main(data_file: Path, config_file: Path) -> None:
|
||||
def process_main(data_file: Path, config_file: Path, task_id: str | None = None) -> None:
|
||||
"""Main function to run the pipeline."""
|
||||
config = load_config(config_file)
|
||||
name = data_file.stem
|
||||
# 优先使用 task_id,否则退回文件 stem
|
||||
name = task_id if task_id else data_file.stem
|
||||
df = read_csv(data_file)
|
||||
|
||||
processor = DataProcessor(config, df)
|
||||
|
||||
@ -66,47 +66,52 @@ def generate_reports(name: str, processor, config: dict):
|
||||
Generates reports, configuration files, and processed output variables for gasflux processing runs.
|
||||
|
||||
Parameters:
|
||||
name (str): The name identifier for the current processing run.
|
||||
name (str): The name identifier for the current processing run (task_id).
|
||||
processor (object): The processing object containing report data and output variables.
|
||||
config (dict): Configuration dictionary used for processing.
|
||||
"""
|
||||
output_dir = Path(config["output_dir"]).expanduser()
|
||||
processing_time = datetime.now()
|
||||
output_path = output_dir / name / processing_time.strftime("%Y-%m-%d_%H-%M-%S-%f_processing_run")
|
||||
# Save directly to outputs/{task_id} directory
|
||||
output_path = output_dir / "outputs" / name
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Save reports
|
||||
for gas, report in processor.reports.items():
|
||||
report_path = output_path / f"{name}_{gas}_report.html"
|
||||
timestamp_str = processing_time.strftime("%Y%m%d_%H%M%S")
|
||||
report_path = output_path / f"{gas}_report_{timestamp_str}.html"
|
||||
with open(report_path, "w", encoding="utf-8") as file:
|
||||
file.write(report)
|
||||
|
||||
# Save config
|
||||
header = f"# Gasflux output config for file {name} from processing run at {processing_time}\n"
|
||||
config_path = output_path / f"{name}_config.yaml"
|
||||
header = f"# Gasflux output config for task {name} from processing run at {processing_time}\n"
|
||||
timestamp_str = processing_time.strftime("%Y%m%d_%H%M%S")
|
||||
config_path = output_path / f"config_{timestamp_str}.yaml"
|
||||
with open(config_path, "w") as file:
|
||||
file.write(header)
|
||||
yaml.safe_dump(config, file)
|
||||
|
||||
# Save DataFrame to CSV
|
||||
# Save DataFrame to Excel
|
||||
if hasattr(processor, 'df') and processor.df is not None:
|
||||
csv_path = output_path / f"{name}_data.csv"
|
||||
processor.df.to_csv(csv_path, index=False)
|
||||
logger.info(f"DataFrame saved to {csv_path}")
|
||||
timestamp_str = processing_time.strftime("%Y%m%d_%H%M%S")
|
||||
excel_path = output_path / f"processed_data_{timestamp_str}.xlsx"
|
||||
processor.df.to_excel(excel_path, index=False, engine='openpyxl')
|
||||
logger.info(f"DataFrame saved to {excel_path}")
|
||||
|
||||
# Save output variables
|
||||
output_vars = processor.output_vars
|
||||
# output_vars = delete_large_arrays(output_vars, threshold_size=50)
|
||||
header = (
|
||||
f"# Gasflux output variables for file {name} from processing run at {processing_time}\n"
|
||||
f"# Gasflux output variables for task {name} from processing run at {processing_time}\n"
|
||||
)
|
||||
filename = output_path / f"{name}_output_vars.json"
|
||||
timestamp_str = processing_time.strftime("%Y%m%d_%H%M%S")
|
||||
filename = output_path / f"output_vars_{timestamp_str}.json"
|
||||
with open(filename, "w") as file:
|
||||
file.write(header)
|
||||
json.dump(
|
||||
output_vars, file, default=lambda item: item.tolist() if isinstance(item, np.ndarray) else item, indent=4
|
||||
)
|
||||
logger.info(f"Processing run saved to {output_path}")
|
||||
logger.info(f"Task {name} results saved to {output_path}")
|
||||
|
||||
|
||||
def delete_large_arrays(output_vars: dict, threshold_size: int) -> dict:
|
||||
|
||||
@ -81,7 +81,7 @@ def main():
|
||||
if args.output:
|
||||
processed_csv = Path(args.output)
|
||||
else:
|
||||
processed_csv = input_path.with_suffix('.processed.csv')
|
||||
processed_csv = input_path.with_suffix('_processed.csv')
|
||||
|
||||
# 确定配置文件
|
||||
if args.config:
|
||||
@ -98,7 +98,7 @@ def main():
|
||||
try:
|
||||
# 第一步:数据预处理
|
||||
print("🔄 第一步:数据预处理...")
|
||||
processed_df = process_file(str(input_path), str(processed_csv))
|
||||
processed_df = process_file(str(input_path), str(processed_csv), str(config_file))
|
||||
print(f"✅ 数据预处理完成,输出文件: {processed_csv}")
|
||||
print()
|
||||
|
||||
|
||||
704
src/gasflux/shared.py
Normal file
704
src/gasflux/shared.py
Normal file
@ -0,0 +1,704 @@
|
||||
"""
|
||||
Shared utilities and constants for GasFlux API.
|
||||
This module contains shared functions and variables to avoid circular imports.
|
||||
"""
|
||||
|
||||
import time
|
||||
import logging
|
||||
from functools import wraps
|
||||
from flask import request, current_app, g
|
||||
from pathlib import Path
|
||||
import threading
|
||||
|
||||
# Task status constants
|
||||
TASK_STATUS_PENDING = "pending"
|
||||
TASK_STATUS_PROCESSING = "processing"
|
||||
TASK_STATUS_COMPLETED = "completed"
|
||||
TASK_STATUS_FAILED = "failed"
|
||||
|
||||
# Global task status storage
|
||||
task_status = {}
|
||||
|
||||
# Task status persistence file override (set by app on startup)
|
||||
_TASK_STATUS_FILE_PATH: Path | None = None
|
||||
_TASK_STATUS_FILE_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def set_task_status_file_path(path: str | Path):
|
||||
"""Set the task status persistence file path (used across threads without Flask app context)."""
|
||||
global _TASK_STATUS_FILE_PATH
|
||||
_TASK_STATUS_FILE_PATH = Path(path)
|
||||
|
||||
|
||||
def _build_simple_downloads_from_results(results: list[dict]) -> dict:
|
||||
"""Build direct download shortcuts for common files (minimal, frontend-friendly)."""
|
||||
downloads: dict = {}
|
||||
|
||||
def set_once(key: str, url: str):
|
||||
if key not in downloads and url:
|
||||
downloads[key] = url
|
||||
|
||||
for item in results or []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
rel_path = item.get('rel_path')
|
||||
if not rel_path:
|
||||
continue
|
||||
|
||||
name_l = (item.get('name') or '').lower()
|
||||
url = f"/download/{rel_path}"
|
||||
|
||||
if name_l.endswith('.xlsx'):
|
||||
set_once('data_xlsx', url)
|
||||
elif name_l.endswith('.xls'):
|
||||
set_once('data_xls', url)
|
||||
elif name_l.endswith('ch4_report.html'):
|
||||
set_once('report_ch4', url)
|
||||
elif name_l.endswith('co2_report.html'):
|
||||
set_once('report_co2', url)
|
||||
elif name_l.endswith(('.yaml', '.yml')):
|
||||
set_once('config', url)
|
||||
elif name_l.endswith('.json') and 'output_vars' in name_l:
|
||||
set_once('metadata', url)
|
||||
elif name_l.endswith('.html'):
|
||||
set_once('report_html', url)
|
||||
|
||||
return downloads
|
||||
|
||||
# File extension constants
|
||||
ALLOWED_DATA_EXTENSIONS = {'xlsx', 'xls'}
|
||||
ALLOWED_CONFIG_EXTENSIONS = {'yaml', 'yml'}
|
||||
|
||||
# Statistics collector
|
||||
class StatisticsCollector:
|
||||
"""Collects and manages API statistics."""
|
||||
|
||||
def __init__(self):
|
||||
self.start_time = time.time()
|
||||
self.stats = {
|
||||
'requests': {
|
||||
'total': 0,
|
||||
'by_method': {},
|
||||
'by_endpoint': {},
|
||||
'by_status': {},
|
||||
'response_times': [],
|
||||
'errors': 0
|
||||
},
|
||||
'tasks': {
|
||||
'total_created': 0,
|
||||
'total_completed': 0,
|
||||
'total_failed': 0,
|
||||
'by_status': {
|
||||
'pending': 0,
|
||||
'processing': 0,
|
||||
'completed': 0,
|
||||
'failed': 0
|
||||
},
|
||||
'processing_times': []
|
||||
},
|
||||
'performance': {
|
||||
'avg_response_time': 0,
|
||||
'max_response_time': 0,
|
||||
'min_response_time': float('inf'),
|
||||
'uptime_seconds': time.time() - self.start_time
|
||||
}
|
||||
}
|
||||
|
||||
def record_request(self, method, endpoint, status_code, response_time):
|
||||
"""Record an API request."""
|
||||
self.stats['requests']['total'] += 1
|
||||
|
||||
# Method stats
|
||||
if method not in self.stats['requests']['by_method']:
|
||||
self.stats['requests']['by_method'][method] = 0
|
||||
self.stats['requests']['by_method'][method] += 1
|
||||
|
||||
# Endpoint stats
|
||||
if endpoint not in self.stats['requests']['by_endpoint']:
|
||||
self.stats['requests']['by_endpoint'][endpoint] = 0
|
||||
self.stats['requests']['by_endpoint'][endpoint] += 1
|
||||
|
||||
# Status stats
|
||||
status_category = str(status_code // 100 * 100) # 200, 400, 500, etc.
|
||||
if status_category not in self.stats['requests']['by_status']:
|
||||
self.stats['requests']['by_status'][status_category] = 0
|
||||
self.stats['requests']['by_status'][status_category] += 1
|
||||
|
||||
# Response time stats
|
||||
self.stats['requests']['response_times'].append(response_time)
|
||||
|
||||
# Keep only last 1000 response times for memory efficiency
|
||||
if len(self.stats['requests']['response_times']) > 1000:
|
||||
self.stats['requests']['response_times'] = self.stats['requests']['response_times'][-1000:]
|
||||
|
||||
# Error tracking
|
||||
if status_code >= 400:
|
||||
self.stats['requests']['errors'] += 1
|
||||
|
||||
# Update performance stats
|
||||
self._update_performance_stats()
|
||||
|
||||
def record_task_status_change(self, old_status, new_status):
|
||||
"""Record task status changes."""
|
||||
if old_status == "unknown": # New task
|
||||
self.stats['tasks']['total_created'] += 1
|
||||
|
||||
if new_status == TASK_STATUS_COMPLETED:
|
||||
self.stats['tasks']['total_completed'] += 1
|
||||
self.stats['tasks']['by_status']['completed'] += 1
|
||||
elif new_status == TASK_STATUS_FAILED:
|
||||
self.stats['tasks']['total_failed'] += 1
|
||||
self.stats['tasks']['by_status']['failed'] += 1
|
||||
elif new_status == TASK_STATUS_PROCESSING:
|
||||
self.stats['tasks']['by_status']['processing'] += 1
|
||||
elif new_status == TASK_STATUS_PENDING:
|
||||
self.stats['tasks']['by_status']['pending'] += 1
|
||||
|
||||
def record_task_completion_time(self, completion_time):
|
||||
"""Record task completion time."""
|
||||
self.stats['tasks']['processing_times'].append(completion_time)
|
||||
|
||||
def reset_stats(self):
|
||||
"""Reset all statistics."""
|
||||
current_time = time.time()
|
||||
self.start_time = current_time
|
||||
self.stats = {
|
||||
'requests': {
|
||||
'total': 0,
|
||||
'by_method': {},
|
||||
'by_endpoint': {},
|
||||
'by_status': {},
|
||||
'response_times': [],
|
||||
'errors': 0
|
||||
},
|
||||
'tasks': {
|
||||
'total_created': 0,
|
||||
'total_completed': 0,
|
||||
'total_failed': 0,
|
||||
'by_status': {
|
||||
'pending': 0,
|
||||
'processing': 0,
|
||||
'completed': 0,
|
||||
'failed': 0
|
||||
},
|
||||
'processing_times': []
|
||||
},
|
||||
'performance': {
|
||||
'avg_response_time': 0,
|
||||
'max_response_time': 0,
|
||||
'min_response_time': float('inf'),
|
||||
'uptime_seconds': current_time - self.start_time
|
||||
}
|
||||
}
|
||||
|
||||
def _update_performance_stats(self):
|
||||
"""Update performance statistics."""
|
||||
response_times = self.stats['requests']['response_times']
|
||||
if response_times:
|
||||
self.stats['performance']['avg_response_time'] = sum(response_times) / len(response_times)
|
||||
self.stats['performance']['max_response_time'] = max(response_times)
|
||||
self.stats['performance']['min_response_time'] = min(response_times)
|
||||
|
||||
self.stats['performance']['uptime_seconds'] = time.time() - self.start_time
|
||||
|
||||
def get_summary(self):
|
||||
"""Get a summary of current statistics."""
|
||||
current_time = time.time()
|
||||
uptime = current_time - self.start_time
|
||||
|
||||
# Calculate rates
|
||||
requests_per_second = self.stats['requests']['total'] / max(uptime, 1)
|
||||
error_rate = (self.stats['requests']['errors'] / max(self.stats['requests']['total'], 1)) * 100
|
||||
|
||||
# Task completion rate
|
||||
total_tasks_processed = self.stats['tasks']['total_completed'] + self.stats['tasks']['total_failed']
|
||||
task_success_rate = (self.stats['tasks']['total_completed'] / max(total_tasks_processed, 1)) * 100
|
||||
|
||||
return {
|
||||
'summary': {
|
||||
'uptime_seconds': uptime,
|
||||
'uptime_formatted': self._format_uptime(uptime),
|
||||
'requests_total': self.stats['requests']['total'],
|
||||
'requests_per_second': round(requests_per_second, 2),
|
||||
'error_rate_percent': round(error_rate, 2),
|
||||
'active_tasks': len([t for t in task_status.values()
|
||||
if t.get('status') in [TASK_STATUS_PENDING, TASK_STATUS_PROCESSING]])
|
||||
},
|
||||
'requests': {
|
||||
'by_method': self.stats['requests']['by_method'],
|
||||
'by_status': self.stats['requests']['by_status'],
|
||||
'top_endpoints': dict(sorted(self.stats['requests']['by_endpoint'].items(),
|
||||
key=lambda x: x[1], reverse=True)[:10])
|
||||
},
|
||||
'tasks': {
|
||||
'total_created': self.stats['tasks']['total_created'],
|
||||
'total_completed': self.stats['tasks']['total_completed'],
|
||||
'total_failed': self.stats['tasks']['total_failed'],
|
||||
'success_rate_percent': round(task_success_rate, 2),
|
||||
'by_status': self.stats['tasks']['by_status']
|
||||
},
|
||||
'performance': {
|
||||
'avg_response_time_ms': round(self.stats['performance']['avg_response_time'] * 1000, 2),
|
||||
'max_response_time_ms': round(self.stats['performance']['max_response_time'] * 1000, 2),
|
||||
'min_response_time_ms': round(self.stats['performance']['min_response_time'] * 1000, 2)
|
||||
if self.stats['performance']['min_response_time'] != float('inf') else 0
|
||||
}
|
||||
}
|
||||
|
||||
def _format_uptime(self, seconds):
|
||||
"""Format uptime in human readable format."""
|
||||
days, remainder = divmod(int(seconds), 86400)
|
||||
hours, remainder = divmod(remainder, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
|
||||
parts = []
|
||||
if days > 0:
|
||||
parts.append(f"{days}d")
|
||||
if hours > 0:
|
||||
parts.append(f"{hours}h")
|
||||
if minutes > 0:
|
||||
parts.append(f"{minutes}m")
|
||||
parts.append(f"{seconds}s")
|
||||
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
# Create global statistics collector instance
|
||||
stats_collector = StatisticsCollector()
|
||||
|
||||
|
||||
# Shared utility functions
|
||||
def log_performance(func):
|
||||
"""Decorator to log function performance."""
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
start_time = time.time()
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
duration = time.time() - start_time
|
||||
logger.debug(f"PERF: {func.__name__} completed in {duration:.3f}s")
|
||||
return result
|
||||
except Exception as e:
|
||||
duration = time.time() - start_time
|
||||
logger.error(f"PERF: {func.__name__} failed after {duration:.3f}s - Error: {str(e)}")
|
||||
raise
|
||||
return wrapper
|
||||
|
||||
|
||||
def log_request_info():
|
||||
"""Log incoming request information."""
|
||||
if hasattr(request, 'remote_addr'):
|
||||
logger.info(f"REQUEST: {request.method} {request.url} - IP: {request.remote_addr} - User-Agent: {request.headers.get('User-Agent', 'Unknown')}")
|
||||
|
||||
|
||||
def log_response_info(response):
|
||||
"""Log outgoing response information."""
|
||||
if hasattr(request, 'url_rule') and request.url_rule:
|
||||
endpoint = request.url_rule.rule
|
||||
else:
|
||||
endpoint = request.path
|
||||
|
||||
start_time = getattr(g, 'start_time', None)
|
||||
if start_time:
|
||||
duration = time.time() - start_time
|
||||
|
||||
logger.info(f"RESPONSE: {request.method} {endpoint} - Status: {response.status_code} - Duration: {duration:.3f}s" if duration else f"RESPONSE: {request.method} {endpoint} - Status: {response.status_code}")
|
||||
|
||||
|
||||
def update_task_status(task_id, status, message=None, results=None, error=None):
|
||||
"""Update task status in the global dictionary."""
|
||||
timestamp = time.time()
|
||||
old_status = task_status.get(task_id, {}).get("status", "unknown")
|
||||
|
||||
task_status[task_id] = {
|
||||
"status": status,
|
||||
"message": message,
|
||||
"results": results,
|
||||
"error": error,
|
||||
"updated_at": timestamp,
|
||||
"created_at": task_status.get(task_id, {}).get("created_at", timestamp)
|
||||
}
|
||||
|
||||
# Record status change in statistics
|
||||
stats_collector.record_task_status_change(old_status, status)
|
||||
|
||||
# Log detailed status change with context
|
||||
log_msg = f"Task {task_id} status changed: {old_status} -> {status}"
|
||||
if message:
|
||||
log_msg += f" | Message: {message}"
|
||||
if results:
|
||||
log_msg += f" | Results count: {len(results) if isinstance(results, list) else 'N/A'}"
|
||||
if error:
|
||||
log_msg += f" | Error: {error}"
|
||||
|
||||
log_level = logging.ERROR if status == TASK_STATUS_FAILED else logging.INFO
|
||||
logger.log(log_level, log_msg)
|
||||
|
||||
# Save task status to file for persistence
|
||||
logger.debug(f"Saving task {task_id} status '{status}' to persistent storage")
|
||||
save_task_status_to_file()
|
||||
|
||||
|
||||
def get_task_status(task_id):
|
||||
"""Get task status from global dictionary."""
|
||||
if task_id in task_status:
|
||||
return task_status[task_id]
|
||||
return {"status": "not_found"}
|
||||
|
||||
|
||||
def cleanup_old_tasks():
|
||||
"""Clean up old completed tasks to prevent memory leak."""
|
||||
current_time = time.time()
|
||||
max_age = 24 * 3600 # 24 hours
|
||||
to_remove = []
|
||||
|
||||
for task_id, task_info in task_status.items():
|
||||
task_age = current_time - task_info.get("updated_at", 0)
|
||||
if task_age > max_age:
|
||||
to_remove.append(task_id)
|
||||
logger.info(f"Task {task_id} scheduled for cleanup (age: {task_age:.1f}s, status: {task_info.get('status')})")
|
||||
|
||||
initial_count = len(task_status)
|
||||
for task_id in to_remove:
|
||||
del task_status[task_id]
|
||||
|
||||
if to_remove:
|
||||
logger.info(f"Cleanup completed: removed {len(to_remove)} tasks, {len(task_status)} tasks remaining")
|
||||
else:
|
||||
logger.debug(f"Cleanup check: no old tasks to remove ({len(task_status)} active tasks)")
|
||||
|
||||
|
||||
def get_task_list(status_filter=None, page=1, page_size=20, sort_by='updated_at', sort_order='desc', cleanup=True):
|
||||
"""
|
||||
Get paginated list of tasks with optional filtering and sorting.
|
||||
|
||||
Args:
|
||||
status_filter (str or list): Filter by task status. Can be single status or list of statuses.
|
||||
page (int): Page number (1-based).
|
||||
page_size (int): Number of tasks per page.
|
||||
sort_by (str): Sort field ('created_at', 'updated_at', 'status').
|
||||
sort_order (str): Sort order ('asc' or 'desc').
|
||||
cleanup (bool): Whether to cleanup old tasks before returning list.
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'tasks': list of task summaries,
|
||||
'total': total number of tasks,
|
||||
'page': current page,
|
||||
'page_size': page size,
|
||||
'total_pages': total pages,
|
||||
'has_next': has next page,
|
||||
'has_prev': has previous page
|
||||
}
|
||||
"""
|
||||
if cleanup:
|
||||
cleanup_old_tasks() # Clean up old tasks before returning list
|
||||
|
||||
# Filter tasks
|
||||
filtered_tasks = []
|
||||
for task_id, task_info in task_status.items():
|
||||
if status_filter:
|
||||
if isinstance(status_filter, str):
|
||||
if task_info.get('status') != status_filter:
|
||||
continue
|
||||
elif isinstance(status_filter, list):
|
||||
if task_info.get('status') not in status_filter:
|
||||
continue
|
||||
filtered_tasks.append((task_id, task_info))
|
||||
|
||||
# Sort tasks
|
||||
def safe_numeric_sort(value):
|
||||
"""Safely convert value to numeric for sorting."""
|
||||
try:
|
||||
if isinstance(value, (int, float)):
|
||||
return value
|
||||
elif isinstance(value, str):
|
||||
# Try to convert string timestamp to float
|
||||
return float(value)
|
||||
else:
|
||||
return 0
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
|
||||
reverse_order = sort_order.lower() == 'desc'
|
||||
if sort_by == 'created_at':
|
||||
filtered_tasks.sort(key=lambda x: safe_numeric_sort(x[1].get('created_at', 0)), reverse=reverse_order)
|
||||
elif sort_by == 'updated_at':
|
||||
filtered_tasks.sort(key=lambda x: safe_numeric_sort(x[1].get('updated_at', 0)), reverse=reverse_order)
|
||||
elif sort_by == 'status':
|
||||
filtered_tasks.sort(key=lambda x: x[1].get('status', ''), reverse=reverse_order)
|
||||
else:
|
||||
# Default sort by updated_at desc
|
||||
filtered_tasks.sort(key=lambda x: safe_numeric_sort(x[1].get('updated_at', 0)), reverse=True)
|
||||
|
||||
# Paginate
|
||||
total_tasks = len(filtered_tasks)
|
||||
total_pages = (total_tasks + page_size - 1) // page_size
|
||||
start_idx = (page - 1) * page_size
|
||||
end_idx = start_idx + page_size
|
||||
|
||||
paginated_tasks = filtered_tasks[start_idx:end_idx]
|
||||
|
||||
# Format task summaries
|
||||
task_summaries = []
|
||||
for task_id, task_info in paginated_tasks:
|
||||
summary = {
|
||||
'task_id': task_id,
|
||||
'status': task_info.get('status'),
|
||||
'message': task_info.get('message'),
|
||||
'created_at': task_info.get('created_at'),
|
||||
'updated_at': task_info.get('updated_at'),
|
||||
'has_results': bool(task_info.get('results')),
|
||||
'has_error': bool(task_info.get('error'))
|
||||
}
|
||||
task_summaries.append(summary)
|
||||
|
||||
return {
|
||||
'tasks': task_summaries,
|
||||
'total': total_tasks,
|
||||
'page': page,
|
||||
'page_size': page_size,
|
||||
'total_pages': total_pages,
|
||||
'has_next': page < total_pages,
|
||||
'has_prev': page > 1
|
||||
}
|
||||
|
||||
|
||||
def get_task_pool_stats():
|
||||
"""
|
||||
Get task pool statistics.
|
||||
|
||||
Returns:
|
||||
dict: Task pool statistics including counts by status.
|
||||
"""
|
||||
# Note: cleanup_old_tasks() is disabled for task pool stats
|
||||
# to preserve historical task data for management purposes
|
||||
# cleanup_old_tasks() # Clean up old tasks before calculating stats
|
||||
|
||||
stats = {
|
||||
'total_tasks': len(task_status),
|
||||
'status_counts': {},
|
||||
'active_tasks': 0,
|
||||
'queued_tasks': 0,
|
||||
'completed_tasks': 0,
|
||||
'failed_tasks': 0
|
||||
}
|
||||
|
||||
for task_info in task_status.values():
|
||||
status = task_info.get('status')
|
||||
if status:
|
||||
stats['status_counts'][status] = stats['status_counts'].get(status, 0) + 1
|
||||
|
||||
if status == TASK_STATUS_PROCESSING:
|
||||
stats['active_tasks'] += 1
|
||||
elif status == TASK_STATUS_PENDING:
|
||||
stats['queued_tasks'] += 1
|
||||
elif status == TASK_STATUS_COMPLETED:
|
||||
stats['completed_tasks'] += 1
|
||||
elif status == TASK_STATUS_FAILED:
|
||||
stats['failed_tasks'] += 1
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def _get_status_file():
|
||||
"""Get the task status file path, preferring OUTPUT_FOLDER for reliability."""
|
||||
if _TASK_STATUS_FILE_PATH is not None:
|
||||
return _TASK_STATUS_FILE_PATH
|
||||
try:
|
||||
# Prefer OUTPUT_FOLDER which is more reliable and writable
|
||||
from flask import current_app
|
||||
output_dir = current_app.config.get('OUTPUT_FOLDER')
|
||||
if output_dir:
|
||||
return Path(output_dir) / "task_status.json"
|
||||
except Exception:
|
||||
pass
|
||||
# Fall back to module directory (for development)
|
||||
return Path(__file__).parent / "task_status.json"
|
||||
|
||||
|
||||
def _to_json_safe(obj):
|
||||
"""Convert numpy types and other non-JSON-serializable objects to JSON-safe types."""
|
||||
# Preserve JSON-native types
|
||||
if obj is None or isinstance(obj, (str, int, float, bool)):
|
||||
return obj
|
||||
# Recursively handle containers
|
||||
if isinstance(obj, list):
|
||||
return [_to_json_safe(v) for v in obj]
|
||||
if isinstance(obj, tuple):
|
||||
return [_to_json_safe(v) for v in obj]
|
||||
if isinstance(obj, dict):
|
||||
return {str(k): _to_json_safe(v) for k, v in obj.items()}
|
||||
|
||||
try:
|
||||
import numpy as np
|
||||
if isinstance(obj, (np.integer,)):
|
||||
return int(obj)
|
||||
if isinstance(obj, (np.floating,)):
|
||||
return float(obj)
|
||||
if isinstance(obj, (np.bool_,)):
|
||||
return bool(obj)
|
||||
if isinstance(obj, (np.ndarray,)):
|
||||
return obj.tolist()
|
||||
except Exception:
|
||||
pass
|
||||
# Non-builtin/unknown types, fall back to string representation
|
||||
try:
|
||||
return str(obj)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def save_task_status_to_file():
|
||||
"""Save current task status to JSON file for persistence."""
|
||||
try:
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
status_file = _get_status_file()
|
||||
logger.debug(f"Attempting to save {len(task_status)} task statuses to {status_file}")
|
||||
|
||||
# Guard: 避免用空内存覆盖已有文件
|
||||
if not task_status and status_file.exists():
|
||||
logger.info("Skipping task status save: empty in-memory status and file already exists")
|
||||
return
|
||||
|
||||
# Ensure only one thread writes the status file at a time (prevents Windows replace/lock issues)
|
||||
with _TASK_STATUS_FILE_LOCK:
|
||||
|
||||
status_to_save = {}
|
||||
for task_id, task_info in task_status.items():
|
||||
try:
|
||||
clean_info = {}
|
||||
for k, v in task_info.items():
|
||||
if k == 'results' and isinstance(v, list):
|
||||
# Keep only metadata for results, remove potentially large data fields
|
||||
cleaned_results = []
|
||||
for item in v:
|
||||
if isinstance(item, dict):
|
||||
cleaned_item = {kk: vv for kk, vv in item.items() if kk not in ['data', 'arrays']}
|
||||
|
||||
# Normalize result metadata for persistence
|
||||
name = cleaned_item.get('name') or ''
|
||||
rel_path = cleaned_item.get('rel_path') or ''
|
||||
|
||||
# Normalize size to int
|
||||
size = cleaned_item.get('size', 0)
|
||||
try:
|
||||
size = int(size)
|
||||
except Exception:
|
||||
size = 0
|
||||
cleaned_item['size'] = size
|
||||
|
||||
# Infer/normalize type if missing or unknown
|
||||
t = cleaned_item.get('type')
|
||||
if (not t) or (t == 'unknown'):
|
||||
t = _get_file_type(name or rel_path)
|
||||
cleaned_item['type'] = t
|
||||
|
||||
# Convert numpy types / other objects to JSON-safe
|
||||
for kk, vv in list(cleaned_item.items()):
|
||||
cleaned_item[kk] = _to_json_safe(vv)
|
||||
|
||||
cleaned_results.append(cleaned_item)
|
||||
|
||||
clean_info['results'] = cleaned_results
|
||||
|
||||
# Slim persistence: store only direct downloads for frontend
|
||||
clean_info['downloads'] = _build_simple_downloads_from_results(cleaned_results)
|
||||
|
||||
# Ensure each result has a download_url (optional but handy)
|
||||
for r in clean_info['results']:
|
||||
if isinstance(r, dict) and r.get('rel_path') and not r.get('download_url'):
|
||||
r['download_url'] = f"/download/{r['rel_path']}"
|
||||
else:
|
||||
clean_info[k] = _to_json_safe(v)
|
||||
|
||||
status_to_save[task_id] = clean_info
|
||||
logger.debug(f"Processed task {task_id} with status {clean_info.get('status')}")
|
||||
|
||||
except Exception as task_e:
|
||||
logger.warning(f"Failed to process task {task_id}: {task_e}")
|
||||
# Skip this task but continue with others
|
||||
continue
|
||||
|
||||
# Ensure the directory exists
|
||||
status_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write to a unique temporary file first, then rename for atomicity with fsync
|
||||
temp_file = status_file.with_suffix(f'.{os.getpid()}.tmp')
|
||||
with open(temp_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(status_to_save, f, indent=2, ensure_ascii=False)
|
||||
f.flush()
|
||||
os.fsync(f.fileno()) # Force write to disk
|
||||
|
||||
# Atomic rename
|
||||
temp_file.replace(status_file)
|
||||
|
||||
logger.info(f"Successfully saved {len(status_to_save)} task statuses to {status_file}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save task status to file: {e}", exc_info=True)
|
||||
|
||||
|
||||
def load_task_status_from_file():
|
||||
"""Load task status from JSON file on startup."""
|
||||
try:
|
||||
import json
|
||||
|
||||
status_file = _get_status_file()
|
||||
|
||||
if not status_file.exists():
|
||||
logger.info("No task status file found, starting with empty task pool")
|
||||
return
|
||||
|
||||
# Ensure no writer thread is updating the file while we read it
|
||||
with _TASK_STATUS_FILE_LOCK:
|
||||
with open(status_file, 'r', encoding='utf-8') as f:
|
||||
loaded_status = json.load(f)
|
||||
|
||||
# Restore task status
|
||||
global task_status
|
||||
task_status.clear()
|
||||
task_status.update(loaded_status)
|
||||
|
||||
logger.info(f"Loaded {len(loaded_status)} task statuses from {status_file}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load task status from file: {e}")
|
||||
|
||||
|
||||
def allowed_file(filename, allowed_extensions):
|
||||
"""Check if file extension is allowed."""
|
||||
return '.' in filename and \
|
||||
filename.rsplit('.', 1)[1].lower() in allowed_extensions
|
||||
|
||||
|
||||
def _format_response(code, message, data=None):
|
||||
"""Format API response in unified format."""
|
||||
response = {
|
||||
"code": code,
|
||||
"message": message,
|
||||
"data": data if data is not None else {}
|
||||
}
|
||||
return response, code
|
||||
|
||||
|
||||
def _get_file_type(filename):
|
||||
"""Determine file type from filename."""
|
||||
fn = (filename or '').lower()
|
||||
if fn.endswith('.html'):
|
||||
return 'report'
|
||||
elif fn.endswith(('.csv', '.xlsx', '.xls')):
|
||||
return 'data'
|
||||
elif fn.endswith('.yaml') or fn.endswith('.yml'):
|
||||
return 'config'
|
||||
elif fn.endswith('.json'):
|
||||
return 'metadata'
|
||||
else:
|
||||
return 'other'
|
||||
|
||||
|
||||
# Initialize logger
|
||||
logger = logging.getLogger('gasflux_api')
|
||||
@ -1,205 +0,0 @@
|
||||
import pandas as pd
|
||||
from src.gasflux import processing
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
from src.gasflux.processing import min_angular_displacement
|
||||
import pytest
|
||||
|
||||
|
||||
testdf = pd.read_csv(Path(__file__).parents[1] / "src" / "gasflux" / "testdata" / "testdata.csv")
|
||||
testconfig = yaml.safe_load(open(Path(__file__).parents[1] / "src" / "gasflux" / "testdata" / "testconfig.yaml"))
|
||||
|
||||
|
||||
def load_cols(cols):
|
||||
return testdf[cols]
|
||||
|
||||
|
||||
def test_min_angular_diff_def():
|
||||
def test_min_angular_displacement():
|
||||
assert min_angular_displacement(10, 350) == 20
|
||||
assert min_angular_displacement(0, 180) == 180
|
||||
x = np.array([10, 0])
|
||||
y = np.array([350, 180])
|
||||
expected = np.array([20, 180])
|
||||
result = min_angular_displacement(x, y)
|
||||
assert np.all(result == expected), "Vectorized function failed"
|
||||
|
||||
|
||||
def test_circ_median():
|
||||
x = np.array([0, 1, 2, 359, 4, 3])
|
||||
median = processing.circ_median(x)
|
||||
assert median == 1.5, "Circular median not calculated correctly"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"plane_angle,expected_winddir_rel,expected_windspeed_normal",
|
||||
[
|
||||
(
|
||||
90,
|
||||
[0, 90, 0, 90, 0],
|
||||
[5, 0, 5, 0, 5],
|
||||
),
|
||||
(
|
||||
30,
|
||||
[60, 30, 60, 30, 60],
|
||||
np.array([1 / 2, np.sqrt(3) / 2, 1 / 2, np.sqrt(3) / 2, 1 / 2]) * 5,
|
||||
),
|
||||
(
|
||||
60,
|
||||
[30, 60, 30, 60, 30],
|
||||
np.array([np.sqrt(3) / 2, 1 / 2, np.sqrt(3) / 2, 1 / 2, np.sqrt(3) / 2]) * 5,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_wind_offset_correction_parametrized(plane_angle, expected_winddir_rel, expected_windspeed_normal):
|
||||
data = {"winddir": [0, 90, 180, 270, 360], "windspeed": [5, 5, 5, 5, 5]}
|
||||
df = pd.DataFrame(data)
|
||||
corrected_df = processing.wind_offset_correction(df, plane_angle)
|
||||
assert "winddir_rel" in corrected_df.columns, f"Relative wind direction column not added for angle {plane_angle}"
|
||||
assert "windspeed" in corrected_df.columns, f"Normalised wind speed column not added for angle {plane_angle}"
|
||||
assert np.allclose(corrected_df["winddir_rel"], expected_winddir_rel, rtol=1e-5, atol=1e-10), (
|
||||
f"Relative wind directions not calculated correctly for angle {plane_angle}"
|
||||
)
|
||||
assert np.allclose(corrected_df["windspeed"], expected_windspeed_normal, rtol=1e-5, atol=1e-10), (
|
||||
f"Normalised wind speeds not calculated correctly for angle {plane_angle}"
|
||||
)
|
||||
|
||||
|
||||
def test_bimodal_azimuth():
|
||||
input_mode = testconfig["transect_azimuth"]
|
||||
input_reciprocal_mode = (input_mode + 180) % 360
|
||||
df = load_cols(["course_azimuth", "height_ato"])
|
||||
mode1, mode2 = processing.bimodal_azimuth(df)
|
||||
assert (
|
||||
min_angular_displacement(mode1, input_mode) < 3 or min_angular_displacement(mode1, input_reciprocal_mode) < 3
|
||||
), "Mode1 does not match expected azimuth or its reciprocal within 3 degrees"
|
||||
|
||||
if min_angular_displacement(mode1, input_mode) < 3:
|
||||
assert min_angular_displacement(mode2, input_reciprocal_mode) < 3, (
|
||||
"Mode2 does not match expected reciprocal azimuth within 3 degrees"
|
||||
)
|
||||
else:
|
||||
assert min_angular_displacement(mode2, input_mode) < 3, "Mode2 does not match expected azimuth within 3 degrees"
|
||||
|
||||
|
||||
def test_bimodal_elevation():
|
||||
df = load_cols(["course_elevation", "height_ato"])
|
||||
input_mode = 0
|
||||
input_reciprocal_mode = 0 - input_mode
|
||||
mode1, mode2 = processing.bimodal_elevation(df)
|
||||
assert (
|
||||
min_angular_displacement(mode1, input_mode) < 3 or min_angular_displacement(mode1, input_reciprocal_mode) < 3
|
||||
), "Mode1 does not match expected elevation or its reciprocal within 3 degrees"
|
||||
if min_angular_displacement(mode1, input_mode) < 3:
|
||||
assert min_angular_displacement(mode2, input_reciprocal_mode) < 3, (
|
||||
"Mode2 does not match expected reciprocal elevation within 3 degrees"
|
||||
)
|
||||
else:
|
||||
assert min_angular_displacement(mode2, input_mode) < 3, (
|
||||
"Mode2 does not match expected elevation within 3 degrees"
|
||||
)
|
||||
|
||||
|
||||
def test_height_transect_splitter():
|
||||
df = load_cols(["height_ato"])
|
||||
df, fig = processing.height_transect_splitter(df)
|
||||
assert "transect_num" in df.columns, "Transect number column not added to dataframe"
|
||||
assert df["transect_num"].nunique() == testconfig["number_of_transects"], (
|
||||
"Dataframe was not split into the right number of transects"
|
||||
)
|
||||
|
||||
|
||||
def test_add_transect_azimuth_switches():
|
||||
df = load_cols(["course_azimuth"])
|
||||
df = processing.add_transect_azimuth_switches(df)
|
||||
assert df["transect_num"].nunique() == testconfig["number_of_transects"], (
|
||||
"Transect azimuth switches not added to dataframe"
|
||||
)
|
||||
|
||||
|
||||
def test_course_filter():
|
||||
df = load_cols(["course_azimuth", "course_elevation", "height_ato"])
|
||||
azimuth_filter = testconfig["filters"]["course_filter"]["azimuth_filter"]
|
||||
azimuth_window = testconfig["filters"]["course_filter"]["azimuth_window"]
|
||||
elevation_filter = testconfig["filters"]["course_filter"]["elevation_filter"]
|
||||
df_filtered, df_unfiltered = processing.course_filter(
|
||||
df, azimuth_filter=azimuth_filter, azimuth_window=azimuth_window, elevation_filter=elevation_filter
|
||||
)
|
||||
input_mode = testconfig["transect_azimuth"]
|
||||
input_reciprocal_mode = (input_mode + 180) % 360
|
||||
# assert that the filtered dataframe contains the expected azimuth or its reciprocal within the window
|
||||
df_filtered["near_mode1"] = df_filtered["rolling_course_azimuth"].apply(
|
||||
lambda x: min_angular_displacement(x, input_mode) < azimuth_window
|
||||
)
|
||||
df_filtered["near_mode2"] = df_filtered["rolling_course_azimuth"].apply(
|
||||
lambda x: min_angular_displacement(x, input_reciprocal_mode) < azimuth_window
|
||||
)
|
||||
assert df_filtered["near_mode1"].any() or df_filtered["near_mode2"].any(), (
|
||||
"Filtered dataframe does not contain expected azimuth or its reciprocal within the window"
|
||||
)
|
||||
|
||||
|
||||
def test_mCount_max():
|
||||
data_dict = {1: -5.4, 2: 0.6, 3: 5.6, 4: 3.2, 5: 10.4, 6: 18.4, 7: 20.8, 8: 19.4}
|
||||
start, end = processing.mCount_max(data_dict)
|
||||
assert start == 4, "Start index of max count not calculated correctly"
|
||||
assert end == 7, "End index of max count not calculated correctly"
|
||||
|
||||
|
||||
def test_largest_monotonic_transect_series():
|
||||
df = load_cols(
|
||||
["timestamp", "height_ato", "course_azimuth", "longitude", "latitude", "utm_easting", "utm_northing"]
|
||||
)
|
||||
df, starttransect, endtransect = processing.largest_monotonic_transect_series(df)
|
||||
starttransect = 1
|
||||
endtransect = testconfig["number_of_transects"]
|
||||
assert starttransect == starttransect, "Start index of largest monotonic transect not calculated correctly"
|
||||
assert endtransect == endtransect, "End index of largest monotonic transect not calculated correctly"
|
||||
|
||||
|
||||
def test_remove_non_transects():
|
||||
df = load_cols(
|
||||
["height_ato", "course_azimuth", "course_elevation", "longitude", "latitude", "utm_easting", "utm_northing"]
|
||||
)
|
||||
retained_df, removed_df = processing.remove_non_transects(df)
|
||||
assert retained_df is not None, "Retained dataframe is None"
|
||||
assert removed_df is not None, "Removed dataframe is None"
|
||||
|
||||
|
||||
def test_flatten_linear_plane():
|
||||
df = load_cols(["height_ato", "utm_easting", "utm_northing"])
|
||||
df, plane_angle = processing.flatten_linear_plane(df)
|
||||
input_plane_angle = testconfig["transect_azimuth"]
|
||||
reciprocal_plane_angle = (input_plane_angle + 180) % 360
|
||||
assert (
|
||||
min_angular_displacement(plane_angle, input_plane_angle) < 3
|
||||
or min_angular_displacement(plane_angle, reciprocal_plane_angle) < 3
|
||||
), "Plane angle not calculated correctly"
|
||||
|
||||
|
||||
def test_drone_anemo_to_point_wind():
|
||||
data = {
|
||||
"yaw": [0, 90, 0, -90, 180],
|
||||
"anemo_u": [0, 0, 10, 10, 10],
|
||||
"anemo_v": [0, 0, 0, 0, 0],
|
||||
"easting": [0, 10, 0, 10, 0],
|
||||
"northing": [0, 0, 0, 0, 10],
|
||||
}
|
||||
df_test = pd.DataFrame(data)
|
||||
yaw_col = "yaw"
|
||||
anemo_u_col = "anemo_u"
|
||||
anemo_v_col = "anemo_v"
|
||||
easting_col = "easting"
|
||||
northing_col = "northing"
|
||||
result_df = processing.drone_anemo_to_point_wind(
|
||||
df_test, yaw_col, anemo_u_col, anemo_v_col, easting_col, northing_col
|
||||
)
|
||||
expected_windspeed = np.array([0, 10, 10, np.sqrt(200), np.sqrt(200)])
|
||||
expected_winddir = np.array(
|
||||
[180, 270, 270, 225, 135]
|
||||
) # 180 not zero because of the way IEEE 754 handles floating point numbers
|
||||
windspeed_diff = np.abs(result_df["windspeed"].values - expected_windspeed)
|
||||
winddir_diff = processing.min_angular_displacement(result_df["winddir"].to_numpy(), expected_winddir)
|
||||
assert np.all(windspeed_diff < 1e-10), "Wind speed not calculated correctly"
|
||||
assert np.all(np.array(winddir_diff) < 3), "Wind direction not calculated correctly"
|
||||
@ -1,58 +0,0 @@
|
||||
from src.gasflux import processing_pipelines
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def setup_test_environment(tmp_path):
|
||||
"""Prepare actual test data and configuration with a modified output directory."""
|
||||
df_path = Path(__file__).parents[1] / "src" / "gasflux" / "testdata" / "testdata.csv"
|
||||
config_path = Path(__file__).parents[1] / "src" / "gasflux" / "testdata" / "testconfig.yaml"
|
||||
|
||||
with open(config_path) as f:
|
||||
config = yaml.safe_load(f)
|
||||
config["output_dir"] = str(tmp_path)
|
||||
|
||||
temp_config_path = tmp_path / "temp_testconfig.yaml"
|
||||
with open(temp_config_path, "w") as f:
|
||||
yaml.safe_dump(config, f)
|
||||
|
||||
return df_path, temp_config_path
|
||||
|
||||
|
||||
def test_process_main_config_output(setup_test_environment):
|
||||
df_path, temp_config_path = setup_test_environment
|
||||
processing_pipelines.process_main(df_path, temp_config_path)
|
||||
with open(temp_config_path) as f:
|
||||
temp_config = yaml.safe_load(f)
|
||||
output_dir = Path(temp_config["output_dir"]) / df_path.stem
|
||||
assert output_dir.exists(), "Output directory does not exist."
|
||||
processing_run_dirs = [d for d in output_dir.iterdir() if d.is_dir()]
|
||||
assert len(processing_run_dirs) > 0, "No processing run directory found."
|
||||
processing_run_dir = processing_run_dirs[0]
|
||||
|
||||
with open(temp_config_path) as f:
|
||||
original_config = yaml.safe_load(f)
|
||||
|
||||
for gas in original_config.get("gases", []):
|
||||
report_path = processing_run_dir / f"{df_path.stem}_{gas}_report.html"
|
||||
assert report_path.exists(), f"Report for {gas} does not exist."
|
||||
|
||||
config_dump_path = processing_run_dir / f"{df_path.stem}_config.yaml"
|
||||
assert config_dump_path.exists(), "Config dump file does not exist."
|
||||
|
||||
def load_and_redump(yaml_file):
|
||||
with open(yaml_file) as file:
|
||||
data = yaml.safe_load(file)
|
||||
redumped_data = yaml.dump(data, sort_keys=True, default_flow_style=False)
|
||||
return redumped_data
|
||||
|
||||
assert load_and_redump(temp_config_path) == load_and_redump(
|
||||
config_dump_path
|
||||
), "The dumped config does not match the input config."
|
||||
|
||||
|
||||
def test_process_main_deterministic_output(setup_test_environment):
|
||||
df_path, temp_config_path = setup_test_environment
|
||||
processing_pipelines.process_main(df_path, temp_config_path)
|
||||
Reference in New Issue
Block a user