water.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. """
  2. 数据匹配接口
  3. @description: 提供耕地数据和采样点数据的匹配与合并功能
  4. """
  5. import os
  6. import shutil
  7. import tempfile
  8. import zipfile
  9. from typing import Any
  10. from fastapi import APIRouter, Form, HTTPException, UploadFile, File, BackgroundTasks
  11. from fastapi.responses import FileResponse
  12. import logging
  13. import geopandas as gpd
  14. import rasterio
  15. from rasterio.mask import mask
  16. import numpy as np
  17. import matplotlib.pyplot as plt
  18. from matplotlib.colors import ListedColormap, BoundaryNorm
  19. import seaborn as sns
  20. import json
  21. import rasterio.plot
  22. from Water.Python_codes import point_match, Figure_raster_mapping, Transfer_csv_to_geotif, Into_Geo
  23. router = APIRouter()
  24. # 配置基础路径
  25. BASE_DATA_DIR = "Water/Raster/" # 根据实际情况修改为您的数据目录
  26. SHP_DIR = os.path.join(BASE_DATA_DIR)
  27. TIF_DIR = os.path.join(BASE_DATA_DIR)
  28. # 设置日志
  29. logger = logging.getLogger(__name__)
  30. async def save_shapefile_zip(upload_file: UploadFile) -> str:
  31. """保存并解压 Shapefile ZIP 文件到临时目录"""
  32. try:
  33. # 创建临时目录
  34. temp_dir = tempfile.mkdtemp()
  35. # 保存 ZIP 文件
  36. zip_path = os.path.join(temp_dir, "shapefile.zip")
  37. with open(zip_path, "wb") as f:
  38. content = await upload_file.read()
  39. f.write(content)
  40. # 解压 ZIP 文件
  41. with zipfile.ZipFile(zip_path, 'r') as zip_ref:
  42. zip_ref.extractall(temp_dir)
  43. # 删除 ZIP 文件
  44. os.remove(zip_path)
  45. return temp_dir
  46. except Exception as e:
  47. logger.error(f"Shapefile ZIP 处理失败: {str(e)}")
  48. raise RuntimeError(f"Shapefile ZIP 处理失败: {str(e)}")
  49. async def save_upload_file(upload_file: UploadFile) -> str:
  50. """保存上传文件到临时位置(通用文件)"""
  51. try:
  52. suffix = os.path.splitext(upload_file.filename)[1]
  53. with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
  54. content = await upload_file.read()
  55. tmp.write(content)
  56. return tmp.name
  57. except Exception as e:
  58. logger.error(f"文件保存失败: {str(e)}")
  59. raise RuntimeError(f"文件保存失败: {str(e)}")
  60. def cleanup_temp_path(path: str) -> None:
  61. """删除临时文件或整个临时目录"""
  62. try:
  63. if os.path.exists(path):
  64. if os.path.isfile(path):
  65. os.unlink(path)
  66. elif os.path.isdir(path):
  67. shutil.rmtree(path)
  68. except Exception as e:
  69. logger.warning(f"清理临时路径失败: {path}, 错误: {e}")
  70. def find_shapefile_in_directory(directory: str) -> str:
  71. """在目录中查找 .shp 文件"""
  72. for file in os.listdir(directory):
  73. if file.endswith('.shp'):
  74. return os.path.join(directory, file)
  75. return None
  76. @router.post("/point-match",
  77. summary="合并耕地数据和采样点数据",
  78. description="根据上传的耕地数据文件和采样点数据文件,合并数据并返回合并后的CSV文件")
  79. async def point_match(
  80. farmland_file: UploadFile = File(..., description="耕地中心点数据文件,CSV格式"),
  81. sample_file: UploadFile = File(..., description="采样点数据文件,CSV格式")
  82. ):
  83. """
  84. 合并耕地数据和采样点数据
  85. @param farmland_file: 耕地中心点数据文件
  86. @param sample_file: 采样点数据文件
  87. @returns {FileResponse} 合并后的数据文件
  88. """
  89. try:
  90. logger.info("开始合并耕地数据和采样点数据")
  91. # 验证文件格式
  92. if not farmland_file.filename.endswith('.csv') or not sample_file.filename.endswith('.csv'):
  93. raise HTTPException(status_code=400, detail="仅支持CSV格式文件")
  94. # 读取CSV数据
  95. farmland_content = await farmland_file.read()
  96. sample_content = await sample_file.read()
  97. farmland_data = point_match.read_csv(farmland_content)
  98. sample_data = point_match.read_csv(sample_content)
  99. if not farmland_data or not sample_data:
  100. raise HTTPException(status_code=400, detail="错误: 缺少必要的数据")
  101. # 合并数据
  102. merged_data = point_match.merge_data(farmland_data, sample_data)
  103. # 保存临时数据文件
  104. temp_file_path = "temp_matched_data.csv"
  105. output_file = point_match.write_csv(merged_data, temp_file_path)
  106. return FileResponse(
  107. path=output_file,
  108. filename="matched_data.csv",
  109. media_type="text/csv"
  110. )
  111. except HTTPException:
  112. raise
  113. except Exception as e:
  114. logger.error(f"合并耕地数据和采样点数据失败: {str(e)}")
  115. raise HTTPException(
  116. status_code=500,
  117. detail=f"合并耕地数据和采样点数据失败: {str(e)}"
  118. )
  119. # =============================================================================
  120. # 一键生成并获取地图接口
  121. # =============================================================================
  122. # 设定一些常用的colormap
  123. colormap1 = ['#FFFECE', '#FFF085', '#FEBA17', '#BE3D2A', '#74512D', '#4E1F00'] # 黄-橙-棕
  124. colormap2 = ['#F6F8D5', '#98D2C0', '#4F959D', '#205781', '#143D60', '#2A3335'] # 蓝色系
  125. colormap3 = ['#FFEFC8', '#F8ED8C', '#D3E671', '#89AC46', '#5F8B4C', '#355F2E'] # 淡黄-草绿
  126. colormap4 = ['#F0F1C5', '#BBD8A3', '#6F826A', '#BF9264', '#735557', '#604652'] # 绿色-棕色
  127. colormap5 = ['#FCFAEE', '#FBF3B9', '#FFDCCC', '#FDB7EA', '#B7B1F2', '#8D77AB'] # 黄-粉-紫
  128. colormap6 = ['#15B392', '#73EC8B', '#FFEB55', '#EE66A6', '#D91656', '#640D5F'] # 绿-黄-红-紫
  129. # 对应 Figure_raster_mapping.py 的 mapping_raster 函数的接口
  130. # 对应 Figure_raster_mapping.py 的 mapping_raster 函数的接口
  131. @router.post("/generate-raster-map",
  132. summary="生成栅格地图",
  133. description="根据矢量数据(ZIP格式)、栅格数据和颜色方案生成栅格地图并返回图片文件")
  134. async def generate_raster_map(
  135. background_tasks: BackgroundTasks,
  136. shp_base_name: str = Form(..., description="矢量数据ZIP文件基本名称,如:'lechang'"),
  137. tif_type: str = Form(..., description="栅格数据类型,如:'水田'、'旱地'或'水浇地'"),
  138. color_map_name: str = Form(..., description="使用的色彩方案,如colormap1"),
  139. title_name: str = Form(..., description="输出数据的图的名称"),
  140. output_size: int = Form(..., description="输出图片的尺寸")
  141. ):
  142. try:
  143. logger.info("开始生成栅格地图")
  144. # 拼接完整文件路径
  145. shp_zip = os.path.join(SHP_DIR, f"{shp_base_name}.zip")
  146. tif_file = os.path.join(TIF_DIR, f"{tif_type}.tif")
  147. logger.info(f"shp_zip路径: {shp_zip}")
  148. logger.info(f"tif_file路径: {tif_file}")
  149. # 获取颜色方案
  150. color_map = {
  151. "colormap1": colormap1,
  152. "colormap2": colormap2,
  153. "colormap3": colormap3,
  154. "colormap4": colormap4,
  155. "colormap5": colormap5,
  156. "colormap6": colormap6
  157. }.get(color_map_name)
  158. if not color_map:
  159. raise HTTPException(status_code=400, detail="无效的颜色方案")
  160. # 检查文件路径是否存在
  161. if not os.path.exists(shp_zip):
  162. raise HTTPException(status_code=400, detail=f"shp_zip文件不存在: {shp_zip}")
  163. if not os.path.exists(tif_file):
  164. raise HTTPException(status_code=400, detail=f"tif_file文件不存在: {tif_file}")
  165. # 创建临时目录用于处理文件
  166. temp_dir = tempfile.mkdtemp()
  167. background_tasks.add_task(cleanup_temp_path, temp_dir)
  168. # 解压 Shapefile ZIP 文件
  169. shp_dir = os.path.join(temp_dir, "shp")
  170. os.makedirs(shp_dir, exist_ok=True)
  171. try:
  172. with zipfile.ZipFile(shp_zip, 'r') as zip_ref:
  173. zip_ref.extractall(shp_dir)
  174. except Exception as e:
  175. logger.error(f"Shapefile ZIP 解压失败: {str(e)}")
  176. raise HTTPException(
  177. status_code=500,
  178. detail=f"Shapefile ZIP 解压失败: {str(e)}"
  179. )
  180. # 在解压目录中查找 .shp 文件
  181. shp_path = find_shapefile_in_directory(shp_dir)
  182. if not shp_path:
  183. raise HTTPException(status_code=400, detail="未找到 .shp 文件")
  184. # 创建输出目录
  185. output_dir = os.path.join(temp_dir, "output")
  186. os.makedirs(output_dir, exist_ok=True)
  187. output_path = os.path.join(output_dir, "raster_map.jpg")
  188. # 生成栅格地图
  189. Figure_raster_mapping.mapping_raster(shp_path, tif_file, color_map, title_name, output_path, output_size)
  190. return FileResponse(
  191. path=output_path,
  192. filename="raster_map.jpg",
  193. media_type="image/jpeg"
  194. )
  195. except HTTPException:
  196. raise
  197. except Exception as e:
  198. logger.error(f"生成栅格地图失败: {str(e)}")
  199. raise HTTPException(
  200. status_code=500,
  201. detail=f"生成栅格地图失败: {str(e)}"
  202. )
  203. # 对应 Transfer_csv_to_geotif.py 的 csv_to_shapefile 和 vector_to_raster 函数的接口
  204. @router.post("/csv-to-geotif",
  205. summary="将CSV文件转换为GeoTIFF文件",
  206. description="根据CSV文件和模板GeoTIFF文件生成新的GeoTIFF文件并返回")
  207. async def csv_to_geotif(
  208. csv_file: UploadFile = File(..., description="CSV文件,第一列为经度,第二列为纬度,第三列为数值"),
  209. template_tif_file: UploadFile = File(..., description="用作模板的GeoTIFF文件"),
  210. field: str = Form(..., description="用于栅格化的属性字段名")
  211. ):
  212. try:
  213. logger.info("开始将CSV文件转换为GeoTIFF文件")
  214. # 保存上传的文件到临时目录
  215. csv_path = f"temp_{csv_file.filename}"
  216. template_tif_path = f"temp_{template_tif_file.filename}"
  217. with open(csv_path, "wb") as f:
  218. f.write(await csv_file.read())
  219. with open(template_tif_path, "wb") as f:
  220. f.write(await template_tif_file.read())
  221. shapefile_output = "temp_shapefile.shp"
  222. Transfer_csv_to_geotif.csv_to_shapefile(csv_path, shapefile_output, os.getcwd())
  223. output_tif = "output_geotif.tif"
  224. Transfer_csv_to_geotif.vector_to_raster(shapefile_output, template_tif_path, output_tif, field)
  225. # 删除临时文件
  226. os.remove(csv_path)
  227. os.remove(template_tif_path)
  228. os.remove(shapefile_output)
  229. return FileResponse(
  230. path=output_tif,
  231. filename="output_geotif.tif",
  232. media_type="image/tiff"
  233. )
  234. except Exception as e:
  235. logger.error(f"将CSV文件转换为GeoTIFF文件失败: {str(e)}")
  236. raise HTTPException(
  237. status_code=500,
  238. detail=f"将CSV文件转换为GeoTIFF文件失败: {str(e)}"
  239. )
  240. # 对应 Figure_raster_mapping.py 的 plot_tif_histogram 函数的接口
  241. @router.post("/generate-tif-histogram",
  242. summary="生成GeoTIFF文件的直方图",
  243. description="根据GeoTIFF文件生成其波段值分布的直方图并返回图片文件")
  244. async def generate_tif_histogram(
  245. tif_type: str = Form(..., description="栅格数据类型,如:'水田'、'旱地'或'水浇地'"),
  246. figsize_width: int = Form(10, description="图像宽度,默认10"),
  247. figsize_height: int = Form(6, description="图像高度,默认6"),
  248. xlabel: str = Form('像元值', description="横坐标标签,默认'像元值'"),
  249. ylabel: str = Form('频率密度', description="纵坐标标签,默认'频率密度'"),
  250. title: str = Form('GeoTIFF 波段值分布图', description="图标题,默认'GeoTIFF 波段值分布图'")
  251. ):
  252. try:
  253. logger.info("开始生成GeoTIFF文件的直方图")
  254. # 拼接完整文件路径
  255. tif_file = os.path.join(TIF_DIR, f"{tif_type}.tif")
  256. logger.info(f"tif_file路径: {tif_file}")
  257. # 检查文件路径是否存在
  258. if not os.path.exists(tif_file):
  259. raise HTTPException(status_code=400, detail=f"tif_file文件不存在: {tif_file}")
  260. # 创建临时目录
  261. temp_dir = tempfile.mkdtemp()
  262. output_path = os.path.join(temp_dir, "histogram.jpg")
  263. # 生成直方图
  264. Figure_raster_mapping.plot_tif_histogram(
  265. tif_file,
  266. figsize=(figsize_width, figsize_height),
  267. xlabel=xlabel,
  268. ylabel=ylabel,
  269. title=title,
  270. save_path=output_path
  271. )
  272. return FileResponse(
  273. path=output_path,
  274. filename="tif_histogram.jpg",
  275. media_type="image/jpeg"
  276. )
  277. except Exception as e:
  278. logger.error(f"生成GeoTIFF文件的直方图失败: {str(e)}")
  279. raise HTTPException(
  280. status_code=500,
  281. detail=f"生成GeoTIFF文件的直方图失败: {str(e)}"
  282. )
  283. # =============================================================================
  284. # TIFF 转 GeoJSON 接口
  285. # =============================================================================
  286. @router.post("/tif-to-geojson",
  287. summary="将 TIFF 文件转换为 GeoJSON 文件",
  288. description="根据上传的 TIFF 文件和指定的输出文件名生成 GeoJSON 文件并返回")
  289. async def tif_to_geojson_api(
  290. output_filename: str = Form(..., description="输出的 GeoJSON 文件名,如:output.geojson"),
  291. input_file: UploadFile = File(..., description="TIFF 格式的栅格数据文件"),
  292. band: int = Form(1, description="要读取的波段,默认为 1"),
  293. mask_value: int = Form(None, description="可选:要忽略的值,如背景值,默认为 None")
  294. ):
  295. """
  296. 将 TIFF 文件转换为 GeoJSON 文件
  297. @param output_filename: 输出的 GeoJSON 文件名
  298. @param input_file: TIFF 数据文件
  299. @param band: 要读取的波段,默认为 1
  300. @param mask_value: 可选:要忽略的值,如背景值,默认为 None
  301. @returns {FileResponse} 转换后的 GeoJSON 文件
  302. """
  303. try:
  304. logger.info(f"开始将 {input_file.filename} 转换为 {output_filename}")
  305. # 验证文件格式
  306. if not input_file.filename.endswith('.tif'):
  307. raise HTTPException(status_code=400, detail="仅支持 TIFF 格式文件")
  308. # 保存临时 TIFF 文件
  309. temp_tif_path = f"temp_{input_file.filename}"
  310. with open(temp_tif_path, "wb") as f:
  311. f.write(await input_file.read())
  312. # 调用 Into_Geo.py 中的函数进行转换
  313. Into_Geo.tif_to_geojson(temp_tif_path, output_filename, band=band, mask_value=mask_value)
  314. # 删除临时 TIFF 文件
  315. os.remove(temp_tif_path)
  316. if not os.path.exists(output_filename):
  317. raise HTTPException(status_code=500, detail="GeoJSON 文件生成失败")
  318. return FileResponse(
  319. path=output_filename,
  320. filename=output_filename,
  321. media_type="application/geo+json"
  322. )
  323. except HTTPException:
  324. raise
  325. except Exception as e:
  326. logger.error(f"将 {input_file.filename} 转换为 {output_filename} 失败: {str(e)}")
  327. raise HTTPException(
  328. status_code=500,
  329. detail=f"将 {input_file.filename} 转换为 {output_filename} 失败: {str(e)}"
  330. )