package solaranalyzer import ( "context" "encoding/json" "errors" "fmt" "strings" "time" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" "github.com/samber/lo" "github.com/tidwall/gjson" "github.com/tiger1103/gfast/v3/internal/app/system/dao" "github.com/tiger1103/gfast/v3/internal/app/system/model/do" "github.com/tiger1103/gfast/v3/internal/app/system/model/entity" "github.com/tiger1103/gfast/v3/library/liberr" "github.com/tomchavakis/geojson/geometry" "github.com/tomchavakis/turf-go" ) type AIResult struct { SolarPanels []XYPosition // 光伏板的坐标点 Brackets []XYPosition // 支架的坐标点 Pillars []XYPosition // 立柱的坐标点 Holes []XYPosition // 钻孔的坐标点 } // ToGeoPoints 将坐标转换为经纬度坐标 func (r *AIResult) ToGeoPoints(tifPath string) *AIResult { r.SolarPanels = convertToLongitudeAndLatitude(tifPath, r.SolarPanels) // 光伏板 r.Brackets = convertToLongitudeAndLatitude(tifPath, r.Brackets) // 支架 r.Pillars = convertToLongitudeAndLatitude(tifPath, r.Pillars) // 立柱 r.Holes = convertToLongitudeAndLatitude(tifPath, r.Holes) // 钻孔 return r } // IsCircleContainsPoint 判断圆形是否包含点,返回桩点的主键ID func (r *AIResult) IsCircleContainsPoint(m map[string]map[string]GeoPoint, usePillars string) []int { var ids []int var elements []XYPosition switch usePillars { case drillingHole: elements = r.Holes case pillar: elements = r.Pillars case bracket: elements = r.Brackets } for _, aiPillar := range elements { // AI 识别点 aiPoint := geometry.Point{Lng: aiPillar.X, Lat: aiPillar.Y} // 方阵层 for _, points := range m { // 立柱层 for _, dbPillar := range points { circle := dbPillar.Coordinates.ExpandToCircle() if ok, err := turf.PointInPolygon(aiPoint, circle); ok && err == nil { ids = append(ids, dbPillar.ID) } } } } return ids } // GetFromDB 从数据库中获取光伏板、支架、桩点的数据 func (r *AIResult) GetFromDB(projectID string) ([]Point, map[string]map[string]GeoPoint) { // 光伏板、支架 var solarPanels []Point var pillars map[string]map[string]GeoPoint if len(r.SolarPanels) != 0 { if points, err := getSolarPanelCenters(projectID); err == nil { solarPanels = points } } if len(r.Pillars) != 0 { pillars = getPillars(projectID) } return solarPanels, pillars } // 传入 AI 结果 // 1. 将其周围的组串,全部标记为已完成 // 2. 根据其 【方阵ID】【Type = 13】更新其在 work_status 中的总数 ok // 3. 根据当前的 Name Etc: G1.123.1.1 将 pv_module 中的 G1.123.1 状态修改为已完成 // 4. 记录该数据由 AI 识别。 func (r *AIResult) Run(projectID string, id int) { // 获取光伏板和支架,桩点的数据 panels, brackets := r.GetFromDB(projectID) fmt.Println("(内容)光伏板和支架: ", panels) fmt.Println("(内容)桩点: ", brackets) // 预处理光伏板为多边形 polygons := make([]geometry.Polygon, len(panels)) for i, rect := range panels { coords := make([]geometry.Point, len(rect.Points)) for j, p := range rect.Points { coords[j] = geometry.Point{Lng: p.X, Lat: p.Y} } polygons[i] = geometry.Polygon{Coordinates: []geometry.LineString{{Coordinates: coords}}} } fmt.Println("(内容)预处理光伏板为多边形: ", polygons) fmt.Println("(内容)光伏板坐标后点: ", r.SolarPanels) // 光伏板的主键ID var matchedRectangles []int // 匹配AI识别的光伏板中心点 for _, point := range r.SolarPanels { for i, polygon := range polygons { if ok, err := turf.PointInPolygon(geometry.Point{Lng: point.X, Lat: point.Y}, polygon); err == nil && ok { matchedRectangles = append(matchedRectangles, panels[i].ID) break } } } _, err := dao.ManageTaskResult.Ctx(context.Background()).Insert( lo.Map(matchedRectangles, func(v int, _ int) do.ManageTaskResult { return do.ManageTaskResult{ TaskId: id, PvId: v, } })) liberr.ErrIsNil(context.Background(), err) if len(matchedRectangles) > 0 { // TODO: 去除写入数据库的逻辑 // if err := ProcessMatchedRectangles(matchedRectangles); err != nil { // g.Log("uav").Error(context.Background(), "更新匹配到的光伏板失败: ", err) // } } // 更新钻孔 if len(r.Holes) > 0 { ids := r.IsCircleContainsPoint(brackets, drillingHole) if len(ids) > 0 { if err := processPillarIDs(ids, 12, id); err != nil { g.Log("uav").Error(context.Background(), "更新匹配到的支架失败: ", err) } } } // 更新支架和桩点 if len(r.Pillars) > 0 { ids := r.IsCircleContainsPoint(brackets, pillar) if len(ids) > 0 { if err := processPillarIDs(ids, 13, id); err != nil { g.Log("uav").Error(context.Background(), "更新匹配到的支架失败: ", err) } } } if len(r.Brackets) > 0 { ids := r.IsCircleContainsPoint(brackets, bracket) if len(ids) > 0 { if err := processPillarIDs(ids, 14, id); err != nil { g.Log("uav").Error(context.Background(), "更新匹配到的支架失败: ", err) } } } } // 通过主键ID,可以从 qianqi_guangfuban_ids_zhijia 表中查询到对应的 “方阵ID” 和 "子项目ID" // 将得到的结果分组,第一层为 方阵ID,第二层 G01.12.18,第三层数组,[ G01.12.18.1,G01.12.18.2,G01.12.18.3 ] // 如果第三层中有数据,则从数据库中,向前后查询相邻组串,并将其组串修改为已完成,同时取出第二层和方阵ID,用于更新 pv_module 和 work_status 表中的数据。 func processPillarIDs(ids []int, t, id int) error { return nil // 查询 qianqi_guangfuban_ids_zhijia 表 var zhijias []entity.QianqiGuangfubanIdsZhijia if err := dao.QianqiGuangfubanIdsZhijia.Ctx(context.Background()). Fields("id,fangzhen_id,sub_projectid,name"). WhereIn(dao.QianqiGuangfubanIdsZhijia.Columns().Id, ids). Scan(&zhijias); err != nil { return err } // 第一层方阵ID, 第二层支架名称, 第三层支架信息 realGeoPoint := make(map[string]map[string][]entity.QianqiGuangfubanIdsZhijia) for _, zhijia := range zhijias { fangzhenID := zhijia.FangzhenId // 方阵ID zhijiaName := zhijia.Name[:strings.LastIndex(zhijia.Name, ".")] // 支架名称 if _, ok := realGeoPoint[fangzhenID]; !ok { realGeoPoint[fangzhenID] = make(map[string][]entity.QianqiGuangfubanIdsZhijia) } realGeoPoint[fangzhenID][zhijiaName] = append(realGeoPoint[fangzhenID][zhijiaName], zhijia) } // 查询支架及其相邻的组串 for _, geoPoints := range realGeoPoint { for pointKey, pointList := range geoPoints { if len(pointList) > 1 { firstPoint := pointList[0] simulationID := firstPoint.FangzhenId namePattern := firstPoint.Name[:strings.LastIndex(firstPoint.Name, ".")] + ".%" var updatedPoints []entity.QianqiGuangfubanIdsZhijia if err := dao.QianqiGuangfubanIdsZhijia.Ctx(context.Background()).Where(dao.QianqiGuangfubanIdsZhijia.Columns().FangzhenId, simulationID). Where(dao.QianqiGuangfubanIdsZhijia.Columns().Name+" LIKE ?", namePattern). Scan(&updatedPoints); err == nil && len(updatedPoints) > 0 { realGeoPoint[simulationID][pointKey] = updatedPoints } } } } // 更新数据库状态 for _, geoPoints := range realGeoPoint { for key, pointList := range geoPoints { // 取出所有的ID primaryIDs := lo.Map(pointList, func(v entity.QianqiGuangfubanIdsZhijia, _ int) int { return v.Id }) g.Try(context.Background(), func(ctx context.Context) { // 根据主键修改为已完成 // 修改组串的状态 // G01.12.18.1 -- G01.12.18.2 -- G01.12.18.3 _, err := dao.QianqiGuangfubanIdsZhijia.Ctx(ctx).WhereIn(dao.QianqiGuangfubanIdsZhijia.Columns().Id, primaryIDs). Data(g.Map{ dao.QianqiGuangfubanIdsZhijia.Columns().Status: 2, }).Update() liberr.ErrIsNil(ctx, err) fangzhenID := pointList[0].FangzhenId // 根据名字从 pv_module 中获取主键id result, err := dao.PvModule.Ctx(ctx).Fields("id").Where(dao.PvModule.Columns().Name, key). Where(dao.PvModule.Columns().FangzhenId, fangzhenID). Where(dao.PvModule.Columns().Type, t).One() liberr.ErrIsNil(ctx, err) // 获取主键ID keyID := result.Map()["id"].(int) dao.ManageTaskResult.Ctx(ctx).Data(do.ManageTaskResult{ TaskId: id, PvId: keyID, }).Insert() // 记录 // 更新 PVModule 表中的状态 // G01.12.18 lastID, err := dao.PvModule.Ctx(ctx).WhereIn(dao.PvModule.Columns().Id, keyID). Where(dao.PvModule.Columns().FangzhenId, fangzhenID). Where(dao.PvModule.Columns().Type, t). Data(g.Map{ dao.PvModule.Columns().Status: 2, }).Update() // 获取影响行数并更新 work_status 表 rows, err := lastID.RowsAffected() liberr.ErrIsNil(ctx, err) if rows > 0 { _, err := dao.WorkStatus.Ctx(ctx).Where(dao.WorkStatus.Columns().FangzhenId, fangzhenID). Where(dao.WorkStatus.Columns().Type, t). Data(g.Map{ dao.WorkStatus.Columns().Finished: gdb.Raw( "finished + 1", ), }).Update() liberr.ErrIsNil(ctx, err) } // 根据 类型和方阵ID 获取对应的 work_id dd := entity.WorkStatus{} err = dao.WorkStatus.Ctx(ctx).Fields("work_id").Where(dao.WorkStatus.Columns().FangzhenId, fangzhenID). Where(dao.WorkStatus.Columns().Type, t).Scan(&dd) liberr.ErrIsNil(ctx, err) work_id := dd.WorkId err = UpdateWorkSchedule(work_id) liberr.ErrIsNil(ctx, err) }) } } return nil } // 处理匹配到的光伏板 func ProcessMatchedRectangles(matchedRectangles []int) error { if len(matchedRectangles) == 0 { return nil } return g.Try(context.Background(), func(ctx context.Context) { timeNow := time.Now().Format("2006-01-02") // 更新光伏板状态 _, err := dao.PvModule.Ctx(ctx). WhereIn(dao.PvModule.Columns().Id, matchedRectangles). Data(g.Map{ dao.PvModule.Columns().Status: 3, dao.PvModule.Columns().DoneTime: timeNow, }).Update() liberr.ErrIsNil(ctx, err, "更新光伏板状态失败") // 获取所有的 work_id pvModules := []entity.PvModule{} err = dao.PvModule.Ctx(ctx).Fields("work_id").WhereIn("id", matchedRectangles).Scan(&pvModules) liberr.ErrIsNil(ctx, err, "查询 pv_module 表失败") // 提取所有的 work_id workIds := lo.Map(pvModules, func(module entity.PvModule, _ int) string { return module.WorkId }) // 查询 work_status 表 var workStatus []entity.WorkStatus err = dao.WorkStatus.Ctx(ctx).WhereIn(dao.WorkStatus.Columns().WorkId, workIds).Scan(&workStatus) liberr.ErrIsNil(ctx, err, "查询 work_status 表失败") statusMap := make(map[string]entity.WorkStatus) for _, status := range workStatus { statusMap[status.WorkId] = status } // 遍历 pv_module 表,根据 work_id 更新 work_status 表 for _, pvModule := range pvModules { if _, ok := statusMap[pvModule.WorkId]; !ok { continue } // 2. 更新计划表 work_schdule 表中计划 // timeNow := time.Now().Format("2006-01-02") // 根据 work_id 查询所有的计划 var workSchedules []entity.WorkSchedule err = dao.WorkSchedule.Ctx(ctx).Where(dao.WorkSchedule.Columns().WorkId, pvModule.WorkId).Scan(&workSchedules) if len(workSchedules) == 0 { liberr.ErrIsNil(ctx, errors.New("需要前往进度计划对应的方阵中,设置计划日期,随后再来实现进度复查功能!")) return } liberr.ErrIsNil(ctx, err, "查询 work_schedule 表失败") // 根据计划起始和结束时间,当天位于哪个计划中 for _, schedule := range workSchedules { startAt := schedule.StartAt.Format("Y-m-d") endAt := schedule.EndAt.Format("Y-m-d") if timeNow >= startAt && timeNow <= endAt { // 反序列化 Detail var details []struct { Date string `json:"date"` PlanNum int `json:"planNum"` FinishedNum int `json:"finishedNum"` // 手动填充的完成数量 AutoFill int `json:"autoFill"` // 是否自动填充 } if err := json.Unmarshal([]byte(schedule.Detail), &details); err != nil { continue } // 遍历 details,找到当天的计划 for i := range details { if details[i].Date == timeNow { details[i].AutoFill++ // 标记为 AI 识别 } } // 序列化 details detailBytes, err := json.Marshal(details) if err != nil { continue } // 更新 work_schedule 表 _, err = dao.WorkSchedule.Ctx(ctx).Where(dao.WorkSchedule.Columns().WorkId, schedule.WorkId). Data(g.Map{ dao.WorkSchedule.Columns().Detail: string(detailBytes), dao.WorkSchedule.Columns().FinishedNum: schedule.FinishedNum + 1, }).Update() liberr.ErrIsNil(ctx, err, "更新 work_schedule 表失败") var currentStatus entity.WorkStatus err = dao.WorkStatus.Ctx(ctx).Where(dao.WorkStatus.Columns().WorkId, schedule.WorkId).Scan(¤tStatus) liberr.ErrIsNil(ctx, err, "查询 work_status 表失败") if currentStatus.Finished+1 <= currentStatus.Total { finished := currentStatus.Finished + 1 _, err = dao.WorkStatus.Ctx(ctx).Where(dao.WorkStatus.Columns().WorkId, currentStatus.WorkId). Where(dao.WorkStatus.Columns().Type, currentStatus.Type). Data(g.Map{ dao.WorkStatus.Columns().Finished: finished, }).Update() liberr.ErrIsNil(ctx, err, "更新 work_status 表失败") } } } } }) } // 传入一个 workID 修改其对应的计划数据 func UpdateWorkSchedule(workID string) error { return g.Try(context.Background(), func(ctx context.Context) { // 查询 work_status 表 var workStatus []entity.WorkStatus err := dao.WorkStatus.Ctx(ctx).Where(dao.WorkStatus.Columns().WorkId, workID).Scan(&workStatus) liberr.ErrIsNil(ctx, err, "查询 work_status 表失败") // 遍历 work_status 表,如果自增后的完成量小于等于总数,则将完成量加一 for _, status := range workStatus { if status.Finished+1 <= status.Total { finished := status.Finished + 1 _, err = dao.WorkStatus.Ctx(ctx).Where(dao.WorkStatus.Columns().WorkId, status.WorkId). Data(g.Map{ dao.WorkStatus.Columns().Finished: finished, }).Update() liberr.ErrIsNil(ctx, err, "更新 work_status 表失败") } // 2. 更新计划表 work_schdule 表中计划 timeNow := time.Now().Format("2006-01-02") // 根据 work_id 查询所有的计划 var workSchedules []entity.WorkSchedule err = dao.WorkSchedule.Ctx(ctx).Where(dao.WorkSchedule.Columns().WorkId, status.WorkId).Scan(&workSchedules) liberr.ErrIsNil(ctx, err, "查询 work_schedule 表失败") // 会出现多个计划的情况下,需要根据计划的起始和结束时间来确定具体的计划 for _, schedule := range workSchedules { startAt := schedule.StartAt.Format("Y-m-d") endAt := schedule.EndAt.Format("Y-m-d") if timeNow >= startAt && timeNow <= endAt { // 反序列化 Detail var details []struct { Date string `json:"date"` PlanNum int `json:"planNum"` FinishedNum int `json:"finishedNum"` // 手动填充的完成数量 AutoFill int `json:"autoFill"` // 是否自动填充 } if err := json.Unmarshal([]byte(schedule.Detail), &details); err != nil { continue } // 遍历 details,找到当天的计划 for i := range details { if details[i].Date == timeNow { // 为自动填充的数量加一 details[i].AutoFill++ } } // 序列化 details detailBytes, err := json.Marshal(details) if err != nil { continue } // 更新 work_schedule 表 _, err = dao.WorkSchedule.Ctx(ctx).Where(dao.WorkSchedule.Columns().WorkId, status.WorkId). Where(dao.WorkSchedule.Columns().Id, schedule.Id). Data(g.Map{ dao.WorkSchedule.Columns().Detail: string(detailBytes), }).Update() liberr.ErrIsNil(ctx, err, "更新 work_schedule 表失败") } } } }) } // parseAIResult 解析识别的结果,提取出光伏板、支架和立柱的中心点 // 该函数解析的 JSON 格式如下: // // { // "targets": [ // { // "type": "pho", // 光伏板 // "size": [100, 100], // "leftTopPoint": [10, 10] // } // ] // } func ParseAIResul2(fileContent string) *AIResult { // 钻孔、桩基、支架、光伏板 var solarPanels, brackets, pillars, holes []XYPosition gjson.Get(fileContent, "targets").ForEach(func(_, target gjson.Result) bool { targetType := target.Get("type").String() if targetType != solarPanel && targetType != bracket && targetType != pillar && targetType != drillingHole { return true } size := lo.FilterMap(target.Get("size").Array(), func(size gjson.Result, _ int) (int, bool) { return int(size.Int()), true }) leftTopPoint := lo.FilterMap(target.Get("leftTopPoint").Array(), func(point gjson.Result, _ int) (int, bool) { return int(point.Int()), true }) // 获取中心点 centroidX := float64(leftTopPoint[0]) + float64(size[0])/2 centroidY := float64(leftTopPoint[1]) + float64(size[1])/2 position := XYPosition{X: centroidX, Y: centroidY} switch targetType { case solarPanel: solarPanels = append(solarPanels, position) case bracket: brackets = append(brackets, position) case pillar: pillars = append(pillars, position) case drillingHole: holes = append(holes, position) } return true }) marshal1, _ := json.Marshal(solarPanels) marshal2, _ := json.Marshal(brackets) marshal3, _ := json.Marshal(pillars) marshal4, _ := json.Marshal(holes) fmt.Println("光伏板的坐标点", string(marshal1)) fmt.Println("支架的坐标点", string(marshal2)) fmt.Println("立柱的坐标点", string(marshal3)) fmt.Println("钻孔的坐标点", string(marshal4)) return &AIResult{ SolarPanels: solarPanels, Brackets: brackets, Pillars: pillars, Holes: holes, } } // getSolarPanelCenters 获取数据库中光伏板的中心点 func getSolarPanelCenters(projectID string) ([]Point, error) { // 获取所有的子项目 subProjectQuery := g.Model("sub_project").Fields("id").Where("project_id", projectID) var pvModules []entity.PvModule if err := dao.PvModule.Ctx(context.Background()). WhereIn(dao.PvModule.Columns().SubProjectid, subProjectQuery). Where(dao.PvModule.Columns().Type, 15). WhereNot(dao.PvModule.Columns().Status, 2). WhereNot(dao.PvModule.Columns().Status, 3). Scan(&pvModules); err != nil { return nil, err } var points []Point // tojson dd, _ := json.Marshal(pvModules) gjson.Parse(string(dd)).ForEach(func(_, record gjson.Result) bool { record.Get("detail").ForEach(func(_, detail gjson.Result) bool { id := record.Get("id").Int() var longitudeAndLatitude []XYPosition gjson.Get(detail.String(), "positions").ForEach(func(_, position gjson.Result) bool { longitude := position.Get("lng").Float() latitude := position.Get("lat").Float() longitudeAndLatitude = append(longitudeAndLatitude, XYPosition{X: longitude, Y: latitude}) return true }) points = append(points, Point{ID: int(id), Points: longitudeAndLatitude}) return true }) return true }) return points, nil } func getPillars(projectID string) map[string]map[string]GeoPoint { ctx := context.Background() // 获取所有的子项目 subProjectQuery := g.Model("sub_project").Fields("id").Where("project_id", projectID) // 查询 qianqi_guangfuban_ids_zhuangdian 表 var zhuangdians []entity.QianqiGuangfubanIdsZhuangdian if err := dao.QianqiGuangfubanIdsZhuangdian.Ctx(ctx). Fields("id,fangzhen_id,detail,name"). WhereIn(dao.QianqiGuangfubanIdsZhuangdian.Columns().SubProjectid, subProjectQuery). Scan(&zhuangdians); err != nil { panic(err) } // 第一层方阵ID, 第二层立柱名称, 第三层立柱信息 realGeoPoint := make(map[string]map[string]GeoPoint) for _, zhuangdian := range zhuangdians { fangzhenID := zhuangdian.FangzhenId pillarName := zhuangdian.Name var coordinates struct { Position struct { Lng float64 `json:"lng"` Lat float64 `json:"lat"` Alt float64 `json:"alt"` } } if err := json.Unmarshal([]byte(zhuangdian.Detail), &coordinates); err != nil { continue } if _, ok := realGeoPoint[fangzhenID]; !ok { realGeoPoint[fangzhenID] = make(map[string]GeoPoint) } realGeoPoint[fangzhenID][pillarName] = GeoPoint{ ID: zhuangdian.Id, Coordinates: Coordinates{ Lng: coordinates.Position.Lng, Lat: coordinates.Position.Lat, Alt: coordinates.Position.Alt, }, } } return realGeoPoint } // UpdateTableData 更新表中的数据 func UpdateTableData(task_id int) error { ctx := context.Background() // 根据任务ID查询出所有的 PV Module ID var pvlist []struct { PvID int `orm:"pv_id"` FangZhenID int `orm:"fangzhen_id"` WorkID string `orm:"work_id"` TypeID int `orm:"type"` } // 查询出基本信息 err := dao.PvModule.Ctx(ctx).As("pv").Fields("pv.id as pv_id", "pv.fangzhen_id", "pv.work_id", "pv.type"). LeftJoin("manage_task_result as mts", "pv.id = mts.pv_id"). Where("mts.task_id = ?", task_id).Scan(&pvlist) if err != nil { return fmt.Errorf("查询 PV Module 表失败: %w", err) } if len(pvlist) == 0 { return nil } return g.Try(ctx, func(ctx context.Context) { for _, v := range pvlist { if v.TypeID == 15 { // 光伏板 if err := ProcessMatchedRectangles([]int{v.PvID}); err != nil { return } continue } // 更新 PV Module 表 lastID, err := dao.PvModule.Ctx(ctx).Where(dao.PvModule.Columns().Id, v.PvID). Where(dao.PvModule.Columns().FangzhenId, v.FangZhenID). Where(dao.PvModule.Columns().Type, v.TypeID). Data(g.Map{ dao.PvModule.Columns().Status: 2, }).Update() liberr.ErrIsNil(ctx, err) rows, err := lastID.RowsAffected() liberr.ErrIsNil(ctx, err) if rows > 0 { // 更新 work_status 表 _, err := dao.WorkStatus.Ctx(ctx).Where(dao.WorkStatus.Columns().FangzhenId, v.FangZhenID). Where(dao.WorkStatus.Columns().Type, v.TypeID). Data(g.Map{ dao.WorkStatus.Columns().Finished: gdb.Raw( "finished + 1", ), }).Update() liberr.ErrIsNil(ctx, err) UpdateWorkSchedule(v.WorkID) } } }) }