123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713 |
- """
- Cd预测模型API接口
- @description: 提供作物Cd和有效态Cd的预测与可视化功能
- """
- from fastapi import APIRouter, HTTPException, BackgroundTasks, UploadFile, File, Form
- from fastapi.responses import FileResponse
- from typing import Dict, Any, Optional, List
- import os
- import logging
- import io
- import pandas as pd
- from ..services.cd_prediction_service import CdPredictionService
- router = APIRouter()
- # 设置日志
- logger = logging.getLogger(__name__)
- # =============================================================================
- # 县市查询接口
- # =============================================================================
- @router.get("/supported-counties",
- summary="获取支持的县市列表",
- description="获取系统当前支持进行Cd预测的县市列表")
- async def get_supported_counties() -> Dict[str, Any]:
- """
- 获取支持的县市列表
-
- @returns {Dict[str, Any]} 支持的县市信息
- """
- try:
- service = CdPredictionService()
- counties = service.get_supported_counties_info()
-
- return {
- "success": True,
- "message": "获取支持县市列表成功",
- "data": {
- "counties": counties,
- "total_count": len(counties)
- }
- }
-
- except Exception as e:
- logger.error(f"获取支持县市列表失败: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=f"获取支持县市列表失败: {str(e)}"
- )
- # =============================================================================
- # 一键生成并获取地图接口
- # =============================================================================
- @router.post("/crop-cd/generate-and-get-map",
- summary="一键生成并获取作物Cd预测地图",
- description="根据县名和CSV数据生成作物Cd预测地图并直接返回图片文件")
- async def generate_and_get_crop_cd_map(
- county_name: str = Form(..., description="县市名称,如:乐昌市"),
- data_file: UploadFile = File(..., description="CSV格式的环境因子数据文件,前两列为经纬度,后续列与areatest.csv结构一致")
- ):
- """
- 一键生成并获取作物Cd预测地图
-
- @param county_name: 县市名称
- @param data_file: CSV数据文件,前两列为经纬度坐标,后面几列和areatest.csv的结构一致
- @returns {FileResponse} 预测地图文件
- """
- try:
- logger.info(f"开始为{county_name}一键生成作物Cd预测地图")
-
- # 验证文件格式
- if not data_file.filename.endswith('.csv'):
- raise HTTPException(status_code=400, detail="仅支持CSV格式文件")
-
- # 读取CSV数据
- content = await data_file.read()
- df = pd.read_csv(io.StringIO(content.decode('utf-8')))
-
- # 验证数据格式
- if df.shape[1] < 3:
- raise HTTPException(
- status_code=400,
- detail="数据至少需要3列:前两列为经纬度,后续列为环境因子"
- )
-
- # 重命名前两列为标准的经纬度列名
- df.columns = ['longitude', 'latitude'] + list(df.columns[2:])
-
- service = CdPredictionService()
-
- # 验证数据
- validation_result = service.validate_input_data(df, county_name)
- if not validation_result['valid']:
- raise HTTPException(
- status_code=400,
- detail=f"数据验证失败: {', '.join(validation_result['errors'])}"
- )
-
- # 保存临时数据文件
- temp_file_path = service.save_temp_data(df, county_name)
-
- # 生成预测结果
- result = await service.generate_crop_cd_prediction_for_county(
- county_name=county_name,
- data_file=temp_file_path
- )
-
- if not result['map_path'] or not os.path.exists(result['map_path']):
- raise HTTPException(status_code=500, detail="地图文件生成失败")
-
- return FileResponse(
- path=result['map_path'],
- filename=f"{county_name}_crop_cd_prediction_map.jpg",
- media_type="image/jpeg"
- )
-
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"为{county_name}一键生成作物Cd预测地图失败: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=f"为{county_name}一键生成作物Cd预测地图失败: {str(e)}"
- )
- @router.post("/effective-cd/generate-and-get-map",
- summary="一键生成并获取有效态Cd预测地图",
- description="根据县名和CSV数据生成有效态Cd预测地图并直接返回图片文件")
- async def generate_and_get_effective_cd_map(
- county_name: str = Form(..., description="县市名称,如:乐昌市"),
- data_file: UploadFile = File(..., description="CSV格式的环境因子数据文件,前两列为经纬度,后续列与areatest.csv结构一致")
- ):
- """
- 一键生成并获取有效态Cd预测地图
-
- @param county_name: 县市名称
- @param data_file: CSV数据文件,前两列为经纬度坐标,后面几列和areatest.csv的结构一致
- @returns {FileResponse} 预测地图文件
- """
- try:
- logger.info(f"开始为{county_name}一键生成有效态Cd预测地图")
-
- # 验证文件格式
- if not data_file.filename.endswith('.csv'):
- raise HTTPException(status_code=400, detail="仅支持CSV格式文件")
-
- # 读取CSV数据
- content = await data_file.read()
- df = pd.read_csv(io.StringIO(content.decode('utf-8')))
-
- # 验证数据格式
- if df.shape[1] < 3:
- raise HTTPException(
- status_code=400,
- detail="数据至少需要3列:前两列为经纬度,后续列为环境因子"
- )
-
- # 重命名前两列为标准的经纬度列名
- df.columns = ['longitude', 'latitude'] + list(df.columns[2:])
-
- service = CdPredictionService()
-
- # 验证数据
- validation_result = service.validate_input_data(df, county_name)
- if not validation_result['valid']:
- raise HTTPException(
- status_code=400,
- detail=f"数据验证失败: {', '.join(validation_result['errors'])}"
- )
-
- # 保存临时数据文件
- temp_file_path = service.save_temp_data(df, county_name)
-
- # 生成预测结果
- result = await service.generate_effective_cd_prediction_for_county(
- county_name=county_name,
- data_file=temp_file_path
- )
-
- if not result['map_path'] or not os.path.exists(result['map_path']):
- raise HTTPException(status_code=500, detail="地图文件生成失败")
-
- return FileResponse(
- path=result['map_path'],
- filename=f"{county_name}_effective_cd_prediction_map.jpg",
- media_type="image/jpeg"
- )
-
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"为{county_name}一键生成有效态Cd预测地图失败: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=f"为{county_name}一键生成有效态Cd预测地图失败: {str(e)}"
- )
- # =============================================================================
- # 获取最新预测结果接口(无需重新计算)
- # =============================================================================
- @router.get("/crop-cd/latest-map/{county_name}",
- summary="获取作物Cd最新地图",
- description="直接返回指定县市的最新作物Cd预测地图,无需重新计算")
- async def get_latest_crop_cd_map(county_name: str):
- """
- 获取指定县市的最新作物Cd预测地图
-
- @param county_name: 县市名称,如:乐昌市
- @returns {FileResponse} 最新的预测地图文件
- """
- try:
- logger.info(f"获取{county_name}的最新作物Cd预测地图")
-
- service = CdPredictionService()
-
- # 验证县市是否支持
- if not service.is_county_supported(county_name):
- raise HTTPException(
- status_code=404,
- detail=f"不支持的县市: {county_name}"
- )
-
- # 查找最新的地图文件
- map_pattern = f"crop_cd_{county_name}_prediction_map_*.jpg"
- map_files = []
-
- # 在输出目录中查找相关文件
- import glob
- output_dir = service.output_figures_dir
- search_pattern = os.path.join(output_dir, map_pattern)
- map_files = glob.glob(search_pattern)
-
- if not map_files:
- raise HTTPException(
- status_code=404,
- detail=f"未找到{county_name}的作物Cd预测地图,请先执行预测"
- )
-
- # 选择最新的文件(按修改时间排序)
- latest_map = max(map_files, key=os.path.getmtime)
-
- if not os.path.exists(latest_map):
- raise HTTPException(status_code=404, detail="地图文件不存在")
-
- return FileResponse(
- path=latest_map,
- filename=f"{county_name}_latest_crop_cd_map.jpg",
- media_type="image/jpeg"
- )
-
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"获取{county_name}最新作物Cd地图失败: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=f"获取{county_name}最新作物Cd地图失败: {str(e)}"
- )
- @router.get("/effective-cd/latest-map/{county_name}",
- summary="获取有效态Cd最新地图",
- description="直接返回指定县市的最新有效态Cd预测地图,无需重新计算")
- async def get_latest_effective_cd_map(county_name: str):
- """
- 获取指定县市的最新有效态Cd预测地图
-
- @param county_name: 县市名称,如:乐昌市
- @returns {FileResponse} 最新的预测地图文件
- """
- try:
- logger.info(f"获取{county_name}的最新有效态Cd预测地图")
-
- service = CdPredictionService()
-
- # 验证县市是否支持
- if not service.is_county_supported(county_name):
- raise HTTPException(
- status_code=404,
- detail=f"不支持的县市: {county_name}"
- )
-
- # 查找最新的地图文件
- map_pattern = f"effective_cd_{county_name}_prediction_map_*.jpg"
- map_files = []
-
- # 在输出目录中查找相关文件
- import glob
- output_dir = service.output_figures_dir
- search_pattern = os.path.join(output_dir, map_pattern)
- map_files = glob.glob(search_pattern)
-
- if not map_files:
- raise HTTPException(
- status_code=404,
- detail=f"未找到{county_name}的有效态Cd预测地图,请先执行预测"
- )
-
- # 选择最新的文件(按修改时间排序)
- latest_map = max(map_files, key=os.path.getmtime)
-
- if not os.path.exists(latest_map):
- raise HTTPException(status_code=404, detail="地图文件不存在")
-
- return FileResponse(
- path=latest_map,
- filename=f"{county_name}_latest_effective_cd_map.jpg",
- media_type="image/jpeg"
- )
-
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"获取{county_name}最新有效态Cd地图失败: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=f"获取{county_name}最新有效态Cd地图失败: {str(e)}"
- )
- @router.get("/crop-cd/latest-histogram/{county_name}",
- summary="获取作物Cd最新直方图",
- description="直接返回指定县市的最新作物Cd预测直方图,无需重新计算")
- async def get_latest_crop_cd_histogram(county_name: str):
- """
- 获取指定县市的最新作物Cd预测直方图
-
- @param county_name: 县市名称,如:乐昌市
- @returns {FileResponse} 最新的预测直方图文件
- """
- try:
- logger.info(f"获取{county_name}的最新作物Cd预测直方图")
-
- service = CdPredictionService()
-
- # 验证县市是否支持
- if not service.is_county_supported(county_name):
- raise HTTPException(
- status_code=404,
- detail=f"不支持的县市: {county_name}"
- )
-
- # 查找最新的直方图文件
- histogram_pattern = f"crop_cd_{county_name}_prediction_histogram_*.jpg"
- histogram_files = []
-
- # 在输出目录中查找相关文件
- import glob
- output_dir = service.output_figures_dir
- search_pattern = os.path.join(output_dir, histogram_pattern)
- histogram_files = glob.glob(search_pattern)
-
- if not histogram_files:
- raise HTTPException(
- status_code=404,
- detail=f"未找到{county_name}的作物Cd预测直方图,请先执行预测"
- )
-
- # 选择最新的文件(按修改时间排序)
- latest_histogram = max(histogram_files, key=os.path.getmtime)
-
- if not os.path.exists(latest_histogram):
- raise HTTPException(status_code=404, detail="直方图文件不存在")
-
- return FileResponse(
- path=latest_histogram,
- filename=f"{county_name}_latest_crop_cd_histogram.jpg",
- media_type="image/jpeg"
- )
-
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"获取{county_name}最新作物Cd直方图失败: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=f"获取{county_name}最新作物Cd直方图失败: {str(e)}"
- )
- @router.get("/effective-cd/latest-histogram/{county_name}",
- summary="获取有效态Cd最新直方图",
- description="直接返回指定县市的最新有效态Cd预测直方图,无需重新计算")
- async def get_latest_effective_cd_histogram(county_name: str):
- """
- 获取指定县市的最新有效态Cd预测直方图
-
- @param county_name: 县市名称,如:乐昌市
- @returns {FileResponse} 最新的预测直方图文件
- """
- try:
- logger.info(f"获取{county_name}的最新有效态Cd预测直方图")
-
- service = CdPredictionService()
-
- # 验证县市是否支持
- if not service.is_county_supported(county_name):
- raise HTTPException(
- status_code=404,
- detail=f"不支持的县市: {county_name}"
- )
-
- # 查找最新的直方图文件
- histogram_pattern = f"effective_cd_{county_name}_prediction_histogram_*.jpg"
- histogram_files = []
-
- # 在输出目录中查找相关文件
- import glob
- output_dir = service.output_figures_dir
- search_pattern = os.path.join(output_dir, histogram_pattern)
- histogram_files = glob.glob(search_pattern)
-
- if not histogram_files:
- raise HTTPException(
- status_code=404,
- detail=f"未找到{county_name}的有效态Cd预测直方图,请先执行预测"
- )
-
- # 选择最新的文件(按修改时间排序)
- latest_histogram = max(histogram_files, key=os.path.getmtime)
-
- if not os.path.exists(latest_histogram):
- raise HTTPException(status_code=404, detail="直方图文件不存在")
-
- return FileResponse(
- path=latest_histogram,
- filename=f"{county_name}_latest_effective_cd_histogram.jpg",
- media_type="image/jpeg"
- )
-
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"获取{county_name}最新有效态Cd直方图失败: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=f"获取{county_name}最新有效态Cd直方图失败: {str(e)}"
- )
- @router.get("/latest-results/{county_name}",
- summary="获取县市最新预测结果概览",
- description="获取指定县市所有最新预测结果的概览信息")
- async def get_latest_results_overview(county_name: str):
- """
- 获取指定县市的最新预测结果概览
-
- @param county_name: 县市名称,如:乐昌市
- @returns {Dict[str, Any]} 最新预测结果的概览信息
- """
- try:
- logger.info(f"获取{county_name}的最新预测结果概览")
-
- service = CdPredictionService()
-
- # 验证县市是否支持
- if not service.is_county_supported(county_name):
- raise HTTPException(
- status_code=404,
- detail=f"不支持的县市: {county_name}"
- )
-
- import glob
- output_dir = service.output_figures_dir
-
- # 查找所有相关文件
- result_files = {
- "crop_cd_maps": [],
- "crop_cd_histograms": [],
- "effective_cd_maps": [],
- "effective_cd_histograms": []
- }
-
- # 作物Cd地图
- crop_map_pattern = os.path.join(output_dir, f"crop_cd_{county_name}_prediction_map_*.jpg")
- result_files["crop_cd_maps"] = glob.glob(crop_map_pattern)
-
- # 作物Cd直方图
- crop_hist_pattern = os.path.join(output_dir, f"crop_cd_{county_name}_prediction_histogram_*.jpg")
- result_files["crop_cd_histograms"] = glob.glob(crop_hist_pattern)
-
- # 有效态Cd地图
- eff_map_pattern = os.path.join(output_dir, f"effective_cd_{county_name}_prediction_map_*.jpg")
- result_files["effective_cd_maps"] = glob.glob(eff_map_pattern)
-
- # 有效态Cd直方图
- eff_hist_pattern = os.path.join(output_dir, f"effective_cd_{county_name}_prediction_histogram_*.jpg")
- result_files["effective_cd_histograms"] = glob.glob(eff_hist_pattern)
-
- # 构建结果概览
- overview = {
- "county_name": county_name,
- "available_results": {},
- "latest_files": {},
- "total_results": 0
- }
-
- for result_type, files in result_files.items():
- if files:
- # 按修改时间排序,获取最新文件
- latest_file = max(files, key=os.path.getmtime)
- file_stats = os.stat(latest_file)
-
- overview["available_results"][result_type] = {
- "count": len(files),
- "latest_timestamp": file_stats.st_mtime,
- "latest_size": file_stats.st_size
- }
-
- overview["latest_files"][result_type] = {
- "filename": os.path.basename(latest_file),
- "path": latest_file,
- "download_url": f"/api/cd-prediction/{result_type.replace('_', '-')}/latest-{'map' if 'map' in result_type else 'histogram'}/{county_name}"
- }
-
- overview["total_results"] += len(files)
-
- if overview["total_results"] == 0:
- raise HTTPException(
- status_code=404,
- detail=f"未找到{county_name}的任何预测结果,请先执行预测"
- )
-
- return {
- "success": True,
- "message": f"获取{county_name}预测结果概览成功",
- "data": overview
- }
-
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"获取{county_name}预测结果概览失败: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=f"获取{county_name}预测结果概览失败: {str(e)}"
- )
- # =============================================================================
- # 预测结果统计信息接口
- # =============================================================================
- @router.get("/crop-cd/statistics/{county_name}",
- summary="获取作物Cd预测统计信息",
- description="获取指定县市的作物Cd预测结果的详细统计信息")
- async def get_crop_cd_statistics(county_name: str):
- """
- 获取指定县市的作物Cd预测统计信息
-
- @param county_name: 县市名称,如:乐昌市
- @returns {Dict[str, Any]} 预测结果的统计信息
- """
- try:
- logger.info(f"获取{county_name}的作物Cd预测统计信息")
-
- service = CdPredictionService()
-
- # 验证县市是否支持
- if not service.is_county_supported(county_name):
- raise HTTPException(
- status_code=404,
- detail=f"不支持的县市: {county_name}"
- )
-
- # 获取预测结果数据
- stats = service.get_crop_cd_statistics(county_name)
-
- if not stats:
- raise HTTPException(
- status_code=404,
- detail=f"未找到{county_name}的作物Cd预测结果,请先执行预测"
- )
-
- return {
- "success": True,
- "message": f"获取{county_name}作物Cd预测统计信息成功",
- "data": stats
- }
-
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"获取{county_name}作物Cd预测统计信息失败: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=f"获取{county_name}作物Cd预测统计信息失败: {str(e)}"
- )
- @router.get("/effective-cd/statistics/{county_name}",
- summary="获取有效态Cd预测统计信息",
- description="获取指定县市的有效态Cd预测结果的详细统计信息")
- async def get_effective_cd_statistics(county_name: str):
- """
- 获取指定县市的有效态Cd预测统计信息
-
- @param county_name: 县市名称,如:乐昌市
- @returns {Dict[str, Any]} 预测结果的统计信息
- """
- try:
- logger.info(f"获取{county_name}的有效态Cd预测统计信息")
-
- service = CdPredictionService()
-
- # 验证县市是否支持
- if not service.is_county_supported(county_name):
- raise HTTPException(
- status_code=404,
- detail=f"不支持的县市: {county_name}"
- )
-
- # 获取预测结果数据
- stats = service.get_effective_cd_statistics(county_name)
-
- if not stats:
- raise HTTPException(
- status_code=404,
- detail=f"未找到{county_name}的有效态Cd预测结果,请先执行预测"
- )
-
- return {
- "success": True,
- "message": f"获取{county_name}有效态Cd预测统计信息成功",
- "data": stats
- }
-
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"获取{county_name}有效态Cd预测统计信息失败: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=f"获取{county_name}有效态Cd预测统计信息失败: {str(e)}"
- )
- @router.get("/combined-statistics/{county_name}",
- summary="获取综合预测统计信息",
- description="获取指定县市的作物Cd和有效态Cd预测结果的综合统计信息")
- async def get_combined_statistics(county_name: str):
- """
- 获取指定县市的综合预测统计信息
-
- @param county_name: 县市名称,如:乐昌市
- @returns {Dict[str, Any]} 综合预测结果的统计信息
- """
- try:
- logger.info(f"获取{county_name}的综合预测统计信息")
-
- service = CdPredictionService()
-
- # 验证县市是否支持
- if not service.is_county_supported(county_name):
- raise HTTPException(
- status_code=404,
- detail=f"不支持的县市: {county_name}"
- )
-
- # 获取综合统计信息
- stats = service.get_combined_statistics(county_name)
-
- if not stats:
- raise HTTPException(
- status_code=404,
- detail=f"未找到{county_name}的预测结果,请先执行预测"
- )
-
- return {
- "success": True,
- "message": f"获取{county_name}综合预测统计信息成功",
- "data": stats
- }
-
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"获取{county_name}综合预测统计信息失败: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=f"获取{county_name}综合预测统计信息失败: {str(e)}"
- )
- @router.get("/all-statistics",
- summary="获取所有县市统计概览",
- description="获取所有支持县市的预测结果统计概览")
- async def get_all_statistics():
- """
- 获取所有支持县市的预测结果统计概览
-
- @returns {Dict[str, Any]} 所有县市的统计概览
- """
- try:
- logger.info("获取所有县市统计概览")
-
- service = CdPredictionService()
-
- # 获取所有县市的统计概览
- all_stats = service.get_all_counties_statistics()
-
- return {
- "success": True,
- "message": "获取所有县市统计概览成功",
- "data": all_stats
- }
-
- except Exception as e:
- logger.error(f"获取所有县市统计概览失败: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=f"获取所有县市统计概览失败: {str(e)}"
- )
-
|