water.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. from fastapi import APIRouter, Form, HTTPException, BackgroundTasks, Query
  2. from fastapi.responses import FileResponse, JSONResponse
  3. import os
  4. import logging
  5. import tempfile
  6. import shutil
  7. from typing import Dict, Any, Optional, List
  8. from datetime import datetime
  9. import json
  10. from pathlib import Path
  11. # 导入服务层函数
  12. from ..services.water_service import (
  13. process_land_to_visualization,
  14. get_land_statistics,
  15. get_base_dir
  16. )
  17. # 配置日志 - 避免重复日志输出
  18. logger = logging.getLogger(__name__)
  19. # 避免重复添加处理器导致重复日志
  20. if not logger.handlers:
  21. logger.setLevel(logging.INFO)
  22. handler = logging.StreamHandler()
  23. formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
  24. handler.setFormatter(formatter)
  25. logger.addHandler(handler)
  26. # 关闭日志传播,避免与父级日志处理器重复输出
  27. logger.propagate = False
  28. router = APIRouter()
  29. # 清理临时路径的函数
  30. def cleanup_temp_path(path: str) -> None:
  31. """删除临时文件或整个临时目录"""
  32. try:
  33. if os.path.exists(path):
  34. if os.path.isfile(path):
  35. os.unlink(path)
  36. elif os.path.isdir(path):
  37. shutil.rmtree(path)
  38. except Exception as e:
  39. logger.warning(f"清理临时路径失败: {path}, 错误: {e}")
  40. @router.get("/default-map",
  41. summary="获取默认地图图片",
  42. description="返回指定土地类型的默认地图图片,支持多个土地类型用下划线连接")
  43. async def get_default_map(
  44. land_type: str = Query("水田", description="土地类型,支持单个或多个(用下划线连接),如:'水田'、'旱地_水浇地'")
  45. ) -> FileResponse:
  46. """返回默认地图图片"""
  47. try:
  48. logger.info(f"获取默认地图: {land_type}")
  49. # 标准化土地类型顺序以匹配文件名
  50. from ..services.water_service import standardize_land_types_order
  51. if '_' in land_type:
  52. # 多个土地类型,按标准顺序排列
  53. types_list = land_type.split('_')
  54. standardized_types = standardize_land_types_order(types_list)
  55. standardized_land_type = '_'.join(standardized_types)
  56. else:
  57. # 单个土地类型
  58. standardized_land_type = land_type.strip()
  59. # 获取基础目录
  60. base_dir = get_base_dir()
  61. raster_dir = os.path.join(base_dir, "..", "static", "water", "Raster")
  62. map_path = os.path.join(raster_dir, f"{standardized_land_type}_Cd含量地图.jpg")
  63. if not os.path.exists(map_path):
  64. logger.warning(f"默认地图文件不存在: {map_path}")
  65. raise HTTPException(status_code=404, detail="地图文件不存在")
  66. return FileResponse(
  67. path=map_path,
  68. filename=f"{land_type}_Cd含量地图.jpg",
  69. media_type="image/jpeg"
  70. )
  71. except HTTPException:
  72. raise
  73. except Exception as e:
  74. logger.error(f"获取默认地图失败: {str(e)}")
  75. raise HTTPException(
  76. status_code=500,
  77. detail=f"获取默认地图失败: {str(e)}"
  78. )
  79. @router.get("/default-histogram",
  80. summary="获取默认直方图",
  81. description="返回指定土地类型的默认直方图图片,支持多个土地类型用下划线连接")
  82. async def get_default_histogram(
  83. land_type: str = Query("水田", description="土地类型,支持单个或多个(用下划线连接),如:'水田'、'旱地_水浇地'")
  84. ) -> FileResponse:
  85. """返回默认直方图图片"""
  86. try:
  87. logger.info(f"获取默认直方图: {land_type}")
  88. # 标准化土地类型顺序以匹配文件名
  89. from ..services.water_service import standardize_land_types_order
  90. if '_' in land_type:
  91. # 多个土地类型,按标准顺序排列
  92. types_list = land_type.split('_')
  93. standardized_types = standardize_land_types_order(types_list)
  94. standardized_land_type = '_'.join(standardized_types)
  95. else:
  96. # 单个土地类型
  97. standardized_land_type = land_type.strip()
  98. # 获取基础目录
  99. base_dir = get_base_dir()
  100. raster_dir = os.path.join(base_dir, "..", "static", "water", "Raster")
  101. hist_path = os.path.join(raster_dir, f"{standardized_land_type}_Cd含量直方图.jpg")
  102. if not os.path.exists(hist_path):
  103. logger.warning(f"默认直方图文件不存在: {hist_path}")
  104. raise HTTPException(status_code=404, detail="直方图文件不存在")
  105. return FileResponse(
  106. path=hist_path,
  107. filename=f"{land_type}_Cd含量直方图.jpg",
  108. media_type="image/jpeg"
  109. )
  110. except HTTPException:
  111. raise
  112. except Exception as e:
  113. logger.error(f"获取默认直方图失败: {str(e)}")
  114. raise HTTPException(
  115. status_code=500,
  116. detail=f"获取默认直方图失败: {str(e)}"
  117. )
  118. @router.post("/calculate",
  119. summary="重新计算土地数据",
  120. description="根据输入的土地类型和系数重新计算土地数据,支持多个土地类型选择(复选框),支持通过area和level参数控制地图边界,支持插值控制")
  121. async def recalculate_land_data(
  122. background_tasks: BackgroundTasks,
  123. land_types: List[str] = Form(..., description="土地类型,支持单个或多个选择,如:['水田']、['旱地', '水浇地']"),
  124. param1: float = Form(711, description="土地类型系数的第一个参数"),
  125. param2: float = Form(0.524, description="土地类型系数的第二个参数"),
  126. color_map_name: str = Form("绿-黄-红-紫", description="使用的色彩方案"),
  127. output_size: int = Form(8, description="输出图片的尺寸"),
  128. area: Optional[str] = Form(None, description="可选的地区名称,如:'乐昌市',用于动态控制地图边界"),
  129. level: Optional[str] = Form(None, description="可选的行政层级,必须为: county | city | province"),
  130. enable_interpolation: Optional[bool] = Form(False, description="是否启用空间插值,默认启用"),
  131. interpolation_method: Optional[str] = Form("linear", description="插值方法: nearest | linear | cubic"),
  132. resolution_factor: Optional[float] = Form(4.0, description="分辨率因子,默认4.0,越大分辨率越高"),
  133. save_csv: Optional[bool] = Form(True, description="是否生成CSV文件,默认生成"),
  134. cleanup_temp_files: Optional[bool] = Form(True, description="是否清理临时文件,默认清理")
  135. ) -> Dict[str, Any]:
  136. """重新计算土地数据并返回结果路径,支持多个土地类型和动态边界控制和插值控制"""
  137. try:
  138. # 处理土地类型列表 - 去除空值和空格,并标准化顺序
  139. from ..services.water_service import standardize_land_types_order
  140. parsed_land_types = standardize_land_types_order(land_types)
  141. land_types_str = ', '.join(parsed_land_types)
  142. logger.info(f"重新计算土地数据: {land_types_str} (共{len(parsed_land_types)}种类型)")
  143. if area and level:
  144. logger.info(f"使用动态边界: {area} ({level})")
  145. else:
  146. logger.info("使用默认边界")
  147. logger.info(f"插值设置: 启用={enable_interpolation}, 方法={interpolation_method}, 分辨率因子={resolution_factor}")
  148. # 获取默认目录
  149. base_dir = get_base_dir()
  150. raster_dir = os.path.join(base_dir, "..", "static", "water", "Raster")
  151. # 确保目录存在
  152. os.makedirs(raster_dir, exist_ok=True)
  153. # 构建系数参数 - 为所有土地类型应用相同的系数
  154. coefficient_params = {}
  155. for land_type in parsed_land_types:
  156. coefficient_params[land_type] = (param1, param2)
  157. # 调用统一的处理函数,支持多个土地类型,CSV生成作为可选参数
  158. results = process_land_to_visualization(
  159. land_types=parsed_land_types, # 传递解析后的土地类型列表
  160. coefficient_params=coefficient_params,
  161. color_map_name=color_map_name,
  162. output_size=output_size,
  163. area=area,
  164. level=level,
  165. enable_interpolation=enable_interpolation,
  166. interpolation_method=interpolation_method,
  167. resolution_factor=resolution_factor,
  168. save_csv=save_csv, # 将CSV生成选项传递给处理函数
  169. cleanup_temp_files=cleanup_temp_files # 将清理选项传递给处理函数
  170. )
  171. if not results:
  172. logger.error(f"重新计算土地数据失败: {land_types_str}")
  173. raise HTTPException(
  174. status_code=500,
  175. detail=f"重新计算土地数据失败: {land_types_str}"
  176. )
  177. cleaned_csv, shapefile, tif_file, map_output, hist_output, used_coeff = results
  178. # 检查关键输出文件是否存在(只检查最终输出的地图和直方图)
  179. if not map_output or not hist_output:
  180. logger.error(f"重新计算土地数据失败,关键输出文件缺失: {land_types_str}")
  181. logger.error(f"地图: {map_output}, 直方图: {hist_output}")
  182. raise HTTPException(
  183. status_code=500,
  184. detail=f"重新计算土地数据失败: {land_types_str}"
  185. )
  186. # 验证输出文件实际存在
  187. if not os.path.exists(map_output) or not os.path.exists(hist_output):
  188. logger.error(f"生成的输出文件不存在: {land_types_str}")
  189. logger.error(f"地图文件存在: {os.path.exists(map_output) if map_output else False}")
  190. logger.error(f"直方图文件存在: {os.path.exists(hist_output) if hist_output else False}")
  191. raise HTTPException(
  192. status_code=500,
  193. detail=f"生成的输出文件不存在: {land_types_str}"
  194. )
  195. # 定义默认路径 - 使用组合的土地类型名称
  196. combined_land_type_name = '_'.join(parsed_land_types)
  197. default_map_path = os.path.join(raster_dir, f"{combined_land_type_name}_Cd含量地图.jpg")
  198. default_hist_path = os.path.join(raster_dir, f"{combined_land_type_name}_Cd含量直方图.jpg")
  199. # 移动文件到默认目录(覆盖旧文件)
  200. shutil.move(map_output, default_map_path)
  201. shutil.move(hist_output, default_hist_path)
  202. return {
  203. "map_path": default_map_path,
  204. "histogram_path": default_hist_path,
  205. "used_coeff": used_coeff
  206. }
  207. except HTTPException:
  208. raise
  209. except Exception as e:
  210. logger.error(f"重新计算土地数据失败: {str(e)}")
  211. raise HTTPException(
  212. status_code=500,
  213. detail=f"重新计算土地数据失败: {str(e)}"
  214. )
  215. @router.get("/statistics",
  216. summary="获取土地类型统计数据",
  217. description="返回指定土地类型的Cd预测结果统计信息,支持多个土地类型用下划线连接")
  218. async def get_land_statistics_endpoint(
  219. land_type: str = Query("水田", description="土地类型,支持单个或多个(用下划线连接),如:'水田'、'旱地_水浇地'")
  220. ) -> JSONResponse:
  221. """返回土地类型Cd预测结果的统计信息"""
  222. try:
  223. logger.info(f"获取土地类型统计数据: {land_type}")
  224. # 标准化土地类型顺序以匹配文件名
  225. from ..services.water_service import standardize_land_types_order
  226. if '_' in land_type:
  227. # 多个土地类型,按标准顺序排列
  228. types_list = land_type.split('_')
  229. standardized_types = standardize_land_types_order(types_list)
  230. standardized_land_type = '_'.join(standardized_types)
  231. else:
  232. # 单个土地类型
  233. standardized_land_type = land_type.strip()
  234. # 调用服务层函数获取统计数据
  235. stats = get_land_statistics(standardized_land_type)
  236. if not stats:
  237. logger.warning(f"未找到{land_type}的土地类型统计数据")
  238. raise HTTPException(status_code=404, detail="统计数据不存在")
  239. return JSONResponse(content=stats)
  240. except HTTPException:
  241. raise
  242. except Exception as e:
  243. logger.error(f"获取土地类型统计数据失败: {str(e)}")
  244. raise HTTPException(
  245. status_code=500,
  246. detail=f"获取土地类型统计数据失败: {str(e)}"
  247. )