cd_flux.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  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. async def update_and_generate_fluxcd_visualization(
  55. background_tasks: BackgroundTasks,
  56. csv_file: Optional[UploadFile] = File(None),
  57. boundary_shp: Optional[str] = Form(None)
  58. ) -> Dict[str, Any]:
  59. try:
  60. logger.info("开始处理Cd通量数据可视化")
  61. # 初始化服务
  62. service = FluxCdVisualizationService()
  63. static_dir = get_static_dir()
  64. os.makedirs(static_dir, exist_ok=True)
  65. # 如果有上传文件,先更新数据
  66. if csv_file:
  67. logger.info("检测到上传文件,开始更新数据")
  68. temp_dir = tempfile.mkdtemp()
  69. temp_csv_path = os.path.join(temp_dir, csv_file.filename)
  70. with open(temp_csv_path, "wb") as f:
  71. content = await csv_file.read()
  72. f.write(content)
  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. # 清理临时文件
  78. background_tasks.add_task(shutil.rmtree, temp_dir, ignore_errors=True)
  79. # 生成所有结果(直接从数据库获取数据)
  80. input_result = service.generate_cd_input_flux_map(
  81. output_dir=static_dir,
  82. boundary_shp=boundary_shp
  83. )
  84. output_result = service.generate_cd_output_flux_map(
  85. output_dir=static_dir,
  86. boundary_shp=boundary_shp
  87. )
  88. net_result = service.generate_cd_net_flux_map(
  89. output_dir=static_dir,
  90. boundary_shp=boundary_shp
  91. )
  92. end_cd_result = service.generate_end_cd_map(
  93. output_dir=static_dir,
  94. boundary_shp=boundary_shp
  95. )
  96. # 保存最新结果信息
  97. latest_result_path = os.path.join(static_dir, LATEST_RESULT_FILENAME)
  98. with open(latest_result_path, "w", encoding="utf-8") as f:
  99. json.dump({
  100. "input": input_result,
  101. "output": output_result,
  102. "net": net_result,
  103. "end_cd": end_cd_result
  104. }, f, ensure_ascii=False, indent=2)
  105. return {
  106. "input": input_result,
  107. "output": output_result,
  108. "net": net_result,
  109. "end_cd": end_cd_result
  110. }
  111. except HTTPException as he:
  112. raise he
  113. except Exception as e:
  114. logger.error(f"处理过程中出错: {str(e)}", exc_info=True)
  115. raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(e)}")
  116. # 添加常量定义
  117. DEFAULT_END_CD_MAP_FILENAME = "fluxcd_end_cd_map.jpg"
  118. DEFAULT_END_CD_HIST_FILENAME = "fluxcd_end_cd_histogram.jpg"
  119. DEFAULT_END_CD_CSV_FILENAME = "fluxcd_end_cd.csv"
  120. # 添加API路由
  121. @router.get("/end-cd/map",
  122. summary="获取当年Cd浓度空间分布图",
  123. description="返回当年Cd浓度的空间分布图")
  124. async def get_end_cd_map() -> FileResponse:
  125. try:
  126. map_path = get_default_file_path(DEFAULT_END_CD_MAP_FILENAME)
  127. if os.path.exists(map_path):
  128. return FileResponse(map_path, media_type="image/jpeg")
  129. else:
  130. raise HTTPException(status_code=404, detail="当年Cd浓度地图不存在,请先生成")
  131. except Exception as e:
  132. logger.error(f"获取当年Cd浓度地图失败: {str(e)}")
  133. raise HTTPException(status_code=500, detail=f"获取当年Cd浓度地图失败: {str(e)}")
  134. @router.get("/end-cd/histogram",
  135. summary="获取当年Cd浓度直方图",
  136. description="返回当年Cd浓度的直方图")
  137. async def get_end_cd_histogram() -> FileResponse:
  138. try:
  139. hist_path = get_default_file_path(DEFAULT_END_CD_HIST_FILENAME)
  140. if os.path.exists(hist_path):
  141. return FileResponse(hist_path, media_type="image/jpeg")
  142. else:
  143. raise HTTPException(status_code=404, detail="当年Cd浓度直方图不存在,请先生成")
  144. except Exception as e:
  145. logger.error(f"获取当年Cd浓度直方图失败: {str(e)}")
  146. raise HTTPException(status_code=500, detail=f"获取当年Cd浓度直方图失败: {str(e)}")
  147. @router.get("/end-cd/statistics",
  148. summary="获取当年Cd浓度统计信息",
  149. description="返回当年Cd浓度的统计信息")
  150. async def get_end_cd_statistics() -> JSONResponse:
  151. try:
  152. latest_result_path = os.path.join(STATIC_DIR, LATEST_RESULT_FILENAME)
  153. if not os.path.exists(latest_result_path):
  154. raise HTTPException(status_code=404, detail="无可用统计信息,请先生成")
  155. with open(latest_result_path, "r", encoding="utf-8") as f:
  156. result = json.load(f)
  157. if not result.get("end_cd") or not result["end_cd"].get("success"):
  158. raise HTTPException(status_code=404, detail="当年Cd浓度结果无效")
  159. stats = result["end_cd"].get("data", {}).get("statistics")
  160. if stats:
  161. return JSONResponse(content=stats)
  162. else:
  163. raise HTTPException(status_code=404, detail="当年Cd浓度统计信息不存在")
  164. except Exception as e:
  165. logger.error(f"获取当年Cd浓度统计信息失败: {str(e)}")
  166. raise HTTPException(status_code=500, detail=f"获取当年Cd浓度统计信息失败: {str(e)}")
  167. @router.get("/end-cd/export-csv",
  168. summary="导出当年Cd浓度CSV文件",
  169. description="导出当年Cd浓度的CSV文件")
  170. async def export_end_cd_csv() -> FileResponse:
  171. try:
  172. csv_path = get_default_file_path(DEFAULT_END_CD_CSV_FILENAME)
  173. if os.path.exists(csv_path):
  174. return FileResponse(
  175. csv_path,
  176. filename="fluxcd_end_cd.csv",
  177. media_type="text/csv"
  178. )
  179. else:
  180. raise HTTPException(status_code=404, detail="当年Cd浓度CSV文件不存在,请先生成")
  181. except Exception as e:
  182. logger.error(f"导出当年Cd浓度CSV失败: {str(e)}")
  183. raise HTTPException(status_code=500, detail=f"导出当年Cd浓度CSV失败: {str(e)}")
  184. @router.get("/input/map",
  185. summary="获取Cd输入通量空间分布图",
  186. description="返回默认的Cd输入通量空间分布图")
  187. async def get_fluxcd_map() -> FileResponse:
  188. try:
  189. # 直接返回静态目录中的默认地图
  190. map_path = get_default_file_path(DEFAULT_INPUT_MAP_FILENAME)
  191. if os.path.exists(map_path):
  192. return FileResponse(map_path, media_type="image/jpeg")
  193. else:
  194. raise HTTPException(status_code=404, detail="默认地图不存在,请先生成")
  195. except Exception as e:
  196. logger.error(f"获取地图失败: {str(e)}")
  197. raise HTTPException(status_code=500, detail=f"获取地图失败: {str(e)}")
  198. @router.get("/output/map",
  199. summary="获取Cd输出通量空间分布图",
  200. description="返回默认的Cd输出通量空间分布图")
  201. async def get_fluxcd_output_map() -> FileResponse:
  202. try:
  203. # 直接返回静态目录中的默认地图
  204. map_path = get_default_file_path(DEFAULT_OUTPUT_MAP_FILENAME)
  205. if os.path.exists(map_path):
  206. return FileResponse(map_path, media_type="image/jpeg")
  207. else:
  208. raise HTTPException(status_code=404, detail="默认输出通量地图不存在,请先生成")
  209. except Exception as e:
  210. logger.error(f"获取输出通量地图失败: {str(e)}")
  211. raise HTTPException(status_code=500, detail=f"获取输出通量地图失败: {str(e)}")
  212. @router.get("/net/map",
  213. summary="获取Cd净通量空间分布图",
  214. description="返回默认的Cd净通量空间分布图")
  215. async def get_fluxcd_net_map() -> FileResponse:
  216. try:
  217. # 直接返回静态目录中的默认地图
  218. map_path = get_default_file_path(DEFAULT_NET_MAP_FILENAME)
  219. if os.path.exists(map_path):
  220. return FileResponse(map_path, media_type="image/jpeg")
  221. else:
  222. raise HTTPException(status_code=404, detail="默认净通量地图不存在,请先生成")
  223. except Exception as e:
  224. logger.error(f"获取净通量地图失败: {str(e)}")
  225. raise HTTPException(status_code=500, detail=f"获取净通量地图失败: {str(e)}")
  226. @router.get("/input/histogram",
  227. summary="获取Cd输入通量直方图",
  228. description="返回默认的Cd输入通量直方图")
  229. async def get_fluxcd_histogram() -> FileResponse:
  230. try:
  231. # 直接返回静态目录中的默认直方图
  232. hist_path = get_default_file_path(DEFAULT_INPUT_HIST_FILENAME)
  233. if os.path.exists(hist_path):
  234. return FileResponse(hist_path, media_type="image/jpeg")
  235. else:
  236. raise HTTPException(status_code=404, detail="默认直方图不存在,请先生成")
  237. except Exception as e:
  238. logger.error(f"获取直方图失败: {str(e)}")
  239. raise HTTPException(status_code=500, detail=f"获取直方图失败: {str(e)}")
  240. @router.get("/output/histogram",
  241. summary="获取Cd输出通量直方图",
  242. description="返回默认的Cd输出通量直方图")
  243. async def get_fluxcd_output_histogram() -> FileResponse:
  244. try:
  245. # 直接返回静态目录中的默认直方图
  246. hist_path = get_default_file_path(DEFAULT_OUTPUT_HIST_FILENAME)
  247. if os.path.exists(hist_path):
  248. return FileResponse(hist_path, media_type="image/jpeg")
  249. else:
  250. raise HTTPException(status_code=404, detail="默认输出通量直方图不存在,请先生成")
  251. except Exception as e:
  252. logger.error(f"获取输出通量直方图失败: {str(e)}")
  253. raise HTTPException(status_code=500, detail=f"获取输出通量直方图失败: {str(e)}")
  254. @router.get("/net/histogram",
  255. summary="获取Cd净通量直方图",
  256. description="返回默认的Cd净通量直方图")
  257. async def get_fluxcd_net_histogram() -> FileResponse:
  258. try:
  259. # 直接返回静态目录中的默认直方图
  260. hist_path = get_default_file_path(DEFAULT_NET_HIST_FILENAME)
  261. if os.path.exists(hist_path):
  262. return FileResponse(hist_path, media_type="image/jpeg")
  263. else:
  264. raise HTTPException(status_code=404, detail="默认净通量直方图不存在,请先生成")
  265. except Exception as e:
  266. logger.error(f"获取净通量直方图失败: {str(e)}")
  267. raise HTTPException(status_code=500, detail=f"获取净通量直方图失败: {str(e)}")
  268. @router.get("/input/statistics",
  269. summary="获取Cd输入通量统计信息",
  270. description="返回最新生成的Cd输入通量统计信息")
  271. async def get_fluxcd_statistics() -> JSONResponse:
  272. try:
  273. latest_result_path = os.path.join(STATIC_DIR, LATEST_RESULT_FILENAME)
  274. if not os.path.exists(latest_result_path):
  275. raise HTTPException(status_code=404, detail="无可用统计信息,请先生成")
  276. with open(latest_result_path, "r", encoding="utf-8") as f:
  277. result = json.load(f)
  278. if not result.get("input") or not result["input"].get("success"):
  279. raise HTTPException(status_code=404, detail="输入通量结果无效")
  280. stats = result["input"].get("data", {}).get("statistics")
  281. if stats:
  282. return JSONResponse(content=stats)
  283. else:
  284. raise HTTPException(status_code=404, detail="统计信息不存在")
  285. except Exception as e:
  286. logger.error(f"获取统计信息失败: {str(e)}")
  287. raise HTTPException(status_code=500, detail=f"获取统计信息失败: {str(e)}")
  288. @router.get("/output/statistics",
  289. summary="获取Cd输出通量统计信息",
  290. description="返回最新生成的Cd输出通量统计信息")
  291. async def get_fluxcd_output_statistics() -> JSONResponse:
  292. try:
  293. latest_result_path = os.path.join(STATIC_DIR, LATEST_RESULT_FILENAME)
  294. if not os.path.exists(latest_result_path):
  295. raise HTTPException(status_code=404, detail="无可用统计信息,请先生成")
  296. with open(latest_result_path, "r", encoding="utf-8") as f:
  297. result = json.load(f)
  298. if not result.get("output") or not result["output"].get("success"):
  299. raise HTTPException(status_code=404, detail="输出通量结果无效")
  300. stats = result["output"].get("data", {}).get("statistics")
  301. if stats:
  302. return JSONResponse(content=stats)
  303. else:
  304. raise HTTPException(status_code=404, detail="输出通量统计信息不存在")
  305. except Exception as e:
  306. logger.error(f"获取输出通量统计信息失败: {str(e)}")
  307. raise HTTPException(status_code=500, detail=f"获取输出通量统计信息失败: {str(e)}")
  308. @router.get("/net/statistics",
  309. summary="获取Cd净通量统计信息",
  310. description="返回最新生成的Cd净通量统计信息")
  311. async def get_fluxcd_net_statistics() -> JSONResponse:
  312. try:
  313. latest_result_path = os.path.join(STATIC_DIR, LATEST_RESULT_FILENAME)
  314. if not os.path.exists(latest_result_path):
  315. raise HTTPException(status_code=404, detail="无可用统计信息,请先生成")
  316. with open(latest_result_path, "r", encoding="utf-8") as f:
  317. result = json.load(f)
  318. if not result.get("net") or not result["net"].get("success"):
  319. raise HTTPException(status_code=404, detail="净通量结果无效")
  320. stats = result["net"].get("data", {}).get("statistics")
  321. if stats:
  322. return JSONResponse(content=stats)
  323. else:
  324. raise HTTPException(status_code=404, detail="净通量统计信息不存在")
  325. except Exception as e:
  326. logger.error(f"获取净通量统计信息失败: {str(e)}")
  327. raise HTTPException(status_code=500, detail=f"获取净通量统计信息失败: {str(e)}")
  328. @router.get("/input/export-csv",
  329. summary="导出fluxcd_input.csv文件",
  330. description="导出默认的fluxcd_input.csv文件")
  331. async def export_fluxcd_csv() -> FileResponse:
  332. try:
  333. # 直接返回静态目录中的CSV文件
  334. csv_path = get_default_file_path(DEFAULT_INPUT_CSV_FILENAME)
  335. if os.path.exists(csv_path):
  336. return FileResponse(
  337. csv_path,
  338. filename="fluxcd_input.csv",
  339. media_type="text/csv"
  340. )
  341. else:
  342. raise HTTPException(status_code=404, detail="CSV文件不存在,请先生成")
  343. except Exception as e:
  344. logger.error(f"导出CSV失败: {str(e)}")
  345. raise HTTPException(status_code=500, detail=f"导出CSV失败: {str(e)}")
  346. @router.get("/output/export-csv",
  347. summary="导出fluxcd_output.csv文件",
  348. description="导出默认的fluxcd_output.csv文件")
  349. async def export_fluxcd_output_csv() -> FileResponse:
  350. try:
  351. # 直接返回静态目录中的CSV文件
  352. csv_path = get_default_file_path(DEFAULT_OUTPUT_CSV_FILENAME)
  353. if os.path.exists(csv_path):
  354. return FileResponse(
  355. csv_path,
  356. filename="fluxcd_output.csv",
  357. media_type="text/csv"
  358. )
  359. else:
  360. raise HTTPException(status_code=404, detail="输出通量CSV文件不存在,请先生成")
  361. except Exception as e:
  362. logger.error(f"导出输出通量CSV失败: {str(e)}")
  363. raise HTTPException(status_code=500, detail=f"导出输出通量CSV失败: {str(e)}")
  364. @router.get("/net/export-csv",
  365. summary="导出fluxcd_net.csv文件",
  366. description="导出默认的fluxcd_net.csv文件")
  367. async def export_fluxcd_net_csv() -> FileResponse:
  368. try:
  369. # 直接返回静态目录中的CSV文件
  370. csv_path = get_default_file_path(DEFAULT_NET_CSV_FILENAME)
  371. if os.path.exists(csv_path):
  372. return FileResponse(
  373. csv_path,
  374. filename="fluxcd_net.csv",
  375. media_type="text/csv"
  376. )
  377. else:
  378. raise HTTPException(status_code=404, detail="净通量CSV文件不存在,请先生成")
  379. except Exception as e:
  380. logger.error(f"导出净通量CSV失败: {str(e)}")
  381. raise HTTPException(status_code=500, detail=f"导出净通量CSV失败: {str(e)}")