package qc import ( "encoding/json" "fmt" "net/http" "verofy-backend/models" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type UndergroundEndpointResult 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"` StartEndpoint string `json:"start_endpoint,omitempty"` EndEndpoint string `json:"end_endpoint,omitempty"` Geometry map[string]interface{} `json:"geometry,omitempty"` } type UndergroundEndpointSummary struct { TotalUndergroundSegments int `json:"total_underground_segments"` ValidSegments int `json:"valid_segments"` InvalidSegments int `json:"invalid_segments"` PassRate float64 `json:"pass_rate"` Results []UndergroundEndpointResult `json:"results"` } func UndergroundEndpointsRoute(router *gin.Engine, db *gorm.DB, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol string) { // Full underground endpoints summary endpoint router.GET("/api/qc/underground-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 := CheckUndergroundEndpoints(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/underground-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 := GetInvalidUndergroundEndpoints(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/underground-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 := UpdateUndergroundEndpointFlags(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))}) }) } // CheckUndergroundEndpoints validates that underground segments have poles or access points at both endpoints func CheckUndergroundEndpoints(db *gorm.DB, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol string) (*UndergroundEndpointSummary, error) { var segments []models.SegmentGeoJSON table := fmt.Sprintf("%s.%s", schema, segmentTable) // Query underground segments err := db.Table(table). Select(fmt.Sprintf("id_0, %s, segment_type, segment_status, %s, protection_status, %s, ST_AsGeoJSON(geom)::json AS geometry", mapIDCol, idCol, qcFlagCol)). Where(fmt.Sprintf("%s = ? AND %s = ? AND LOWER(segment_type) = ?", mapIDCol, zoneCol), mapID, zone, "underground"). Find(&segments).Error if err != nil { return nil, fmt.Errorf("failed to fetch underground segments: %w", err) } // Get poles and access points for the same map/zone poles, err := getPoles(db, mapID, schema) if err != nil { return nil, fmt.Errorf("failed to fetch poles: %w", err) } accessPoints, err := getAccessPoints(db, mapID, schema) if err != nil { return nil, fmt.Errorf("failed to fetch access points: %w", err) } summary := &UndergroundEndpointSummary{ TotalUndergroundSegments: len(segments), Results: make([]UndergroundEndpointResult, 0, len(segments)), } for _, segment := range segments { result := validateUndergroundEndpoints(segment, poles, accessPoints) summary.Results = append(summary.Results, result) if result.IsValid { summary.ValidSegments++ } else { summary.InvalidSegments++ } } // Calculate pass rate if summary.TotalUndergroundSegments > 0 { summary.PassRate = float64(summary.ValidSegments) / float64(summary.TotalUndergroundSegments) * 100 } return summary, nil } // validateUndergroundEndpoints checks if underground segment has poles/access points at both ends func validateUndergroundEndpoints(segment models.SegmentGeoJSON, poles []models.PolesGeoJSON, accessPoints []models.AccessPointGeoJSON) UndergroundEndpointResult { result := UndergroundEndpointResult{ 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/access points near start and end coordinates // Using a buffer distance of ~10 meters (0.0001 degrees approximately) bufferDistance := 0.0001 startHasEndpoint := hasEndpointNearCoordinate(startCoord, poles, accessPoints, bufferDistance) endHasEndpoint := hasEndpointNearCoordinate(endCoord, poles, accessPoints, bufferDistance) startEndpointType := getEndpointTypeNearCoordinate(startCoord, poles, accessPoints, bufferDistance) endEndpointType := getEndpointTypeNearCoordinate(endCoord, poles, accessPoints, bufferDistance) result.StartEndpoint = startEndpointType result.EndEndpoint = endEndpointType if startHasEndpoint && endHasEndpoint { result.IsValid = true } else { errorParts := []string{} if !startHasEndpoint { errorParts = append(errorParts, "no pole/access point at start") } if !endHasEndpoint { errorParts = append(errorParts, "no pole/access point at end") } result.ErrorMessage = fmt.Sprintf("Underground segment missing endpoints: %s", fmt.Sprintf("%s", errorParts)) } return result } // getSegmentEndpoints extracts start and end coordinates from segment geometry func getSegmentEndpoints(geometryRaw json.RawMessage) ([2]float64, [2]float64, map[string]interface{}, error) { var geometry map[string]interface{} if err := json.Unmarshal(geometryRaw, &geometry); err != nil { return [2]float64{}, [2]float64{}, nil, fmt.Errorf("invalid GeoJSON: %w", err) } geometryType, ok := geometry["type"].(string) if !ok { return [2]float64{}, [2]float64{}, geometry, fmt.Errorf("missing or invalid geometry type") } coordinates, ok := geometry["coordinates"] if !ok { return [2]float64{}, [2]float64{}, geometry, fmt.Errorf("missing coordinates") } var startCoord, endCoord [2]float64 switch geometryType { case "LineString": // LineString coordinates are [[x,y], [x,y], ...] if coordArray, ok := coordinates.([]interface{}); ok && len(coordArray) >= 2 { if startPoint, ok := coordArray[0].([]interface{}); ok && len(startPoint) >= 2 { if x, ok := startPoint[0].(float64); ok { startCoord[0] = x } if y, ok := startPoint[1].(float64); ok { startCoord[1] = y } } if endPoint, ok := coordArray[len(coordArray)-1].([]interface{}); ok && len(endPoint) >= 2 { if x, ok := endPoint[0].(float64); ok { endCoord[0] = x } if y, ok := endPoint[1].(float64); ok { endCoord[1] = y } } } else { return [2]float64{}, [2]float64{}, geometry, fmt.Errorf("invalid LineString coordinates") } case "MultiLineString": // For MultiLineString, use first and last coordinates of the entire geometry if coordArrays, ok := coordinates.([]interface{}); ok && len(coordArrays) > 0 { // Get start from first LineString if firstLine, ok := coordArrays[0].([]interface{}); ok && len(firstLine) >= 2 { if startPoint, ok := firstLine[0].([]interface{}); ok && len(startPoint) >= 2 { if x, ok := startPoint[0].(float64); ok { startCoord[0] = x } if y, ok := startPoint[1].(float64); ok { startCoord[1] = y } } } // Get end from last LineString if lastLine, ok := coordArrays[len(coordArrays)-1].([]interface{}); ok && len(lastLine) >= 2 { if endPoint, ok := lastLine[len(lastLine)-1].([]interface{}); ok && len(endPoint) >= 2 { if x, ok := endPoint[0].(float64); ok { endCoord[0] = x } if y, ok := endPoint[1].(float64); ok { endCoord[1] = y } } } } else { return [2]float64{}, [2]float64{}, geometry, fmt.Errorf("invalid MultiLineString coordinates") } default: return [2]float64{}, [2]float64{}, geometry, fmt.Errorf("unsupported geometry type: %s", geometryType) } return startCoord, endCoord, geometry, nil } // hasEndpointNearCoordinate checks if there's a pole or access point within buffer distance of coordinate func hasEndpointNearCoordinate(coord [2]float64, poles []models.PolesGeoJSON, accessPoints []models.AccessPointGeoJSON, buffer float64) bool { // Check poles for _, pole := range poles { if poleCoord, err := getPointCoordinates(pole.Geometry); err == nil { if distance(coord, poleCoord) <= buffer { return true } } } // Check access points for _, ap := range accessPoints { if apCoord, err := getPointCoordinates(ap.Geometry); err == nil { if distance(coord, apCoord) <= buffer { return true } } } return false } // getEndpointTypeNearCoordinate returns the type of endpoint near the coordinate func getEndpointTypeNearCoordinate(coord [2]float64, poles []models.PolesGeoJSON, accessPoints []models.AccessPointGeoJSON, buffer float64) string { // Check poles first for _, pole := range poles { if poleCoord, err := getPointCoordinates(pole.Geometry); err == nil { if distance(coord, poleCoord) <= buffer { return fmt.Sprintf("Pole (ID: %d)", *pole.ID) } } } // Check access points for _, ap := range accessPoints { if apCoord, err := getPointCoordinates(ap.Geometry); err == nil { if distance(coord, apCoord) <= buffer { return fmt.Sprintf("Access Point (ID: %d)", *ap.ID) } } } return "None" } // getPointCoordinates extracts coordinates from point geometry func getPointCoordinates(geometryRaw json.RawMessage) ([2]float64, error) { var geometry map[string]interface{} if err := json.Unmarshal(geometryRaw, &geometry); err != nil { return [2]float64{}, fmt.Errorf("invalid GeoJSON: %w", err) } geometryType, ok := geometry["type"].(string) if !ok || geometryType != "Point" { return [2]float64{}, fmt.Errorf("not a Point geometry") } coordinates, ok := geometry["coordinates"].([]interface{}) if !ok || len(coordinates) < 2 { return [2]float64{}, fmt.Errorf("invalid Point coordinates") } var coord [2]float64 if x, ok := coordinates[0].(float64); ok { coord[0] = x } if y, ok := coordinates[1].(float64); ok { coord[1] = y } return coord, nil } // distance calculates simple Euclidean distance between two coordinates func distance(a, b [2]float64) float64 { dx := a[0] - b[0] dy := a[1] - b[1] return dx*dx + dy*dy // Using squared distance for efficiency } // getPoles fetches poles for the given map ID func getPoles(db *gorm.DB, mapID, schema string) ([]models.PolesGeoJSON, error) { var poles []models.PolesGeoJSON table := fmt.Sprintf("%s.poles", schema) err := db.Table(table). Select("gid, id, mapprojectid, name, tags, group1, group2, owner, poleheight, attachmentheight, ST_AsGeoJSON(geom)::json AS geometry"). Where("mapprojectid = ?", mapID). Find(&poles).Error return poles, err } // getAccessPoints fetches access points for the given map ID func getAccessPoints(db *gorm.DB, mapID, schema string) ([]models.AccessPointGeoJSON, error) { var accessPoints []models.AccessPointGeoJSON table := fmt.Sprintf("%s.access_points", schema) err := db.Table(table). Select(`gid, id, name, mapprojectid, latitude, longitude, manufacturer, size, locked, description, aka, createdby, createddate, modifiedby, modifieddate, historyid, group1, group2, typeid, statusid, crmvendorid, billdate, ST_AsGeoJSON(geom)::json AS geometry`). Where("mapprojectid = ?", mapID). Find(&accessPoints).Error return accessPoints, err } // GetInvalidUndergroundEndpoints returns only the segments that failed the check func GetInvalidUndergroundEndpoints(db *gorm.DB, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol string) ([]UndergroundEndpointResult, error) { summary, err := CheckUndergroundEndpoints(db, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol) if err != nil { return nil, err } var invalid []UndergroundEndpointResult for _, result := range summary.Results { if !result.IsValid { invalid = append(invalid, result) } } return invalid, nil } // UpdateUndergroundEndpointFlags updates QC flags for invalid segments func UpdateUndergroundEndpointFlags(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, "underground_endpoint_issue").Error }