cd_flux.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. import os
  2. import json
  3. import tempfile
  4. import shutil
  5. import logging
  6. import sys
  7. from pathlib import Path
  8. from typing import Optional, Dict, Any
  9. from fastapi import APIRouter, File, UploadFile, Form, HTTPException, BackgroundTasks
  10. from fastapi.responses import FileResponse, JSONResponse
  11. from app.services.cd_flux_service import FluxCdVisualizationService
  12. # 固定文件名
  13. DEFAULT_INPUT_MAP_FILENAME = "fluxcd_input_map.jpg"
  14. DEFAULT_INPUT_HIST_FILENAME = "fluxcd_input_histogram.jpg"
  15. DEFAULT_INPUT_CSV_FILENAME = "fluxcd_input.csv"
  16. DEFAULT_OUTPUT_MAP_FILENAME = "fluxcd_output_map.jpg"
  17. DEFAULT_OUTPUT_HIST_FILENAME = "fluxcd_output_histogram.jpg"
  18. DEFAULT_OUTPUT_CSV_FILENAME = "fluxcd_output.csv"
  19. DEFAULT_NET_MAP_FILENAME = "fluxcd_net_map.jpg"
  20. DEFAULT_NET_HIST_FILENAME = "fluxcd_net_histogram.jpg"
  21. DEFAULT_NET_CSV_FILENAME = "fluxcd_net.csv"
  22. LATEST_RESULT_FILENAME = "latest_result.json"
  23. # 配置日志 - 避免重复日志输出
  24. logger = logging.getLogger(__name__)
  25. # 避免重复添加处理器导致重复日志
  26. if not logger.handlers:
  27. logger.setLevel(logging.INFO)
  28. handler = logging.StreamHandler()
  29. formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
  30. handler.setFormatter(formatter)
  31. logger.addHandler(handler)
  32. # 关闭日志传播,避免与父级日志处理器重复输出
  33. logger.propagate = False
  34. router = APIRouter()
  35. def get_base_dir():
  36. """获取基础目录路径(与土地数据处理函数一致)"""
  37. if getattr(sys, 'frozen', False):
  38. # 打包后的可执行文件
  39. return os.path.dirname(sys.executable)
  40. else:
  41. # 脚本运行模式
  42. return os.path.dirname(os.path.abspath(__file__))
  43. def get_static_dir():
  44. """获取静态资源目录"""
  45. base_dir = get_base_dir()
  46. return os.path.join(base_dir, "..", "static", "cd_flux")
  47. STATIC_DIR = get_static_dir() # 静态目录,即static/cd_flux
  48. def get_default_file_path(filename: str) -> str:
  49. """获取默认文件路径"""
  50. static_dir = get_static_dir()
  51. path = os.path.join(static_dir, filename)
  52. return path
  53. @router.post("/calculate",
  54. summary="上传CSV文件更新数据并生成Cd通量可视化结果",
  55. description="上传包含镉通量数据的CSV文件,更新数据库,并生成空间分布图和直方图")
  56. async def update_and_generate_fluxcd_visualization(
  57. background_tasks: BackgroundTasks,
  58. csv_file: UploadFile = File(..., description="CSV文件,包含经纬度和通量数据"),
  59. boundary_shp: Optional[str] = Form(None, description="可选,边界SHP文件路径(如果使用默认则不提供)")
  60. ) -> Dict[str, Any]:
  61. try:
  62. logger.info("开始处理Cd通量数据更新和可视化")
  63. # 创建临时目录保存上传的CSV文件
  64. temp_dir = tempfile.mkdtemp()
  65. temp_csv_path = os.path.join(temp_dir, csv_file.filename)
  66. # 保存上传的文件
  67. with open(temp_csv_path, "wb") as f:
  68. content = await csv_file.read()
  69. f.write(content)
  70. # 初始化服务
  71. service = FluxCdVisualizationService()
  72. # 步骤1: 更新数据
  73. update_result = service.update_from_csv(temp_csv_path)
  74. if not update_result.get("success"):
  75. logger.error(f"更新数据失败: {update_result.get('message')}")
  76. raise HTTPException(status_code=500, detail=update_result.get('message'))
  77. # 步骤2: 生成可视化结果
  78. # 使用静态目录作为输出目录
  79. static_dir = get_static_dir()
  80. os.makedirs(static_dir, exist_ok=True)
  81. # 生成三种通量结果
  82. input_result = service.generate_cd_input_flux_map(
  83. output_dir=static_dir,
  84. boundary_shp=boundary_shp
  85. )
  86. output_result = service.generate_cd_output_flux_map(
  87. output_dir=static_dir,
  88. boundary_shp=boundary_shp
  89. )
  90. net_result = service.generate_cd_net_flux_map(
  91. output_dir=static_dir,
  92. boundary_shp=boundary_shp
  93. )
  94. # 保存最新结果信息
  95. latest_result_path = os.path.join(static_dir, LATEST_RESULT_FILENAME)
  96. with open(latest_result_path, "w", encoding="utf-8") as f:
  97. json.dump({
  98. "input": input_result,
  99. "output": output_result,
  100. "net": net_result
  101. }, f, ensure_ascii=False, indent=2)
  102. return {
  103. "input": input_result,
  104. "output": output_result,
  105. "net": net_result
  106. }
  107. except HTTPException as he:
  108. raise he
  109. except Exception as e:
  110. logger.error(f"处理过程中出错: {str(e)}", exc_info=True)
  111. raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(e)}")
  112. finally:
  113. # 清理临时目录(使用后台任务)
  114. background_tasks.add_task(shutil.rmtree, temp_dir, ignore_errors=True)
  115. @router.get("/input/map",
  116. summary="获取Cd输入通量空间分布图",
  117. description="返回默认的Cd输入通量空间分布图")
  118. async def get_fluxcd_map() -> FileResponse:
  119. try:
  120. # 直接返回静态目录中的默认地图
  121. map_path = get_default_file_path(DEFAULT_INPUT_MAP_FILENAME)
  122. if os.path.exists(map_path):
  123. return FileResponse(map_path, media_type="image/jpeg")
  124. else:
  125. raise HTTPException(status_code=404, detail="默认地图不存在,请先生成")
  126. except Exception as e:
  127. logger.error(f"获取地图失败: {str(e)}")
  128. raise HTTPException(status_code=500, detail=f"获取地图失败: {str(e)}")
  129. @router.get("/output/map",
  130. summary="获取Cd输出通量空间分布图",
  131. description="返回默认的Cd输出通量空间分布图")
  132. async def get_fluxcd_output_map() -> FileResponse:
  133. try:
  134. # 直接返回静态目录中的默认地图
  135. map_path = get_default_file_path(DEFAULT_OUTPUT_MAP_FILENAME)
  136. if os.path.exists(map_path):
  137. return FileResponse(map_path, media_type="image/jpeg")
  138. else:
  139. raise HTTPException(status_code=404, detail="默认输出通量地图不存在,请先生成")
  140. except Exception as e:
  141. logger.error(f"获取输出通量地图失败: {str(e)}")
  142. raise HTTPException(status_code=500, detail=f"获取输出通量地图失败: {str(e)}")
  143. @router.get("/net/map",
  144. summary="获取Cd净通量空间分布图",
  145. description="返回默认的Cd净通量空间分布图")
  146. async def get_fluxcd_net_map() -> FileResponse:
  147. try:
  148. # 直接返回静态目录中的默认地图
  149. map_path = get_default_file_path(DEFAULT_NET_MAP_FILENAME)
  150. if os.path.exists(map_path):
  151. return FileResponse(map_path, media_type="image/jpeg")
  152. else:
  153. raise HTTPException(status_code=404, detail="默认净通量地图不存在,请先生成")
  154. except Exception as e:
  155. logger.error(f"获取净通量地图失败: {str(e)}")
  156. raise HTTPException(status_code=500, detail=f"获取净通量地图失败: {str(e)}")
  157. @router.get("/input/histogram",
  158. summary="获取Cd输入通量直方图",
  159. description="返回默认的Cd输入通量直方图")
  160. async def get_fluxcd_histogram() -> FileResponse:
  161. try:
  162. # 直接返回静态目录中的默认直方图
  163. hist_path = get_default_file_path(DEFAULT_INPUT_HIST_FILENAME)
  164. if os.path.exists(hist_path):
  165. return FileResponse(hist_path, media_type="image/jpeg")
  166. else:
  167. raise HTTPException(status_code=404, detail="默认直方图不存在,请先生成")
  168. except Exception as e:
  169. logger.error(f"获取直方图失败: {str(e)}")
  170. raise HTTPException(status_code=500, detail=f"获取直方图失败: {str(e)}")
  171. @router.get("/output/histogram",
  172. summary="获取Cd输出通量直方图",
  173. description="返回默认的Cd输出通量直方图")
  174. async def get_fluxcd_output_histogram() -> FileResponse:
  175. try:
  176. # 直接返回静态目录中的默认直方图
  177. hist_path = get_default_file_path(DEFAULT_OUTPUT_HIST_FILENAME)
  178. if os.path.exists(hist_path):
  179. return FileResponse(hist_path, media_type="image/jpeg")
  180. else:
  181. raise HTTPException(status_code=404, detail="默认输出通量直方图不存在,请先生成")
  182. except Exception as e:
  183. logger.error(f"获取输出通量直方图失败: {str(e)}")
  184. raise HTTPException(status_code=500, detail=f"获取输出通量直方图失败: {str(e)}")
  185. @router.get("/net/histogram",
  186. summary="获取Cd净通量直方图",
  187. description="返回默认的Cd净通量直方图")
  188. async def get_fluxcd_net_histogram() -> FileResponse:
  189. try:
  190. # 直接返回静态目录中的默认直方图
  191. hist_path = get_default_file_path(DEFAULT_NET_HIST_FILENAME)
  192. if os.path.exists(hist_path):
  193. return FileResponse(hist_path, media_type="image/jpeg")
  194. else:
  195. raise HTTPException(status_code=404, detail="默认净通量直方图不存在,请先生成")
  196. except Exception as e:
  197. logger.error(f"获取净通量直方图失败: {str(e)}")
  198. raise HTTPException(status_code=500, detail=f"获取净通量直方图失败: {str(e)}")
  199. @router.get("/input/statistics",
  200. summary="获取Cd输入通量统计信息",
  201. description="返回最新生成的Cd输入通量统计信息")
  202. async def get_fluxcd_statistics() -> JSONResponse:
  203. try:
  204. latest_result_path = os.path.join(STATIC_DIR, LATEST_RESULT_FILENAME)
  205. if not os.path.exists(latest_result_path):
  206. raise HTTPException(status_code=404, detail="无可用统计信息,请先生成")
  207. with open(latest_result_path, "r", encoding="utf-8") as f:
  208. result = json.load(f)
  209. if not result.get("input") or not result["input"].get("success"):
  210. raise HTTPException(status_code=404, detail="输入通量结果无效")
  211. stats = result["input"].get("data", {}).get("statistics")
  212. if stats:
  213. return JSONResponse(content=stats)
  214. else:
  215. raise HTTPException(status_code=404, detail="统计信息不存在")
  216. except Exception as e:
  217. logger.error(f"获取统计信息失败: {str(e)}")
  218. raise HTTPException(status_code=500, detail=f"获取统计信息失败: {str(e)}")
  219. @router.get("/output/statistics",
  220. summary="获取Cd输出通量统计信息",
  221. description="返回最新生成的Cd输出通量统计信息")
  222. async def get_fluxcd_output_statistics() -> JSONResponse:
  223. try:
  224. latest_result_path = os.path.join(STATIC_DIR, LATEST_RESULT_FILENAME)
  225. if not os.path.exists(latest_result_path):
  226. raise HTTPException(status_code=404, detail="无可用统计信息,请先生成")
  227. with open(latest_result_path, "r", encoding="utf-8") as f:
  228. result = json.load(f)
  229. if not result.get("output") or not result["output"].get("success"):
  230. raise HTTPException(status_code=404, detail="输出通量结果无效")
  231. stats = result["output"].get("data", {}).get("statistics")
  232. if stats:
  233. return JSONResponse(content=stats)
  234. else:
  235. raise HTTPException(status_code=404, detail="输出通量统计信息不存在")
  236. except Exception as e:
  237. logger.error(f"获取输出通量统计信息失败: {str(e)}")
  238. raise HTTPException(status_code=500, detail=f"获取输出通量统计信息失败: {str(e)}")
  239. @router.get("/net/statistics",
  240. summary="获取Cd净通量统计信息",
  241. description="返回最新生成的Cd净通量统计信息")
  242. async def get_fluxcd_net_statistics() -> JSONResponse:
  243. try:
  244. latest_result_path = os.path.join(STATIC_DIR, LATEST_RESULT_FILENAME)
  245. if not os.path.exists(latest_result_path):
  246. raise HTTPException(status_code=404, detail="无可用统计信息,请先生成")
  247. with open(latest_result_path, "r", encoding="utf-8") as f:
  248. result = json.load(f)
  249. if not result.get("net") or not result["net"].get("success"):
  250. raise HTTPException(status_code=404, detail="净通量结果无效")
  251. stats = result["net"].get("data", {}).get("statistics")
  252. if stats:
  253. return JSONResponse(content=stats)
  254. else:
  255. raise HTTPException(status_code=404, detail="净通量统计信息不存在")
  256. except Exception as e:
  257. logger.error(f"获取净通量统计信息失败: {str(e)}")
  258. raise HTTPException(status_code=500, detail=f"获取净通量统计信息失败: {str(e)}")
  259. @router.get("/input/export-csv",
  260. summary="导出fluxcd_input.csv文件",
  261. description="导出默认的fluxcd_input.csv文件")
  262. async def export_fluxcd_csv() -> FileResponse:
  263. try:
  264. # 直接返回静态目录中的CSV文件
  265. csv_path = get_default_file_path(DEFAULT_INPUT_CSV_FILENAME)
  266. if os.path.exists(csv_path):
  267. return FileResponse(
  268. csv_path,
  269. filename="fluxcd_input.csv",
  270. media_type="text/csv"
  271. )
  272. else:
  273. raise HTTPException(status_code=404, detail="CSV文件不存在,请先生成")
  274. except Exception as e:
  275. logger.error(f"导出CSV失败: {str(e)}")
  276. raise HTTPException(status_code=500, detail=f"导出CSV失败: {str(e)}")
  277. @router.get("/output/export-csv",
  278. summary="导出fluxcd_output.csv文件",
  279. description="导出默认的fluxcd_output.csv文件")
  280. async def export_fluxcd_output_csv() -> FileResponse:
  281. try:
  282. # 直接返回静态目录中的CSV文件
  283. csv_path = get_default_file_path(DEFAULT_OUTPUT_CSV_FILENAME)
  284. if os.path.exists(csv_path):
  285. return FileResponse(
  286. csv_path,
  287. filename="fluxcd_output.csv",
  288. media_type="text/csv"
  289. )
  290. else:
  291. raise HTTPException(status_code=404, detail="输出通量CSV文件不存在,请先生成")
  292. except Exception as e:
  293. logger.error(f"导出输出通量CSV失败: {str(e)}")
  294. raise HTTPException(status_code=500, detail=f"导出输出通量CSV失败: {str(e)}")
  295. @router.get("/net/export-csv",
  296. summary="导出fluxcd_net.csv文件",
  297. description="导出默认的fluxcd_net.csv文件")
  298. async def export_fluxcd_net_csv() -> FileResponse:
  299. try:
  300. # 直接返回静态目录中的CSV文件
  301. csv_path = get_default_file_path(DEFAULT_NET_CSV_FILENAME)
  302. if os.path.exists(csv_path):
  303. return FileResponse(
  304. csv_path,
  305. filename="fluxcd_net.csv",
  306. media_type="text/csv"
  307. )
  308. else:
  309. raise HTTPException(status_code=404, detail="净通量CSV文件不存在,请先生成")
  310. except Exception as e:
  311. logger.error(f"导出净通量CSV失败: {str(e)}")
  312. raise HTTPException(status_code=500, detail=f"导出净通量CSV失败: {str(e)}")