package repository import ( "context" "bugulma/backend/internal/domain" "bugulma/backend/internal/geospatial" "gorm.io/gorm" ) // SiteRepository implements domain.SiteRepository with GORM type SiteRepository struct { *BaseRepository[domain.Site] } // NewSiteRepository creates a new GORM-based site repository func NewSiteRepository(db *gorm.DB) domain.SiteRepository { return &SiteRepository{ BaseRepository: NewBaseRepository[domain.Site](db), } } // GetByOrganizationID retrieves sites owned by a specific organization func (r *SiteRepository) GetByOrganizationID(ctx context.Context, organizationID string) ([]*domain.Site, error) { return r.FindWhereWithContext(ctx, "owner_organization_id = ?", organizationID) } // GetWithinRadius retrieves sites within a geographic radius using PostGIS (PostgreSQL) or Haversine (fallback) func (r *SiteRepository) GetWithinRadius(ctx context.Context, lat, lng, radiusKm float64) ([]*domain.Site, error) { // Check if we're using PostgreSQL with PostGIS support dialector := r.DB().Dialector.Name() if dialector == "postgres" { // Check if PostGIS extension and location_geometry column exist var postgisAvailable bool var columnExists bool if err := r.DB().Raw("SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'postgis')").Scan(&postgisAvailable).Error; err == nil && postgisAvailable { if err := r.DB().Raw(` SELECT EXISTS( SELECT 1 FROM information_schema.columns WHERE table_name = 'sites' AND column_name = 'location_geometry' ) `).Scan(&columnExists).Error; err != nil { columnExists = false } } if postgisAvailable && columnExists { // Use PostGIS for PostgreSQL var sites []*domain.Site // use helper to build safe PostGIS snippets and args geo := geospatial.NewGeoHelper(r.DB()) query := ` SELECT * FROM sites WHERE location_geometry IS NOT NULL AND ` + geo.DWithinExpr("location_geometry") + ` ORDER BY ` + geo.OrderByDistanceExpr("location_geometry") + ` ` args := geo.PointRadiusArgs(lng, lat, radiusKm, true) result := r.DB().WithContext(ctx).Raw(query, args...).Scan(&sites) if result.Error != nil { return nil, result.Error } return sites, nil } } // Fallback to simple bounding box approximation for other databases or when PostGIS is not available // For 10km radius, approximate delta_lat ~ 0.09, delta_lng ~ 0.15 at lat 52 var sites []*domain.Site query := ` SELECT * FROM sites WHERE latitude IS NOT NULL AND longitude IS NOT NULL AND abs(latitude - ?) <= 0.09 AND abs(longitude - ?) <= 0.15 ` result := r.DB().WithContext(ctx).Raw(query, lat, lng).Scan(&sites) if result.Error != nil { return nil, result.Error } return sites, nil } // GetBySiteType retrieves sites by type func (r *SiteRepository) GetBySiteType(ctx context.Context, siteType domain.SiteType) ([]*domain.Site, error) { return r.FindWhereWithContext(ctx, "site_type = ?", siteType) } // getLocalizedValue retrieves a localized value from the localizations table // Returns empty string if not found (will fallback to Russian from main table) func (r *SiteRepository) getLocalizedValue(entityType, entityID, field, locale string) string { if locale == "ru" { return "" // Russian is stored in main table, not in localizations } locRepo := NewLocalizationRepository(r.DB()) loc, err := locRepo.GetByEntityAndField(context.Background(), entityType, entityID, field, locale) if err != nil || loc == nil { return "" // Not found, will fallback to Russian } return loc.Value } // GetHeritageSites retrieves sites that have heritage status with localization support func (r *SiteRepository) GetHeritageSites(ctx context.Context, locale string) ([]*domain.Site, error) { // Default to Russian if locale is empty or invalid if locale == "" { locale = "ru" } if locale != "ru" && locale != "en" && locale != "tt" { locale = "ru" } var sites []*domain.Site if err := r.DB().WithContext(ctx).Where("heritage_status IS NOT NULL AND heritage_status != ''").Find(&sites).Error; err != nil { return nil, err } // Apply localization to each site for i := range sites { site := sites[i] // Localize name if localizedName := r.getLocalizedValue(site.GetEntityType(), site.GetEntityID(), "name", locale); localizedName != "" { site.Name = localizedName } // Localize notes (description) if localizedNotes := r.getLocalizedValue(site.GetEntityType(), site.GetEntityID(), "notes", locale); localizedNotes != "" { site.Notes = localizedNotes } // Localize builder_owner if localizedBuilderOwner := r.getLocalizedValue(site.GetEntityType(), site.GetEntityID(), "builder_owner", locale); localizedBuilderOwner != "" { site.BuilderOwner = localizedBuilderOwner } // Localize architect if localizedArchitect := r.getLocalizedValue(site.GetEntityType(), site.GetEntityID(), "architect", locale); localizedArchitect != "" { site.Architect = localizedArchitect } // Localize original_purpose if localizedOriginalPurpose := r.getLocalizedValue(site.GetEntityType(), site.GetEntityID(), "original_purpose", locale); localizedOriginalPurpose != "" { site.OriginalPurpose = localizedOriginalPurpose } // Localize current_use if localizedCurrentUse := r.getLocalizedValue(site.GetEntityType(), site.GetEntityID(), "current_use", locale); localizedCurrentUse != "" { site.CurrentUse = localizedCurrentUse } // Localize style if localizedStyle := r.getLocalizedValue(site.GetEntityType(), site.GetEntityID(), "style", locale); localizedStyle != "" { site.Style = localizedStyle } // Localize materials if localizedMaterials := r.getLocalizedValue(site.GetEntityType(), site.GetEntityID(), "materials", locale); localizedMaterials != "" { site.Materials = localizedMaterials } } return sites, nil }