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:
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
echo Building server...
|
||||
go build -o server.exe main.go
|
||||
echo Build complete! Run with: server.exe
|
||||
@@ -0,0 +1,3 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
|
||||
@@ -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=
|
||||
@@ -0,0 +1,3 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
package qc
|
||||
@@ -0,0 +1,3 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
@echo off
|
||||
go build -o server.exe main.go && server.exe
|
||||
@@ -0,0 +1,3 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
ReferrerUrl=C:\Users\AlexanderHall\Downloads\Auto_LLD-QC-main.zip
|
||||
Reference in New Issue
Block a user