dragndrop_hld/oldqc/Backend/qc/segment_single_span.go
alex 12407b74e4 Initial commit - Stage 1 working version
Saving current working state before proceeding to Stage 2.
Includes:
- Backend: Python-based QC validator with shapefile processing
- Frontend: Drag-and-drop file upload interface
- Sample files for testing
- Documentation and revision history

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 13:43:57 -07:00

203 lines
6.5 KiB
Go

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
}