package qc import ( "fmt" "net/http" "verofy-backend/models" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type AerialEndpointResult struct { SegmentID int `json:"segment_id"` SegmentName string `json:"segment_name"` Type string `json:"type"` IsValid bool `json:"is_valid"` ErrorMessage string `json:"error_message,omitempty"` StartPoleCount int `json:"start_pole_count"` EndPoleCount int `json:"end_pole_count"` StartPoleIDs []int `json:"start_pole_ids,omitempty"` EndPoleIDs []int `json:"end_pole_ids,omitempty"` Geometry map[string]interface{} `json:"geometry,omitempty"` } type AerialEndpointSummary struct { TotalAerialSegments int `json:"total_aerial_segments"` ValidSegments int `json:"valid_segments"` InvalidSegments int `json:"invalid_segments"` PassRate float64 `json:"pass_rate"` Results []AerialEndpointResult `json:"results"` } func AerialEndpointsRoute(router *gin.Engine, db *gorm.DB, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol string) { // Full aerial endpoints summary endpoint router.GET("/api/qc/aerial-endpoints", func(c *gin.Context) { mapID := c.Query("map_id") zone := c.Query("zone") if mapID == "" || zone == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "map_id and zone are required"}) return } summary, err := CheckAerialEndpoints(db, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, summary) }) // Invalid segments only endpoint router.GET("/api/qc/aerial-endpoints/invalid", func(c *gin.Context) { mapID := c.Query("map_id") zone := c.Query("zone") if mapID == "" || zone == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "map_id and zone are required"}) return } invalid, err := GetInvalidAerialEndpoints(db, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "invalid_segments": invalid, "count": len(invalid), }) }) // Update QC flags endpoint router.POST("/api/qc/aerial-endpoints/update-flags", func(c *gin.Context) { var request struct { SegmentIDs []int `json:"segment_ids"` MapID string `json:"map_id"` Zone string `json:"zone"` } if err := c.ShouldBindJSON(&request); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } err := UpdateAerialEndpointFlags(db, request.SegmentIDs, schema, segmentTable, idCol, qcFlagCol) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Updated QC flags for %d segments", len(request.SegmentIDs))}) }) } // CheckAerialEndpoints validates that aerial segments have exactly one pole at each endpoint func CheckAerialEndpoints(db *gorm.DB, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol string) (*AerialEndpointSummary, error) { var segments []models.SegmentGeoJSON table := fmt.Sprintf("%s.%s", schema, segmentTable) // Query aerial segments err := db.Table(table). Select(fmt.Sprintf("id_0, %s, segment_type, segment_status, %s, protection_status, %s, ST_AsGeoJSON(ST_Transform(geom, 4326))::json AS geometry", mapIDCol, idCol, qcFlagCol)). Where(fmt.Sprintf("%s = ? AND %s = ? AND LOWER(segment_type) = ?", mapIDCol, zoneCol), mapID, zone, "aerial"). Find(&segments).Error if err != nil { return nil, fmt.Errorf("failed to fetch aerial segments: %w", err) } // Get poles for the same map poles, err := getPoles(db, mapID, schema) if err != nil { return nil, fmt.Errorf("failed to fetch poles: %w", err) } summary := &AerialEndpointSummary{ TotalAerialSegments: len(segments), Results: make([]AerialEndpointResult, 0, len(segments)), } for _, segment := range segments { result := validateAerialEndpoints(segment, poles) summary.Results = append(summary.Results, result) if result.IsValid { summary.ValidSegments++ } else { summary.InvalidSegments++ } } // Calculate pass rate if summary.TotalAerialSegments > 0 { summary.PassRate = float64(summary.ValidSegments) / float64(summary.TotalAerialSegments) * 100 } return summary, nil } // validateAerialEndpoints checks if aerial segment has exactly one pole at each endpoint func validateAerialEndpoints(segment models.SegmentGeoJSON, poles []models.PolesGeoJSON) AerialEndpointResult { result := AerialEndpointResult{ SegmentID: int(segment.ID), Type: segment.SegmentType, IsValid: false, } // Parse the geometry to get start and end coordinates if len(segment.Geometry) == 0 { result.ErrorMessage = "Segment has no geometry data" return result } startCoord, endCoord, geometry, err := getSegmentEndpoints(segment.Geometry) if err != nil { result.ErrorMessage = fmt.Sprintf("Failed to parse geometry: %v", err) return result } result.Geometry = geometry // Check for poles near start and end coordinates // Using a buffer distance of ~10 meters (0.0001 degrees approximately) bufferDistance := 0.0001 startPoles, startPoleIDs := getPolesNearCoordinate(startCoord, poles, bufferDistance) endPoles, endPoleIDs := getPolesNearCoordinate(endCoord, poles, bufferDistance) result.StartPoleCount = startPoles result.EndPoleCount = endPoles result.StartPoleIDs = startPoleIDs result.EndPoleIDs = endPoleIDs // Valid if exactly ONE pole at each endpoint if startPoles == 1 && endPoles == 1 { result.IsValid = true } else { errorParts := []string{} if startPoles == 0 { errorParts = append(errorParts, "no pole at start") } else if startPoles > 1 { errorParts = append(errorParts, fmt.Sprintf("%d poles at start (should be 1)", startPoles)) } if endPoles == 0 { errorParts = append(errorParts, "no pole at end") } else if endPoles > 1 { errorParts = append(errorParts, fmt.Sprintf("%d poles at end (should be 1)", endPoles)) } if len(errorParts) > 0 { result.ErrorMessage = fmt.Sprintf("Aerial segment pole issues: %v", errorParts) } } return result } // getPolesNearCoordinate counts poles within buffer distance of coordinate and returns their IDs func getPolesNearCoordinate(coord [2]float64, poles []models.PolesGeoJSON, buffer float64) (int, []int) { count := 0 poleIDs := []int{} for _, pole := range poles { if poleCoord, err := getPointCoordinates(pole.Geometry); err == nil { if distance(coord, poleCoord) <= buffer { count++ if pole.ID != nil { poleIDs = append(poleIDs, *pole.ID) } } } } return count, poleIDs } // GetInvalidAerialEndpoints returns only the segments that failed the check func GetInvalidAerialEndpoints(db *gorm.DB, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol string) ([]AerialEndpointResult, error) { summary, err := CheckAerialEndpoints(db, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol) if err != nil { return nil, err } var invalid []AerialEndpointResult for _, result := range summary.Results { if !result.IsValid { invalid = append(invalid, result) } } return invalid, nil } // UpdateAerialEndpointFlags updates QC flags for invalid segments func UpdateAerialEndpointFlags(db *gorm.DB, segmentIDs []int, schema, segmentTable, idCol, qcFlagCol string) error { if len(segmentIDs) == 0 { return nil } table := fmt.Sprintf("%s.%s", schema, segmentTable) return db.Table(table). Where(fmt.Sprintf("%s IN ?", idCol), segmentIDs). Update(qcFlagCol, "aerial_endpoint_issue").Error }