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>
This commit is contained in:
2025-12-04 13:43:57 -07:00
commit 12407b74e4
147 changed files with 8524 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "tmp\\main.exe"
cmd = "go build -o ./tmp/main.exe ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true
+3
View File
@@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
+11
View File
@@ -0,0 +1,11 @@
DB_HOST=bomar.cloud
DB_USER=ospe
DB_PASS=R5TU8Ml8KHE05LKdMvwulJl0VOeQwUCUMXQrMMqXb10=
DB_NAME=vero
DB_PORT=5432
SCHEMA_NAME=eli_test
SEGMENT_TABLE=segment2
ZONE_COLUMN=group_1
MAPID_COLUMN=mapid
ID_COLUMN=id
QCFLAG_COLUMN=qc_flag
+3
View File
@@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
+4
View File
@@ -0,0 +1,4 @@
@echo off
echo Building server...
go build -o server.exe main.go
echo Build complete! Run with: server.exe
+3
View File
@@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
+48
View File
@@ -0,0 +1,48 @@
module verofy-backend
go 1.24.3
require (
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.10.1
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.30.0
)
require (
github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/joho/godotenv v1.5.1
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+3
View File
@@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
+112
View File
@@ -0,0 +1,112 @@
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+3
View File
@@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
+357
View File
@@ -0,0 +1,357 @@
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)
}
}
+3
View File
@@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
@@ -0,0 +1,18 @@
-- Add connectivity fields to sites table for site connectivity QC
-- Run this script against your database to add the required columns
-- Add connectivity_status column (connected/disconnected)
ALTER TABLE eli_test.sites
ADD COLUMN IF NOT EXISTS connectivity_status VARCHAR(20) DEFAULT NULL;
-- Add connectivity_distance column (distance to nearest segment in meters)
ALTER TABLE eli_test.sites
ADD COLUMN IF NOT EXISTS connectivity_distance FLOAT DEFAULT NULL;
-- Create index for performance on connectivity queries
CREATE INDEX IF NOT EXISTS idx_sites_connectivity_status
ON eli_test.sites(connectivity_status);
-- Optional: Add comments to document the columns
COMMENT ON COLUMN eli_test.sites.connectivity_status IS 'Site connectivity status: connected/disconnected based on distance to network';
COMMENT ON COLUMN eli_test.sites.connectivity_distance IS 'Distance in meters to nearest network segment';
@@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
+172
View File
@@ -0,0 +1,172 @@
package models
import "encoding/json"
type Segment struct {
ID0 int `gorm:"column:id_0;primaryKey" json:"id_0"`
MapID int `gorm:"column:mapid" json:"mapid"`
SegmentType string `gorm:"column:segment_type" json:"segment_type"`
SegmentStatus string `gorm:"column:segment_status" json:"segment_status"`
ID int `gorm:"column:id" json:"id"`
ProtectionStatus string `gorm:"column:protection_status" json:"protection_status"`
QCFlag string `gorm:"column:qc_flag" json:"qc_flag"`
Geometry json.RawMessage `gorm:"column:geometry" json:"geometry"`
}
func (Segment) TableName() string {
return "eli_test.segment2"
}
// New struct for GeoJSON response with Geometry as raw JSON
type SegmentGeoJSON struct {
ID0 int `gorm:"column:id_0" json:"id_0"`
MapID int `gorm:"column:mapid" json:"mapid"`
SegmentType string `gorm:"column:segment_type" json:"segment_type"`
SegmentStatus string `gorm:"column:segment_status" json:"segment_status"`
ID int `gorm:"column:id" json:"id"`
ProtectionStatus string `gorm:"column:protection_status" json:"protection_status"`
QCFlag string `gorm:"column:qc_flag" json:"qc_flag"`
Geometry json.RawMessage `gorm:"column:geometry" json:"geometry"` // Added missing geometry field
}
// Sites struct (exported, with tags and GORM column names)
type Sites struct {
GID int `json:"gid" gorm:"primaryKey;column:gid"`
ID *int `json:"id" gorm:"column:id"`
MapProjectID *int `json:"mapprojectid" gorm:"column:mapprojectid"`
Longitude *string `json:"longitude" gorm:"column:longitude"`
Latitude *string `json:"latitude" gorm:"column:latitude"`
Exclude *int `json:"exclude" gorm:"column:exclude"`
Custom *int `json:"custom" gorm:"column:custom"`
Color *string `json:"color" gorm:"column:color"`
Opacity *string `json:"opacity" gorm:"column:opacity"`
ShapeID *string `json:"shapeid" gorm:"column:shapeid"`
StyleSize *string `json:"stylesize" gorm:"column:stylesize"`
CreatedBy *int `json:"createdby" gorm:"column:createdby"`
CreatedDate *int `json:"createddate" gorm:"column:createddate"`
ModifiedBy *int `json:"modifiedby" gorm:"column:modifiedby"`
ModifiedDate *int `json:"modifieddate" gorm:"column:modifieddate"`
HistoryID *int `json:"historyid" gorm:"column:historyid"`
Name *string `json:"name" gorm:"column:name"`
StatusID *int `json:"statusid" gorm:"column:statusid"`
Group1 *string `json:"group1" gorm:"column:group1"`
Group2 *string `json:"group2" gorm:"column:group2"`
IconTypeID *int `json:"icontypeid" gorm:"column:icontypeid"`
SchoolID *string `json:"schoolid" gorm:"column:schoolid"`
SiteDemarc *string `json:"sitedemarc" gorm:"column:sitedemarc"`
Address1 *string `json:"address1" gorm:"column:address1"`
Address2 *string `json:"address2" gorm:"column:address2"`
City *string `json:"city" gorm:"column:city"`
State *string `json:"state" gorm:"column:state"`
Zip *string `json:"zip" gorm:"column:zip"`
ConnectivityStatus *string `json:"connectivity_status" gorm:"column:connectivity_status"`
ConnectivityDistance *float64 `json:"connectivity_distance" gorm:"column:connectivity_distance"`
}
// SitesGeoJSON struct (for your geojson API response)
type SitesGeoJSON struct {
GID int `json:"gid" gorm:"column:gid"`
ID *int `json:"id" gorm:"column:id"`
MapProjectID *int `json:"mapprojectid" gorm:"column:mapprojectid"`
Name *string `json:"name" gorm:"column:name"`
Address1 *string `json:"address1" gorm:"column:address1"`
City *string `json:"city" gorm:"column:city"`
State *string `json:"state" gorm:"column:state"`
Zip *string `json:"zip" gorm:"column:zip"`
Geometry json.RawMessage `json:"geometry" gorm:"column:geometry"`
}
// Poles struct (exported, full DB mapping)
type Poles struct {
GID int `json:"gid" gorm:"primaryKey;column:gid"`
ID *int `json:"id" gorm:"column:id"`
MapProjectID *int `json:"mapprojectid" gorm:"column:mapprojectid"`
Latitude *string `json:"latitude" gorm:"column:latitude"`
Longitude *string `json:"longitude" gorm:"column:longitude"`
Custom *int `json:"custom" gorm:"column:custom"`
Color *string `json:"color" gorm:"column:color"`
ShapeID *string `json:"shapeid" gorm:"column:shapeid"`
StyleSize *string `json:"stylesize" gorm:"column:stylesize"`
Opacity *string `json:"opacity" gorm:"column:opacity"`
CreatedBy *int `json:"createdby" gorm:"column:createdby"`
CreatedDate *int `json:"createddate" gorm:"column:createddate"`
ModifiedBy *int `json:"modifiedby" gorm:"column:modifiedby"`
ModifiedDate *int `json:"modifieddate" gorm:"column:modifieddate"`
HistoryID *int `json:"historyid" gorm:"column:historyid"`
Name *string `json:"name" gorm:"column:name"`
Tags *string `json:"tags" gorm:"column:tags"`
Group1 *string `json:"group1" gorm:"column:group1"`
Group2 *string `json:"group2" gorm:"column:group2"`
MRStateID *int `json:"mrstateid" gorm:"column:mrstateid"`
CommsMRChoiceID *int `json:"commsmrchoiceid" gorm:"column:commsmrchoiceid"`
PowerMRChoiceID *string `json:"powermrchoiceid" gorm:"column:powermrchoiceid"`
PoleHeight *string `json:"poleheight" gorm:"column:poleheight"`
AttachmentHeight *string `json:"attachmentheight" gorm:"column:attachmentheight"`
MRNotes *string `json:"mrnotes" gorm:"column:mrnotes"`
Owner *string `json:"owner" gorm:"column:owner"`
Geom []byte `json:"geom" gorm:"column:geom"`
}
// PolesGeoJSON struct (for geojson response)
type PolesGeoJSON struct {
GID int `json:"gid" gorm:"column:gid"`
ID *int `json:"id" gorm:"column:id"`
MapProjectID *int `json:"mapprojectid" gorm:"column:mapprojectid"`
Name *string `json:"name" gorm:"column:name"`
Tags *string `json:"tags" gorm:"column:tags"`
Group1 *string `json:"group1" gorm:"column:group1"`
Group2 *string `json:"group2" gorm:"column:group2"`
Owner *string `json:"owner" gorm:"column:owner"`
PoleHeight *string `json:"poleheight" gorm:"column:poleheight"`
AttachmentHeight *string `json:"attachmentheight" gorm:"column:attachmentheight"`
Geometry json.RawMessage `json:"geometry" gorm:"column:geometry"`
}
type AccessPointGeoJSON struct {
GID int `json:"gid" gorm:"column:gid"`
ID *int `json:"id" gorm:"column:id"`
Name *string `json:"name" gorm:"column:name"`
MapProjectID *int `json:"mapprojectid" gorm:"column:mapprojectid"`
Latitude *string `json:"latitude" gorm:"column:latitude"`
Longitude *string `json:"longitude" gorm:"column:longitude"`
Manufacturer *string `json:"manufacturer" gorm:"column:manufacturer"`
Size *string `json:"size" gorm:"column:size"`
Locked *int `json:"locked" gorm:"column:locked"`
Description *string `json:"description" gorm:"column:description"`
AKA *string `json:"aka" gorm:"column:aka"`
CreatedBy *int `json:"createdby" gorm:"column:createdby"`
CreatedDate *int `json:"createddate" gorm:"column:createddate"`
ModifiedBy *string `json:"modifiedby" gorm:"column:modifiedby"`
ModifiedDate *string `json:"modifieddate" gorm:"column:modifieddate"`
HistoryID *int `json:"historyid" gorm:"column:historyid"`
Group1 *string `json:"group1" gorm:"column:group1"`
Group2 *string `json:"group2" gorm:"column:group2"`
TypeID *int `json:"typeid" gorm:"column:typeid"`
StatusID *int `json:"statusid" gorm:"column:statusid"`
CRMVendorID *string `json:"crmvendorid" gorm:"column:crmvendorid"`
BillDate *string `json:"billdate" gorm:"column:billdate"`
Geometry json.RawMessage `json:"geometry" gorm:"column:geometry"` // Changed to json.RawMessage
}
func (AccessPointGeoJSON) TableName() string {
return "verofy.access_points"
}
type InfoGeoJSON struct {
ID int `json:"id" gorm:"primaryKey;column:id"`
Name *string `json:"name" gorm:"column:name"`
Tags *string `json:"tags" gorm:"column:tags"`
Description *string `json:"description" gorm:"column:description"`
Group1 *string `json:"group_1" gorm:"column:group_1"`
Group2 *string `json:"group_2" gorm:"column:group_2"`
Geometry json.RawMessage `json:"geometry" gorm:"column:geometry"` // Fixed column name
}
func (InfoGeoJSON) TableName() string {
return "verofy.Info"
}
type MarketOption struct {
MapID int `json:"mapid" gorm:"column:mapid"`
Project string `json:"project" gorm:"column:project"`
}
@@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
+246
View File
@@ -0,0 +1,246 @@
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
}
@@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
+53
View File
@@ -0,0 +1,53 @@
package qc
import (
"encoding/json"
"fmt"
"net/http"
"verofy-backend/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func GraphConnectivityRoute(router *gin.Engine, db *gorm.DB, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol string) {
router.GET("/api/qc/connectivity", 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
}
var segments []models.SegmentGeoJSON
table := fmt.Sprintf("%s.%s", schema, segmentTable)
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 = ?", mapIDCol, zoneCol), mapID, zone).
Find(&segments)
features := []map[string]interface{}{}
for _, s := range segments {
var geometry interface{}
if err := json.Unmarshal(s.Geometry, &geometry); err == nil {
features = append(features, map[string]interface{}{
"type": "Feature",
"geometry": 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,
},
})
}
}
c.JSON(http.StatusOK, map[string]interface{}{
"type": "FeatureCollection",
"features": features,
})
})
}
@@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
+1
View File
@@ -0,0 +1 @@
package qc
@@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
+202
View File
@@ -0,0 +1,202 @@
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
}
@@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
+125
View File
@@ -0,0 +1,125 @@
package qc
import (
"fmt"
"net/http"
"strconv"
"verofy-backend/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func SiteConnectivityRoute(router *gin.Engine, db *gorm.DB, schema string) {
router.GET("/api/qc/site-connectivity", func(c *gin.Context) {
mapID := c.Query("map_id")
zone := c.Query("zone")
maxDistanceStr := c.Query("max_distance")
if mapID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "map_id is required"})
return
}
// Default max distance is 50 meters
maxDistance := 50.0
if maxDistanceStr != "" {
if dist, err := strconv.ParseFloat(maxDistanceStr, 64); err == nil {
maxDistance = dist
}
}
// Get all sites for the market (and zone if specified)
var sites []models.SitesGeoJSON
siteQuery := db.Table(fmt.Sprintf("%s.sites", schema)).
Select("gid, id, mapprojectid, name, address1, city, state, zip, ST_AsGeoJSON(geom)::json AS geometry")
if mapID != "" {
siteQuery = siteQuery.Where("mapprojectid = ?", mapID)
}
siteQuery.Find(&sites)
// Get all segments for connectivity analysis
var segments []models.SegmentGeoJSON
segmentQuery := db.Table(fmt.Sprintf("%s.segment2", schema)).
Select("id_0, mapid, segment_type, segment_status, id, protection_status, qc_flag, ST_AsGeoJSON(geom)::json AS geometry")
if mapID != "" {
segmentQuery = segmentQuery.Where("mapid = ?", mapID)
}
if zone != "" {
segmentQuery = segmentQuery.Where("group_1 = ?", zone)
}
segmentQuery.Find(&segments)
// Analyze connectivity for each site
results := []map[string]interface{}{}
connectedCount := 0
disconnectedCount := 0
for _, site := range sites {
if len(site.Geometry) == 0 {
continue
}
// Use PostGIS to find the nearest segment within max distance
var nearestDistance float64
nearestQuery := fmt.Sprintf(`
SELECT ST_Distance(
ST_Transform(sites.geom, 3857),
ST_Transform(segments.geom, 3857)
) as distance
FROM %s.sites sites, %s.segment2 segments
WHERE sites.gid = ? AND segments.mapid = ?
ORDER BY ST_Distance(
ST_Transform(sites.geom, 3857),
ST_Transform(segments.geom, 3857)
)
LIMIT 1
`, schema, schema)
db.Raw(nearestQuery, site.GID, mapID).Scan(&nearestDistance)
isConnected := nearestDistance <= maxDistance
status := "connected"
if !isConnected {
status = "disconnected"
disconnectedCount++
} else {
connectedCount++
}
// Update the site's connectivity status in the database
updateQuery := fmt.Sprintf("UPDATE %s.sites SET connectivity_status = ?, connectivity_distance = ? WHERE gid = ?", schema)
db.Exec(updateQuery, status, nearestDistance, site.GID)
siteResult := map[string]interface{}{
"site_id": site.GID,
"site_name": site.Name,
"mapprojectid": site.MapProjectID,
"is_connected": isConnected,
"nearest_distance": nearestDistance,
"connectivity_status": status,
"geometry": site.Geometry,
"address": site.Address1,
"city": site.City,
"state": site.State,
}
results = append(results, siteResult)
}
response := map[string]interface{}{
"total_sites": len(sites),
"connected_sites": connectedCount,
"disconnected_sites": disconnectedCount,
"connectivity_rate": float64(connectedCount) / float64(len(sites)) * 100,
"max_distance_meters": maxDistance,
"results": results,
}
c.JSON(http.StatusOK, response)
})
}
@@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
+412
View File
@@ -0,0 +1,412 @@
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
}
@@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
+743
View File
@@ -0,0 +1,743 @@
package qc
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"verofy-backend/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type ZoneContainmentResult struct {
ElementID int `json:"element_id"`
ElementType string `json:"element_type"` // "segment", "site", "pole", "access_point"
ElementName string `json:"element_name,omitempty"`
AssignedZone *string `json:"assigned_zone"`
ActualZones []string `json:"actual_zones,omitempty"`
IsValid bool `json:"is_valid"`
ErrorMessage string `json:"error_message,omitempty"`
Geometry map[string]interface{} `json:"geometry,omitempty"`
}
type ZoneContainmentSummary struct {
TotalElements int `json:"total_elements"`
ValidElements int `json:"valid_elements"`
InvalidElements int `json:"invalid_elements"`
PassRate float64 `json:"pass_rate"`
ByType map[string]TypeSummary `json:"by_type"`
Results []ZoneContainmentResult `json:"results"`
}
type TypeSummary struct {
Total int `json:"total"`
Valid int `json:"valid"`
Invalid int `json:"invalid"`
}
func ZoneContainmentRoute(router *gin.Engine, db *gorm.DB, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol string) {
// Full zone containment summary endpoint
router.GET("/api/qc/zone-containment", 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 := CheckZoneContainment(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 elements only endpoint
router.GET("/api/qc/zone-containment/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 := GetInvalidZoneContainment(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_elements": invalid,
"count": len(invalid),
})
})
// Update QC flags endpoint (for segments)
router.POST("/api/qc/zone-containment/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 := UpdateZoneContainmentFlags(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))})
})
}
// CheckZoneContainment validates that network elements are within their assigned zones
func CheckZoneContainment(db *gorm.DB, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol string) (*ZoneContainmentSummary, error) {
summary := &ZoneContainmentSummary{
Results: make([]ZoneContainmentResult, 0),
ByType: map[string]TypeSummary{
"segment": {Total: 0, Valid: 0, Invalid: 0},
"site": {Total: 0, Valid: 0, Invalid: 0},
"pole": {Total: 0, Valid: 0, Invalid: 0},
"access_point": {Total: 0, Valid: 0, Invalid: 0},
},
}
// Get all zone polygons
zones, err := getZonePolygons(db, schema)
if err != nil {
return nil, fmt.Errorf("failed to fetch zone polygons: %w", err)
}
// Check segments
segmentResults, err := checkSegmentZones(db, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, zones)
if err != nil {
return nil, fmt.Errorf("failed to check segments: %w", err)
}
summary.Results = append(summary.Results, segmentResults...)
// Check sites
siteResults, err := checkSiteZones(db, mapID, schema, zones)
if err != nil {
return nil, fmt.Errorf("failed to check sites: %w", err)
}
summary.Results = append(summary.Results, siteResults...)
// Check poles
poleResults, err := checkPoleZones(db, mapID, schema, zones)
if err != nil {
return nil, fmt.Errorf("failed to check poles: %w", err)
}
summary.Results = append(summary.Results, poleResults...)
// Check access points
accessPointResults, err := checkAccessPointZones(db, mapID, schema, zones)
if err != nil {
return nil, fmt.Errorf("failed to check access points: %w", err)
}
summary.Results = append(summary.Results, accessPointResults...)
// Calculate summary statistics
for _, result := range summary.Results {
typeSummary := summary.ByType[result.ElementType]
typeSummary.Total++
if result.IsValid {
typeSummary.Valid++
summary.ValidElements++
} else {
typeSummary.Invalid++
summary.InvalidElements++
}
summary.ByType[result.ElementType] = typeSummary
summary.TotalElements++
}
// Calculate pass rate
if summary.TotalElements > 0 {
summary.PassRate = float64(summary.ValidElements) / float64(summary.TotalElements) * 100
}
return summary, nil
}
// getZonePolygons fetches all zone polygons from the info table
func getZonePolygons(db *gorm.DB, schema string) ([]models.InfoGeoJSON, error) {
var zones []models.InfoGeoJSON
table := fmt.Sprintf("%s.info", schema)
err := db.Table(table).
Select("id, name, group_1, ST_AsGeoJSON(geom)::json AS geometry").
Find(&zones).Error
if err != nil {
return nil, err
}
return zones, nil
}
// checkSegmentZones validates segments against their assigned zones using PostGIS
func checkSegmentZones(db *gorm.DB, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol string, zones []models.InfoGeoJSON) ([]ZoneContainmentResult, error) {
// Use PostGIS to check intersection directly in the database
type SegmentZoneCheck struct {
ID int `gorm:"column:id"`
SegmentType string `gorm:"column:segment_type"`
AssignedZone *string `gorm:"column:assigned_zone"`
ActualZones string `gorm:"column:actual_zones"`
Geometry json.RawMessage `gorm:"column:geometry"`
}
var results []SegmentZoneCheck
table := fmt.Sprintf("%s.%s", schema, segmentTable)
infoTable := fmt.Sprintf("%s.info", schema)
query := fmt.Sprintf(`
SELECT
s.%s as id,
s.segment_type,
s."%s" as assigned_zone,
STRING_AGG(i.group_1, ',') as actual_zones,
ST_AsGeoJSON(ST_Transform(s.geom, 4326))::json AS geometry
FROM %s s
LEFT JOIN %s i ON ST_Intersects(ST_Transform(s.geom, 4326), i.geom)
WHERE s.%s = ? AND s."%s" = ?
GROUP BY s.%s, s.segment_type, s."%s", s.geom
`, idCol, zoneCol, table, infoTable, mapIDCol, zoneCol, idCol, zoneCol)
err := db.Raw(query, mapID, zone).Scan(&results).Error
if err != nil {
return nil, err
}
qcResults := make([]ZoneContainmentResult, 0, len(results))
for _, seg := range results {
result := ZoneContainmentResult{
ElementID: seg.ID,
ElementType: "segment",
ElementName: seg.SegmentType,
AssignedZone: seg.AssignedZone,
Geometry: parseGeometryToMap(seg.Geometry),
}
// Parse actual zones
if seg.ActualZones != "" {
result.ActualZones = splitZones(seg.ActualZones)
} else {
result.ActualZones = []string{}
}
// Check validity
if seg.AssignedZone == nil || *seg.AssignedZone == "" {
result.IsValid = false
result.ErrorMessage = "Element has no assigned zone (NULL or blank)"
} else if len(result.ActualZones) == 0 {
result.IsValid = false
result.ErrorMessage = fmt.Sprintf("Element assigned to '%s' but not found in any zone", *seg.AssignedZone)
} else {
// Check if assigned zone is in actual zones
result.IsValid = false
for _, actualZone := range result.ActualZones {
if actualZone == *seg.AssignedZone {
result.IsValid = true
break
}
}
if !result.IsValid {
result.ErrorMessage = fmt.Sprintf("Element assigned to '%s' but found in: %v", *seg.AssignedZone, result.ActualZones)
}
}
qcResults = append(qcResults, result)
}
return qcResults, nil
}
// Helper function to split comma-separated zones
func splitZones(zones string) []string {
if zones == "" {
return []string{}
}
parts := []string{}
for _, z := range strings.Split(zones, ",") {
z = strings.TrimSpace(z)
if z != "" {
parts = append(parts, z)
}
}
return parts
}
// Helper to parse geometry JSON to map
func parseGeometryToMap(geomJSON json.RawMessage) map[string]interface{} {
var geomMap map[string]interface{}
if err := json.Unmarshal(geomJSON, &geomMap); err != nil {
return nil
}
return geomMap
}
// checkSiteZones validates sites against their assigned zones using PostGIS
func checkSiteZones(db *gorm.DB, mapID, schema string, zones []models.InfoGeoJSON) ([]ZoneContainmentResult, error) {
type SiteZoneCheck struct {
ID int `gorm:"column:id"`
Name *string `gorm:"column:name"`
AssignedZone *string `gorm:"column:assigned_zone"`
ActualZones string `gorm:"column:actual_zones"`
Geometry json.RawMessage `gorm:"column:geometry"`
}
var results []SiteZoneCheck
table := fmt.Sprintf("%s.sites", schema)
infoTable := fmt.Sprintf("%s.info", schema)
query := fmt.Sprintf(`
SELECT
COALESCE(s.id, s.gid) as id,
s."Name" as name,
s."Group 1" as assigned_zone,
STRING_AGG(i.group_1, ',') as actual_zones,
ST_AsGeoJSON(s.geometry)::json AS geometry
FROM %s s
LEFT JOIN %s i ON ST_Within(s.geometry, i.geom)
WHERE s."MapProjectID" = ?
GROUP BY s.gid, s.id, s."Name", s."Group 1", s.geometry
`, table, infoTable)
err := db.Raw(query, mapID).Scan(&results).Error
if err != nil {
return nil, err
}
qcResults := make([]ZoneContainmentResult, 0, len(results))
for _, site := range results {
result := ZoneContainmentResult{
ElementID: site.ID,
ElementType: "site",
ElementName: "",
AssignedZone: site.AssignedZone,
Geometry: parseGeometryToMap(site.Geometry),
}
if site.Name != nil {
result.ElementName = *site.Name
}
// Parse actual zones
if site.ActualZones != "" {
result.ActualZones = splitZones(site.ActualZones)
} else {
result.ActualZones = []string{}
}
// Check validity
if site.AssignedZone == nil || *site.AssignedZone == "" {
result.IsValid = false
result.ErrorMessage = "Element has no assigned zone (NULL or blank)"
} else if len(result.ActualZones) == 0 {
result.IsValid = false
result.ErrorMessage = fmt.Sprintf("Element assigned to '%s' but not found in any zone", *site.AssignedZone)
} else {
// Check if assigned zone is in actual zones
result.IsValid = false
for _, actualZone := range result.ActualZones {
if actualZone == *site.AssignedZone {
result.IsValid = true
break
}
}
if !result.IsValid {
result.ErrorMessage = fmt.Sprintf("Element assigned to '%s' but found in: %v", *site.AssignedZone, result.ActualZones)
}
}
qcResults = append(qcResults, result)
}
return qcResults, nil
}
// checkPoleZones validates poles against their assigned zones using PostGIS
func checkPoleZones(db *gorm.DB, mapID, schema string, zones []models.InfoGeoJSON) ([]ZoneContainmentResult, error) {
type PoleZoneCheck struct {
ID int `gorm:"column:id"`
Name *string `gorm:"column:name"`
AssignedZone *string `gorm:"column:assigned_zone"`
ActualZones string `gorm:"column:actual_zones"`
Geometry json.RawMessage `gorm:"column:geometry"`
}
var results []PoleZoneCheck
table := fmt.Sprintf("%s.poles", schema)
infoTable := fmt.Sprintf("%s.info", schema)
query := fmt.Sprintf(`
SELECT
COALESCE(p.id, p.gid) as id,
p.name,
p.group1 as assigned_zone,
STRING_AGG(i.group_1, ',') as actual_zones,
ST_AsGeoJSON(ST_Transform(p.geom, 4326))::json AS geometry
FROM %s p
LEFT JOIN %s i ON ST_Within(ST_Transform(p.geom, 4326), i.geom)
WHERE p.mapprojectid = ?
GROUP BY p.gid, p.id, p.name, p.group1, p.geom
`, table, infoTable)
err := db.Raw(query, mapID).Scan(&results).Error
if err != nil {
return nil, err
}
qcResults := make([]ZoneContainmentResult, 0, len(results))
for _, pole := range results {
result := ZoneContainmentResult{
ElementID: pole.ID,
ElementType: "pole",
ElementName: "",
AssignedZone: pole.AssignedZone,
Geometry: parseGeometryToMap(pole.Geometry),
}
if pole.Name != nil {
result.ElementName = *pole.Name
}
// Parse actual zones
if pole.ActualZones != "" {
result.ActualZones = splitZones(pole.ActualZones)
} else {
result.ActualZones = []string{}
}
// Check validity
if pole.AssignedZone == nil || *pole.AssignedZone == "" {
result.IsValid = false
result.ErrorMessage = "Element has no assigned zone (NULL or blank)"
} else if len(result.ActualZones) == 0 {
result.IsValid = false
result.ErrorMessage = fmt.Sprintf("Element assigned to '%s' but not found in any zone", *pole.AssignedZone)
} else {
// Check if assigned zone is in actual zones
result.IsValid = false
for _, actualZone := range result.ActualZones {
if actualZone == *pole.AssignedZone {
result.IsValid = true
break
}
}
if !result.IsValid {
result.ErrorMessage = fmt.Sprintf("Element assigned to '%s' but found in: %v", *pole.AssignedZone, result.ActualZones)
}
}
qcResults = append(qcResults, result)
}
return qcResults, nil
}
// checkAccessPointZones validates access points against their assigned zones using PostGIS
func checkAccessPointZones(db *gorm.DB, mapID, schema string, zones []models.InfoGeoJSON) ([]ZoneContainmentResult, error) {
type AccessPointZoneCheck struct {
ID int `gorm:"column:id"`
Name *string `gorm:"column:name"`
AssignedZone *string `gorm:"column:assigned_zone"`
ActualZones string `gorm:"column:actual_zones"`
Geometry json.RawMessage `gorm:"column:geometry"`
}
var results []AccessPointZoneCheck
table := fmt.Sprintf("%s.access_points", schema)
infoTable := fmt.Sprintf("%s.info", schema)
query := fmt.Sprintf(`
SELECT
COALESCE(ap.id, ap.gid) as id,
ap.name,
ap.group1 as assigned_zone,
STRING_AGG(i.group_1, ',') as actual_zones,
ST_AsGeoJSON(ST_Transform(ap.geom, 4326))::json AS geometry
FROM %s ap
LEFT JOIN %s i ON ST_Within(ST_Transform(ap.geom, 4326), i.geom)
WHERE ap.mapprojectid = ?
GROUP BY ap.gid, ap.id, ap.name, ap.group1, ap.geom
`, table, infoTable)
err := db.Raw(query, mapID).Scan(&results).Error
if err != nil {
return nil, err
}
qcResults := make([]ZoneContainmentResult, 0, len(results))
for _, ap := range results {
result := ZoneContainmentResult{
ElementID: ap.ID,
ElementType: "access_point",
ElementName: "",
AssignedZone: ap.AssignedZone,
Geometry: parseGeometryToMap(ap.Geometry),
}
if ap.Name != nil {
result.ElementName = *ap.Name
}
// Parse actual zones
if ap.ActualZones != "" {
result.ActualZones = splitZones(ap.ActualZones)
} else {
result.ActualZones = []string{}
}
// Check validity
if ap.AssignedZone == nil || *ap.AssignedZone == "" {
result.IsValid = false
result.ErrorMessage = "Element has no assigned zone (NULL or blank)"
} else if len(result.ActualZones) == 0 {
result.IsValid = false
result.ErrorMessage = fmt.Sprintf("Element assigned to '%s' but not found in any zone", *ap.AssignedZone)
} else {
// Check if assigned zone is in actual zones
result.IsValid = false
for _, actualZone := range result.ActualZones {
if actualZone == *ap.AssignedZone {
result.IsValid = true
break
}
}
if !result.IsValid {
result.ErrorMessage = fmt.Sprintf("Element assigned to '%s' but found in: %v", *ap.AssignedZone, result.ActualZones)
}
}
qcResults = append(qcResults, result)
}
return qcResults, nil
}
// validateElementZone checks if an element is within its assigned zone
// For segments: isLineString=true, allows partial intersection
// For points: isLineString=false, requires point to be within zone
func validateElementZone(elementID int, elementType, elementName string, assignedZone *string, geometry json.RawMessage, zones []models.InfoGeoJSON, isLineString bool) ZoneContainmentResult {
result := ZoneContainmentResult{
ElementID: elementID,
ElementType: elementType,
ElementName: elementName,
AssignedZone: assignedZone,
IsValid: false,
ActualZones: []string{},
}
// Parse geometry
var geomMap map[string]interface{}
if err := json.Unmarshal(geometry, &geomMap); err != nil {
result.ErrorMessage = "Failed to parse geometry"
return result
}
result.Geometry = geomMap
// Check if assigned zone is NULL or empty - this is INVALID
if assignedZone == nil || *assignedZone == "" {
result.ErrorMessage = "Element has no assigned zone (NULL or blank)"
return result
}
// Find which zones contain this element
for _, zone := range zones {
if zone.Group1 == nil {
continue
}
if isLineString {
// For segments (LineStrings): check if ANY part intersects with the zone
if geometryIntersectsZone(geomMap, zone.Geometry) {
result.ActualZones = append(result.ActualZones, *zone.Group1)
}
} else {
// For points: check if point is within the zone
if pointWithinZone(geomMap, zone.Geometry) {
result.ActualZones = append(result.ActualZones, *zone.Group1)
}
}
}
// Validate: assigned zone must be in the list of actual zones
for _, actualZone := range result.ActualZones {
if actualZone == *assignedZone {
result.IsValid = true
return result
}
}
// Element is not in its assigned zone
if len(result.ActualZones) == 0 {
result.ErrorMessage = fmt.Sprintf("Element assigned to '%s' but not found in any zone", *assignedZone)
} else {
result.ErrorMessage = fmt.Sprintf("Element assigned to '%s' but found in: %v", *assignedZone, result.ActualZones)
}
return result
}
// geometryIntersectsZone checks if a LineString geometry intersects with a zone polygon
func geometryIntersectsZone(lineGeom map[string]interface{}, zoneGeometry json.RawMessage) bool {
var zonePoly map[string]interface{}
if err := json.Unmarshal(zoneGeometry, &zonePoly); err != nil {
return false
}
// Get line coordinates
lineCoords, ok := lineGeom["coordinates"].([]interface{})
if !ok || len(lineCoords) == 0 {
return false
}
// Get polygon coordinates (first ring is outer boundary)
polyCoords, ok := zonePoly["coordinates"].([]interface{})
if !ok || len(polyCoords) == 0 {
return false
}
outerRing, ok := polyCoords[0].([]interface{})
if !ok || len(outerRing) == 0 {
return false
}
// Check if ANY point of the line is within the polygon
for _, coordInterface := range lineCoords {
coord, ok := coordInterface.([]interface{})
if !ok || len(coord) < 2 {
continue
}
lng, ok1 := coord[0].(float64)
lat, ok2 := coord[1].(float64)
if !ok1 || !ok2 {
continue
}
if pointInPolygon(lng, lat, outerRing) {
return true
}
}
return false
}
// pointWithinZone checks if a Point geometry is within a zone polygon
func pointWithinZone(pointGeom map[string]interface{}, zoneGeometry json.RawMessage) bool {
var zonePoly map[string]interface{}
if err := json.Unmarshal(zoneGeometry, &zonePoly); err != nil {
return false
}
// Get point coordinates
pointCoords, ok := pointGeom["coordinates"].([]interface{})
if !ok || len(pointCoords) < 2 {
return false
}
lng, ok1 := pointCoords[0].(float64)
lat, ok2 := pointCoords[1].(float64)
if !ok1 || !ok2 {
return false
}
// Get polygon coordinates
polyCoords, ok := zonePoly["coordinates"].([]interface{})
if !ok || len(polyCoords) == 0 {
return false
}
outerRing, ok := polyCoords[0].([]interface{})
if !ok || len(outerRing) == 0 {
return false
}
return pointInPolygon(lng, lat, outerRing)
}
// pointInPolygon uses ray casting algorithm to determine if point is inside polygon
func pointInPolygon(lng, lat float64, ring []interface{}) bool {
inside := false
j := len(ring) - 1
for i := 0; i < len(ring); i++ {
coord, ok := ring[i].([]interface{})
if !ok || len(coord) < 2 {
continue
}
coordJ, ok := ring[j].([]interface{})
if !ok || len(coordJ) < 2 {
continue
}
xi, ok1 := coord[0].(float64)
yi, ok2 := coord[1].(float64)
xj, ok3 := coordJ[0].(float64)
yj, ok4 := coordJ[1].(float64)
if !ok1 || !ok2 || !ok3 || !ok4 {
continue
}
intersect := ((yi > lat) != (yj > lat)) && (lng < (xj-xi)*(lat-yi)/(yj-yi)+xi)
if intersect {
inside = !inside
}
j = i
}
return inside
}
// GetInvalidZoneContainment returns only elements that failed the check
func GetInvalidZoneContainment(db *gorm.DB, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol string) ([]ZoneContainmentResult, error) {
summary, err := CheckZoneContainment(db, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol)
if err != nil {
return nil, err
}
var invalid []ZoneContainmentResult
for _, result := range summary.Results {
if !result.IsValid {
invalid = append(invalid, result)
}
}
return invalid, nil
}
// UpdateZoneContainmentFlags updates QC flags for invalid segments
func UpdateZoneContainmentFlags(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, "zone_containment_invalid").Error
}
// Helper function to get string pointer
func getStringPointer(s string) *string {
return &s
}
@@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
+2
View File
@@ -0,0 +1,2 @@
@echo off
go build -o server.exe main.go && server.exe
+3
View File
@@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip