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 func (Point) GormDataType() string { return "geometry(Point,4326)" } // 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 bytes []byte switch v := value.(type) { case []byte: bytes = v case string: bytes = []byte(v) default: return fmt.Errorf("cannot scan %T into Point", value) } // PostGIS returns geometry in EWKB format or WKT format // Try to parse as WKT first (common format) str := string(bytes) // Handle EWKB format (starts with specific bytes) if len(bytes) > 0 && bytes[0] == 0x00 { // This is EWKB format, we need to decode it // For now, we'll extract coordinates from a common pattern // In production, consider using a library like github.com/twpayne/go-geom return fmt.Errorf("EWKB format not yet supported, use WKT") } // Try to parse WKT format: POINT(lng lat) or POINT(lat lng) // PostGIS typically returns: "0101000020E6100000..." (EWKB hex) or WKT // For simplicity, we'll handle the case where we have lat/lng separately // and let the database handle the conversion // If it's a hex string (EWKB), we can't easily parse it here // The best approach is to use ST_AsText in queries or handle it at DB level if strings.HasPrefix(str, "0101") || len(str) > 50 { // Likely EWKB hex format - we'll need to query as text or use ST_AsText p.Valid = false return nil } // Try WKT format: POINT(lng lat) if strings.HasPrefix(str, "POINT") { var lng, lat float64 _, err := fmt.Sscanf(str, "POINT(%f %f)", &lng, &lat) if err != nil { // Try reverse order: POINT(lat lng) _, err = fmt.Sscanf(str, "POINT(%f %f)", &lat, &lng) if err != nil { p.Valid = false return nil } } p.Longitude = lng p.Latitude = lat p.Valid = true return nil } 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 for PostGIS // PostGIS will convert this to the proper geometry type return fmt.Sprintf("SRID=4326;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 } }