123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- from fastapi import APIRouter, Form, HTTPException, BackgroundTasks, Query
- from fastapi.responses import FileResponse, JSONResponse
- import os
- import logging
- import tempfile
- import shutil
- from typing import Dict, Any, Optional, List
- from datetime import datetime
- import json
- from pathlib import Path
- # 导入服务层函数
- from ..services.water_service import (
- process_land_to_visualization,
- get_land_statistics,
- get_base_dir
- )
- # 配置日志 - 避免重复日志输出
- logger = logging.getLogger(__name__)
- # 避免重复添加处理器导致重复日志
- if not logger.handlers:
- logger.setLevel(logging.INFO)
- handler = logging.StreamHandler()
- formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
- handler.setFormatter(formatter)
- logger.addHandler(handler)
- # 关闭日志传播,避免与父级日志处理器重复输出
- logger.propagate = False
- router = APIRouter()
- # 清理临时路径的函数
- def cleanup_temp_path(path: str) -> None:
- """删除临时文件或整个临时目录"""
- try:
- if os.path.exists(path):
- if os.path.isfile(path):
- os.unlink(path)
- elif os.path.isdir(path):
- shutil.rmtree(path)
- except Exception as e:
- logger.warning(f"清理临时路径失败: {path}, 错误: {e}")
- @router.get("/default-map",
- summary="获取默认地图图片",
- description="返回指定土地类型的默认地图图片,支持多个土地类型用下划线连接")
- async def get_default_map(
- land_type: str = Query("水田", description="土地类型,支持单个或多个(用下划线连接),如:'水田'、'旱地_水浇地'")
- ) -> FileResponse:
- """返回默认地图图片"""
- try:
- logger.info(f"获取默认地图: {land_type}")
- # 标准化土地类型顺序以匹配文件名
- from ..services.water_service import standardize_land_types_order
- if '_' in land_type:
- # 多个土地类型,按标准顺序排列
- types_list = land_type.split('_')
- standardized_types = standardize_land_types_order(types_list)
- standardized_land_type = '_'.join(standardized_types)
- else:
- # 单个土地类型
- standardized_land_type = land_type.strip()
- # 获取基础目录
- base_dir = get_base_dir()
- raster_dir = os.path.join(base_dir, "..", "static", "water", "Raster")
- map_path = os.path.join(raster_dir, f"{standardized_land_type}_Cd含量地图.jpg")
- if not os.path.exists(map_path):
- logger.warning(f"默认地图文件不存在: {map_path}")
- raise HTTPException(status_code=404, detail="地图文件不存在")
- return FileResponse(
- path=map_path,
- filename=f"{land_type}_Cd含量地图.jpg",
- media_type="image/jpeg"
- )
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"获取默认地图失败: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=f"获取默认地图失败: {str(e)}"
- )
- @router.get("/default-histogram",
- summary="获取默认直方图",
- description="返回指定土地类型的默认直方图图片,支持多个土地类型用下划线连接")
- async def get_default_histogram(
- land_type: str = Query("水田", description="土地类型,支持单个或多个(用下划线连接),如:'水田'、'旱地_水浇地'")
- ) -> FileResponse:
- """返回默认直方图图片"""
- try:
- logger.info(f"获取默认直方图: {land_type}")
- # 标准化土地类型顺序以匹配文件名
- from ..services.water_service import standardize_land_types_order
- if '_' in land_type:
- # 多个土地类型,按标准顺序排列
- types_list = land_type.split('_')
- standardized_types = standardize_land_types_order(types_list)
- standardized_land_type = '_'.join(standardized_types)
- else:
- # 单个土地类型
- standardized_land_type = land_type.strip()
- # 获取基础目录
- base_dir = get_base_dir()
- raster_dir = os.path.join(base_dir, "..", "static", "water", "Raster")
- hist_path = os.path.join(raster_dir, f"{standardized_land_type}_Cd含量直方图.jpg")
- if not os.path.exists(hist_path):
- logger.warning(f"默认直方图文件不存在: {hist_path}")
- raise HTTPException(status_code=404, detail="直方图文件不存在")
- return FileResponse(
- path=hist_path,
- filename=f"{land_type}_Cd含量直方图.jpg",
- media_type="image/jpeg"
- )
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"获取默认直方图失败: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=f"获取默认直方图失败: {str(e)}"
- )
- @router.post("/calculate",
- summary="重新计算土地数据",
- description="根据输入的土地类型和系数重新计算土地数据,支持多个土地类型选择(复选框),支持通过area和level参数控制地图边界,支持插值控制")
- async def recalculate_land_data(
- background_tasks: BackgroundTasks,
- land_types: List[str] = Form(..., description="土地类型,支持单个或多个选择,如:['水田']、['旱地', '水浇地']"),
- param1: float = Form(711, description="土地类型系数的第一个参数"),
- param2: float = Form(0.524, description="土地类型系数的第二个参数"),
- color_map_name: str = Form("绿-黄-红-紫", description="使用的色彩方案"),
- output_size: int = Form(8, description="输出图片的尺寸"),
- area: Optional[str] = Form(None, description="可选的地区名称,如:'乐昌市',用于动态控制地图边界"),
- level: Optional[str] = Form(None, description="可选的行政层级,必须为: county | city | province"),
- enable_interpolation: Optional[bool] = Form(False, description="是否启用空间插值,默认启用"),
- interpolation_method: Optional[str] = Form("linear", description="插值方法: nearest | linear | cubic"),
- resolution_factor: Optional[float] = Form(4.0, description="分辨率因子,默认4.0,越大分辨率越高"),
- save_csv: Optional[bool] = Form(True, description="是否生成CSV文件,默认生成"),
- cleanup_temp_files: Optional[bool] = Form(True, description="是否清理临时文件,默认清理")
- ) -> Dict[str, Any]:
- """重新计算土地数据并返回结果路径,支持多个土地类型和动态边界控制和插值控制"""
- try:
- # 处理土地类型列表 - 去除空值和空格,并标准化顺序
- from ..services.water_service import standardize_land_types_order
- parsed_land_types = standardize_land_types_order(land_types)
- land_types_str = ', '.join(parsed_land_types)
-
- logger.info(f"重新计算土地数据: {land_types_str} (共{len(parsed_land_types)}种类型)")
- if area and level:
- logger.info(f"使用动态边界: {area} ({level})")
- else:
- logger.info("使用默认边界")
- logger.info(f"插值设置: 启用={enable_interpolation}, 方法={interpolation_method}, 分辨率因子={resolution_factor}")
- # 获取默认目录
- base_dir = get_base_dir()
- raster_dir = os.path.join(base_dir, "..", "static", "water", "Raster")
- # 确保目录存在
- os.makedirs(raster_dir, exist_ok=True)
- # 构建系数参数 - 为所有土地类型应用相同的系数
- coefficient_params = {}
- for land_type in parsed_land_types:
- coefficient_params[land_type] = (param1, param2)
- # 调用统一的处理函数,支持多个土地类型,CSV生成作为可选参数
- results = process_land_to_visualization(
- land_types=parsed_land_types, # 传递解析后的土地类型列表
- coefficient_params=coefficient_params,
- color_map_name=color_map_name,
- output_size=output_size,
- area=area,
- level=level,
- enable_interpolation=enable_interpolation,
- interpolation_method=interpolation_method,
- resolution_factor=resolution_factor,
- save_csv=save_csv, # 将CSV生成选项传递给处理函数
- cleanup_temp_files=cleanup_temp_files # 将清理选项传递给处理函数
- )
- if not results:
- logger.error(f"重新计算土地数据失败: {land_types_str}")
- raise HTTPException(
- status_code=500,
- detail=f"重新计算土地数据失败: {land_types_str}"
- )
- cleaned_csv, shapefile, tif_file, map_output, hist_output, used_coeff = results
-
- # 检查关键输出文件是否存在(只检查最终输出的地图和直方图)
- if not map_output or not hist_output:
- logger.error(f"重新计算土地数据失败,关键输出文件缺失: {land_types_str}")
- logger.error(f"地图: {map_output}, 直方图: {hist_output}")
- raise HTTPException(
- status_code=500,
- detail=f"重新计算土地数据失败: {land_types_str}"
- )
-
- # 验证输出文件实际存在
- if not os.path.exists(map_output) or not os.path.exists(hist_output):
- logger.error(f"生成的输出文件不存在: {land_types_str}")
- logger.error(f"地图文件存在: {os.path.exists(map_output) if map_output else False}")
- logger.error(f"直方图文件存在: {os.path.exists(hist_output) if hist_output else False}")
- raise HTTPException(
- status_code=500,
- detail=f"生成的输出文件不存在: {land_types_str}"
- )
- # 定义默认路径 - 使用组合的土地类型名称
- combined_land_type_name = '_'.join(parsed_land_types)
- default_map_path = os.path.join(raster_dir, f"{combined_land_type_name}_Cd含量地图.jpg")
- default_hist_path = os.path.join(raster_dir, f"{combined_land_type_name}_Cd含量直方图.jpg")
- # 移动文件到默认目录(覆盖旧文件)
- shutil.move(map_output, default_map_path)
- shutil.move(hist_output, default_hist_path)
- return {
- "map_path": default_map_path,
- "histogram_path": default_hist_path,
- "used_coeff": used_coeff
- }
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"重新计算土地数据失败: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=f"重新计算土地数据失败: {str(e)}"
- )
- @router.get("/statistics",
- summary="获取土地类型统计数据",
- description="返回指定土地类型的Cd预测结果统计信息,支持多个土地类型用下划线连接")
- async def get_land_statistics_endpoint(
- land_type: str = Query("水田", description="土地类型,支持单个或多个(用下划线连接),如:'水田'、'旱地_水浇地'")
- ) -> JSONResponse:
- """返回土地类型Cd预测结果的统计信息"""
- try:
- logger.info(f"获取土地类型统计数据: {land_type}")
- # 标准化土地类型顺序以匹配文件名
- from ..services.water_service import standardize_land_types_order
- if '_' in land_type:
- # 多个土地类型,按标准顺序排列
- types_list = land_type.split('_')
- standardized_types = standardize_land_types_order(types_list)
- standardized_land_type = '_'.join(standardized_types)
- else:
- # 单个土地类型
- standardized_land_type = land_type.strip()
- # 调用服务层函数获取统计数据
- stats = get_land_statistics(standardized_land_type)
- if not stats:
- logger.warning(f"未找到{land_type}的土地类型统计数据")
- raise HTTPException(status_code=404, detail="统计数据不存在")
- return JSONResponse(content=stats)
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"获取土地类型统计数据失败: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=f"获取土地类型统计数据失败: {str(e)}"
- )
|