package qc import ( "encoding/json" "fmt" "net/http" "verofy-backend/models" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type SingleSpanResult struct { SegmentID int `json:"segment_id"` SegmentName string `json:"segment_name"` Type string `json:"type"` VertexCount int `json:"vertex_count"` IsValid bool `json:"is_valid"` ErrorMessage string `json:"error_message,omitempty"` Geometry map[string]interface{} `json:"geometry,omitempty"` } type SingleSpanSummary struct { TotalAerialSegments int `json:"total_aerial_segments"` ValidSegments int `json:"valid_segments"` InvalidSegments int `json:"invalid_segments"` PassRate float64 `json:"pass_rate"` Results []SingleSpanResult `json:"results"` } func SingleSpanRoute(router *gin.Engine, db *gorm.DB, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol string) { // Full single-span summary endpoint router.GET("/api/qc/single-span", 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 := CheckSingleSpan(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/single-span/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 := GetInvalidSingleSpanSegments(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), }) }) } // CheckSingleSpan validates that aerial segments have exactly 2 vertices func CheckSingleSpan(db *gorm.DB, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol string) (*SingleSpanSummary, error) { var segments []models.SegmentGeoJSON table := fmt.Sprintf("%s.%s", schema, segmentTable) // Query aerial segments using the same pattern as graph_connect.go 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) } summary := &SingleSpanSummary{ TotalAerialSegments: len(segments), Results: make([]SingleSpanResult, 0, len(segments)), } for _, segment := range segments { result := validateSegmentSpan(segment) 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 } // validateSegmentSpan checks if a segment has exactly 2 vertices func validateSegmentSpan(segment models.SegmentGeoJSON) SingleSpanResult { result := SingleSpanResult{ SegmentID: int(segment.ID), Type: segment.SegmentType, IsValid: false, } // Parse the geometry to count vertices if len(segment.Geometry) > 0 { vertexCount, geometry, err := countVerticesFromRawMessage(segment.Geometry) if err != nil { result.ErrorMessage = fmt.Sprintf("Failed to parse geometry: %v", err) return result } result.VertexCount = vertexCount result.Geometry = geometry // Aerial segments should have exactly 2 vertices (one span) if vertexCount == 2 { result.IsValid = true } else if vertexCount < 2 { result.ErrorMessage = fmt.Sprintf("Segment has only %d vertex(es), needs exactly 2 for a valid span", vertexCount) } else { result.ErrorMessage = fmt.Sprintf("Segment has %d vertices, should be exactly 2 for a single span (pole to pole)", vertexCount) } } else { result.ErrorMessage = "Segment has no geometry data" } return result } // countVerticesFromRawMessage parses json.RawMessage and counts vertices func countVerticesFromRawMessage(geometryRaw json.RawMessage) (int, map[string]interface{}, error) { var geometry map[string]interface{} if err := json.Unmarshal(geometryRaw, &geometry); err != nil { return 0, nil, fmt.Errorf("invalid GeoJSON: %w", err) } geometryType, ok := geometry["type"].(string) if !ok { return 0, geometry, fmt.Errorf("missing or invalid geometry type") } coordinates, ok := geometry["coordinates"] if !ok { return 0, geometry, fmt.Errorf("missing coordinates") } var vertexCount int switch geometryType { case "LineString": // LineString coordinates are [[x,y], [x,y], ...] if coordArray, ok := coordinates.([]interface{}); ok { vertexCount = len(coordArray) } case "MultiLineString": // MultiLineString coordinates are [[[x,y], [x,y]], [[x,y], [x,y]]] if coordArrays, ok := coordinates.([]interface{}); ok { for _, coordArray := range coordArrays { if lineCoords, ok := coordArray.([]interface{}); ok { vertexCount += len(lineCoords) } } } default: return 0, geometry, fmt.Errorf("unsupported geometry type: %s", geometryType) } return vertexCount, geometry, nil } // GetInvalidSingleSpanSegments returns only the segments that failed the check func GetInvalidSingleSpanSegments(db *gorm.DB, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol string) ([]SingleSpanResult, error) { summary, err := CheckSingleSpan(db, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol) if err != nil { return nil, err } var invalid []SingleSpanResult for _, result := range summary.Results { if !result.IsValid { invalid = append(invalid, result) } } return invalid, nil }