turash/bugulma/backend/internal/domain/postgis.go
Damir Mukimov 000eab4740
Major repository reorganization and missing backend endpoints implementation
Repository Structure:
- Move files from cluttered root directory into organized structure
- Create archive/ for archived data and scraper results
- Create bugulma/ for the complete application (frontend + backend)
- Create data/ for sample datasets and reference materials
- Create docs/ for comprehensive documentation structure
- Create scripts/ for utility scripts and API tools

Backend Implementation:
- Implement 3 missing backend endpoints identified in gap analysis:
  * GET /api/v1/organizations/{id}/matching/direct - Direct symbiosis matches
  * GET /api/v1/users/me/organizations - User organizations
  * POST /api/v1/proposals/{id}/status - Update proposal status
- Add complete proposal domain model, repository, and service layers
- Create database migration for proposals table
- Fix CLI server command registration issue

API Documentation:
- Add comprehensive proposals.md API documentation
- Update README.md with Users and Proposals API sections
- Document all request/response formats, error codes, and business rules

Code Quality:
- Follow existing Go backend architecture patterns
- Add proper error handling and validation
- Match frontend expected response schemas
- Maintain clean separation of concerns (handler -> service -> repository)
2025-11-25 06:01:16 +01:00

137 lines
3.5 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
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
}
}