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 } }