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>
358 lines
11 KiB
Go
358 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"verofy-backend/models"
|
|
"verofy-backend/qc"
|
|
|
|
"github.com/gin-contrib/cors"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/joho/godotenv"
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var db *gorm.DB
|
|
|
|
func getEnv(key, fallback string) string {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func initDB() {
|
|
if err := godotenv.Load(); err != nil {
|
|
log.Println("No .env file found")
|
|
}
|
|
|
|
dsn := fmt.Sprintf(
|
|
"host=%s user=%s password=%s dbname=%s port=%s sslmode=require",
|
|
getEnv("DB_HOST", "localhost"),
|
|
getEnv("DB_USER", "postgres"),
|
|
getEnv("DB_PASS", ""),
|
|
getEnv("DB_NAME", "verofy"),
|
|
getEnv("DB_PORT", "5432"),
|
|
)
|
|
|
|
var err error
|
|
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
|
if err != nil {
|
|
log.Fatal("Failed to connect to database:", err)
|
|
}
|
|
}
|
|
|
|
// Helper function to parse geometry from json.RawMessage
|
|
func parseGeometry(rawGeometry json.RawMessage) interface{} {
|
|
if len(rawGeometry) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var geometry interface{}
|
|
if err := json.Unmarshal(rawGeometry, &geometry); err != nil {
|
|
log.Printf("Failed to parse geometry: %v", err)
|
|
return nil
|
|
}
|
|
return geometry
|
|
}
|
|
|
|
func main() {
|
|
initDB()
|
|
|
|
// Define configuration variables
|
|
schema := getEnv("SCHEMA_NAME", "eli_test")
|
|
segmentTable := getEnv("SEGMENT_TABLE", "segment2")
|
|
zoneCol := getEnv("ZONE_COLUMN", "group_1")
|
|
mapIDCol := getEnv("MAPID_COLUMN", "mapid")
|
|
idCol := getEnv("ID_COLUMN", "id")
|
|
qcFlagCol := getEnv("QCFLAG_COLUMN", "qc_flag")
|
|
serverPort := getEnv("SERVER_PORT", "8080")
|
|
|
|
router := gin.Default()
|
|
router.Use(cors.Default())
|
|
|
|
router.Static("/static", "../Frontend")
|
|
|
|
router.GET("/", func(c *gin.Context) {
|
|
c.File("../Frontend/index.html")
|
|
})
|
|
|
|
// Register QC routes
|
|
qc.GraphConnectivityRoute(router, db, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol)
|
|
qc.SingleSpanRoute(router, db, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol)
|
|
qc.SiteConnectivityRoute(router, db, schema)
|
|
qc.UndergroundEndpointsRoute(router, db, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol)
|
|
qc.AerialEndpointsRoute(router, db, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol)
|
|
qc.ZoneContainmentRoute(router, db, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol)
|
|
|
|
router.GET("/api/markets", func(c *gin.Context) {
|
|
var markets []models.MarketOption
|
|
table := fmt.Sprintf("%s.map_projects", schema)
|
|
db.Table(table).Select("mapid, TRIM(project) as project").Where("mapid IS NOT NULL").Order("project").Scan(&markets)
|
|
c.JSON(http.StatusOK, markets)
|
|
})
|
|
|
|
router.GET("/api/zones", func(c *gin.Context) {
|
|
mapID := c.Query("map_id")
|
|
if mapID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing map_id query parameter"})
|
|
return
|
|
}
|
|
|
|
var zones []string
|
|
table := fmt.Sprintf("%s.%s", schema, segmentTable)
|
|
db.Table(table).Where(fmt.Sprintf("%s = ? AND %s IS NOT NULL", mapIDCol, zoneCol), mapID).Distinct(zoneCol).Pluck(zoneCol, &zones)
|
|
c.JSON(http.StatusOK, zones)
|
|
})
|
|
|
|
router.GET("/api/segments", func(c *gin.Context) {
|
|
mapID := c.Query("map_id")
|
|
zone := c.Query("zone")
|
|
|
|
var segments []struct {
|
|
ID0 int `gorm:"column:id_0"`
|
|
MapID int `gorm:"column:mapid"`
|
|
SegmentType string `gorm:"column:segment_type"`
|
|
SegmentStatus string `gorm:"column:segment_status"`
|
|
ID int `gorm:"column:id"`
|
|
ProtectionStatus string `gorm:"column:protection_status"`
|
|
QCFlag string `gorm:"column:qc_flag"`
|
|
Group1 *string `gorm:"column:group_1"`
|
|
Geometry json.RawMessage `gorm:"column:geometry"`
|
|
}
|
|
table := fmt.Sprintf("%s.%s", schema, segmentTable)
|
|
query := db.Table(table).Select(fmt.Sprintf("id_0, %s, segment_type, segment_status, %s, protection_status, %s, \"%s\" as group_1, ST_AsGeoJSON(ST_Transform(geom, 4326))::json AS geometry", mapIDCol, idCol, qcFlagCol, zoneCol))
|
|
|
|
if mapID != "" {
|
|
query = query.Where(fmt.Sprintf("%s = ?", mapIDCol), mapID)
|
|
}
|
|
if zone != "" {
|
|
query = query.Where(fmt.Sprintf("\"%s\" = ?", zoneCol), zone)
|
|
}
|
|
|
|
query.Find(&segments)
|
|
|
|
features := []map[string]interface{}{}
|
|
for _, s := range segments {
|
|
features = append(features, map[string]interface{}{
|
|
"type": "Feature",
|
|
"geometry": parseGeometry(s.Geometry),
|
|
"properties": map[string]interface{}{
|
|
"id_0": s.ID0,
|
|
"mapid": s.MapID,
|
|
"segment_type": s.SegmentType,
|
|
"segment_status": s.SegmentStatus,
|
|
"id": s.ID,
|
|
"protection_status": s.ProtectionStatus,
|
|
"qc_flag": s.QCFlag,
|
|
"group_1": s.Group1,
|
|
},
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, map[string]interface{}{
|
|
"type": "FeatureCollection",
|
|
"features": features,
|
|
})
|
|
})
|
|
|
|
// SITES
|
|
router.GET("/api/sites", func(c *gin.Context) {
|
|
mapID := c.Query("mapprojectid")
|
|
|
|
var sites []struct {
|
|
GID int `gorm:"column:gid"`
|
|
ID *int `gorm:"column:id"`
|
|
MapProjectID *int `gorm:"column:mapprojectid"`
|
|
Name *string `gorm:"column:name"`
|
|
Address1 *string `gorm:"column:address"`
|
|
City *string `gorm:"column:city"`
|
|
State *string `gorm:"column:state"`
|
|
Zip *string `gorm:"column:zip"`
|
|
Group1 *string `gorm:"column:group1"`
|
|
Geometry json.RawMessage `gorm:"column:geometry"`
|
|
}
|
|
table := fmt.Sprintf("%s.sites", schema)
|
|
query := db.Table(table).Select("gid, id, \"MapProjectID\" as mapprojectid, \"Name\" as name, \"Address1\" as address, \"City\" as city, \"State\" as state, \"Zip\" as zip, \"Group 1\" as group1, ST_AsGeoJSON(geometry)::json AS geometry")
|
|
|
|
if mapID != "" {
|
|
query = query.Where("\"MapProjectID\" = ?", mapID)
|
|
}
|
|
|
|
query.Find(&sites)
|
|
|
|
features := []map[string]interface{}{}
|
|
for _, s := range sites {
|
|
features = append(features, map[string]interface{}{
|
|
"type": "Feature",
|
|
"geometry": parseGeometry(s.Geometry),
|
|
"properties": map[string]interface{}{
|
|
"gid": s.GID,
|
|
"id": s.ID,
|
|
"mapprojectid": s.MapProjectID,
|
|
"name": s.Name,
|
|
"address": s.Address1,
|
|
"city": s.City,
|
|
"state": s.State,
|
|
"zip": s.Zip,
|
|
"group1": s.Group1,
|
|
},
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, map[string]interface{}{
|
|
"type": "FeatureCollection",
|
|
"features": features,
|
|
})
|
|
})
|
|
|
|
// POLES
|
|
router.GET("/api/poles", func(c *gin.Context) {
|
|
mapID := c.Query("map_id")
|
|
|
|
var poles []models.PolesGeoJSON
|
|
table := fmt.Sprintf("%s.poles", schema)
|
|
query := db.Table(table).Select("gid, id, mapprojectid, name, tags, group1, group2, owner, poleheight, attachmentheight, ST_AsGeoJSON(ST_Transform(geom, 4326))::json AS geometry")
|
|
|
|
if mapID != "" {
|
|
query = query.Where("mapprojectid = ?", mapID)
|
|
}
|
|
|
|
query.Find(&poles)
|
|
|
|
features := []map[string]interface{}{}
|
|
for _, p := range poles {
|
|
features = append(features, map[string]interface{}{
|
|
"type": "Feature",
|
|
"geometry": parseGeometry(p.Geometry),
|
|
"properties": map[string]interface{}{
|
|
"gid": p.GID,
|
|
"id": p.ID,
|
|
"mapprojectid": p.MapProjectID,
|
|
"name": p.Name,
|
|
"tags": p.Tags,
|
|
"group1": p.Group1,
|
|
"group2": p.Group2,
|
|
"owner": p.Owner,
|
|
"poleheight": p.PoleHeight,
|
|
"attachmentheight": p.AttachmentHeight,
|
|
},
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, map[string]interface{}{
|
|
"type": "FeatureCollection",
|
|
"features": features,
|
|
})
|
|
})
|
|
|
|
// Access_Points
|
|
router.GET("/api/access_points", func(c *gin.Context) {
|
|
mapID := c.Query("map_id")
|
|
|
|
var accessPoints []models.AccessPointGeoJSON
|
|
table := fmt.Sprintf("%s.access_points", schema)
|
|
query := 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(ST_Transform(geom, 4326))::json AS geometry
|
|
`)
|
|
|
|
if mapID != "" {
|
|
query = query.Where("mapprojectid = ?", mapID)
|
|
}
|
|
|
|
query.Find(&accessPoints)
|
|
|
|
features := []map[string]interface{}{}
|
|
for _, ap := range accessPoints {
|
|
features = append(features, map[string]interface{}{
|
|
"type": "Feature",
|
|
"geometry": parseGeometry(ap.Geometry),
|
|
"properties": map[string]interface{}{
|
|
"gid": ap.GID,
|
|
"id": ap.ID,
|
|
"name": ap.Name,
|
|
"mapprojectid": ap.MapProjectID,
|
|
"latitude": ap.Latitude,
|
|
"longitude": ap.Longitude,
|
|
"manufacturer": ap.Manufacturer,
|
|
"size": ap.Size,
|
|
"locked": ap.Locked,
|
|
"description": ap.Description,
|
|
"aka": ap.AKA,
|
|
"createdby": ap.CreatedBy,
|
|
"createddate": ap.CreatedDate,
|
|
"modifiedby": ap.ModifiedBy,
|
|
"modifieddate": ap.ModifiedDate,
|
|
"historyid": ap.HistoryID,
|
|
"group1": ap.Group1,
|
|
"group2": ap.Group2,
|
|
"typeid": ap.TypeID,
|
|
"statusid": ap.StatusID,
|
|
"crmvendorid": ap.CRMVendorID,
|
|
"billdate": ap.BillDate,
|
|
},
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, map[string]interface{}{
|
|
"type": "FeatureCollection",
|
|
"features": features,
|
|
})
|
|
})
|
|
|
|
// Info Objects - FIXED
|
|
router.GET("/api/info", func(c *gin.Context) {
|
|
mapID := c.Query("map_id")
|
|
|
|
var infos []models.InfoGeoJSON
|
|
table := fmt.Sprintf("%s.info", schema)
|
|
query := db.Table(table).Select(`
|
|
id, name, tags, description, group_1, group_2,
|
|
ST_AsGeoJSON(geom)::json AS geometry
|
|
`)
|
|
|
|
if mapID != "" {
|
|
query = query.Where("mapprojectid = ?", mapID)
|
|
}
|
|
|
|
if err := query.Find(&infos).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
features := []map[string]interface{}{}
|
|
for _, info := range infos {
|
|
features = append(features, map[string]interface{}{
|
|
"type": "Feature",
|
|
"geometry": parseGeometry(info.Geometry),
|
|
"properties": map[string]interface{}{
|
|
"id": info.ID,
|
|
"name": info.Name,
|
|
"tags": info.Tags,
|
|
"description": info.Description,
|
|
"group_1": info.Group1,
|
|
"group_2": info.Group2,
|
|
},
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, map[string]interface{}{
|
|
"type": "FeatureCollection",
|
|
"features": features,
|
|
})
|
|
})
|
|
|
|
// Server Start
|
|
log.Printf("Server is running on http://localhost:%s", serverPort)
|
|
if err := router.Run(":" + serverPort); err != nil {
|
|
log.Fatal("Server failed:", err)
|
|
}
|
|
}
|