turash/bugulma/backend/internal/domain/postgis.go

123 lines
3.0 KiB
Go

package domain
import (
"database/sql/driver"
"fmt"
"strings"
)
// Point represents a PostGIS Point geometry with SRID 4326 (WGS84)
// Implements sql.Scanner and driver.Valuer for proper GORM integration
type Point struct {
Longitude float64
Latitude float64
Valid bool
}
// GormDataType specifies the database column type for GORM
// We use text type for GORM migrations, and handle PostGIS geometry conversion in Value/Scan
func (Point) GormDataType() string {
return "text"
}
// Scan implements sql.Scanner interface to read PostGIS geometry from database
func (p *Point) Scan(value interface{}) error {
if value == nil {
p.Valid = false
return nil
}
var str string
switch v := value.(type) {
case []byte:
str = string(v)
case string:
str = v
default:
return fmt.Errorf("cannot scan %T into Point", value)
}
// Handle empty or invalid values
if str == "" || str == "POINT EMPTY" {
p.Valid = false
return nil
}
// Try WKT format: POINT(lng lat) or POINT(lat lng)
// This works whether the data comes from PostGIS geometry or plain text storage
if strings.HasPrefix(str, "POINT(") && strings.HasSuffix(str, ")") {
// Extract coordinates from POINT(lng lat) format
coords := strings.TrimPrefix(str, "POINT(")
coords = strings.TrimSuffix(coords, ")")
var lng, lat float64
_, err := fmt.Sscanf(coords, "%f %f", &lng, &lat)
if err != nil {
p.Valid = false
return nil
}
p.Longitude = lng
p.Latitude = lat
p.Valid = true
return nil
}
// If we can't parse it, mark as invalid
p.Valid = false
return nil
}
// Value implements driver.Valuer interface to write PostGIS geometry to database
func (p Point) Value() (driver.Value, error) {
if !p.Valid {
return nil, nil
}
// Return WKT format that works with or without PostGIS
// When PostGIS is available, it will be stored as geometry
// When PostGIS is not available, it will be stored as text
return fmt.Sprintf("POINT(%f %f)", p.Longitude, p.Latitude), nil
}
// String returns the WKT representation of the point
func (p Point) String() string {
if !p.Valid {
return "POINT EMPTY"
}
return fmt.Sprintf("POINT(%f %f)", p.Longitude, p.Latitude)
}
// IsEmpty returns true if the point is not valid
func (p Point) IsEmpty() bool {
return !p.Valid
}
// NewPoint creates a new Point from longitude and latitude
func NewPoint(longitude, latitude float64) Point {
return Point{
Longitude: longitude,
Latitude: latitude,
Valid: true,
}
}
// AfterFind hook helper to populate Point from lat/lng if geometry is not set
// This can be used in GORM hooks to ensure geometry is always populated
func (p *Point) EnsureFromLatLng(lat, lng float64) {
if !p.Valid && lat != 0 && lng != 0 {
p.Longitude = lng
p.Latitude = lat
p.Valid = true
}
}
// BeforeSave hook helper to ensure geometry is set from lat/lng
// This can be used in GORM BeforeSave hooks
func (p *Point) EnsureValid(lat, lng float64) {
if lat != 0 && lng != 0 {
p.Longitude = lng
p.Latitude = lat
p.Valid = true
}
}