package repository import ( "context" "fmt" "bugulma/backend/internal/domain" "gorm.io/gorm" ) // GeographicalFeatureRepository implements domain.GeographicalFeatureRepository with GORM and PostGIS type GeographicalFeatureRepository struct { *BaseRepository[domain.GeographicalFeature] } // NewGeographicalFeatureRepository creates a new GORM-based geographical feature repository func NewGeographicalFeatureRepository(db *gorm.DB) domain.GeographicalFeatureRepository { return &GeographicalFeatureRepository{ BaseRepository: NewBaseRepository[domain.GeographicalFeature](db), } } // GetByType retrieves features by type func (r *GeographicalFeatureRepository) GetByType(ctx context.Context, featureType domain.GeographicalFeatureType) ([]*domain.GeographicalFeature, error) { return r.FindWhereWithContext(ctx, "feature_type = ?", featureType) } // GetWithinBounds retrieves features within geographical bounds using PostGIS func (r *GeographicalFeatureRepository) GetWithinBounds(ctx context.Context, minLat, minLng, maxLat, maxLng float64) ([]*domain.GeographicalFeature, error) { var features []*domain.GeographicalFeature // Use PostGIS ST_MakeEnvelope for bounding box queries query := ` SELECT * FROM geographical_features WHERE ST_Intersects( geometry, ST_MakeEnvelope(?, ?, ?, ?, 4326) ) ` result := r.DB().WithContext(ctx).Raw(query, minLng, minLat, maxLng, maxLat).Scan(&features) if result.Error != nil { return nil, result.Error } return features, nil } // GetIntersectingGeometry retrieves features that intersect with a given geometry (WKT format) func (r *GeographicalFeatureRepository) GetIntersectingGeometry(ctx context.Context, wktGeometry string) ([]*domain.GeographicalFeature, error) { var features []*domain.GeographicalFeature query := ` SELECT * FROM geographical_features WHERE ST_Intersects( geometry, ST_GeomFromText(?, 4326) ) ` result := r.DB().WithContext(ctx).Raw(query, wktGeometry).Scan(&features) if result.Error != nil { return nil, result.Error } return features, nil } // GetByOSMID retrieves a feature by OSM type and ID func (r *GeographicalFeatureRepository) GetByOSMID(ctx context.Context, osmType, osmID string) (*domain.GeographicalFeature, error) { return r.FindOneWhereWithContext(ctx, "osm_type = ? AND osm_id = ?", osmType, osmID) } // BulkCreate inserts multiple geographical features efficiently func (r *GeographicalFeatureRepository) BulkCreate(ctx context.Context, features []*domain.GeographicalFeature) error { if len(features) == 0 { return nil } // Use GORM's CreateInBatches for efficient bulk insertion result := r.DB().WithContext(ctx).CreateInBatches(features, 100) if result.Error != nil { return fmt.Errorf("bulk create failed: %w", result.Error) } return nil } // GetFeaturesWithinRadius retrieves features of a specific type within a radius of a point func (r *GeographicalFeatureRepository) GetFeaturesWithinRadius(ctx context.Context, featureType domain.GeographicalFeatureType, lat, lng, radiusKm float64) ([]*domain.GeographicalFeature, error) { var features []*domain.GeographicalFeature query := ` SELECT * FROM geographical_features WHERE feature_type = ? AND ST_DWithin( geometry::geography, ST_GeogFromText('POINT(? ?)'), ? * 1000 ) ORDER BY ST_Distance(geometry::geography, ST_GeogFromText('POINT(? ?)')) ` result := r.DB().WithContext(ctx).Raw(query, featureType, lng, lat, radiusKm, lng, lat).Scan(&features) if result.Error != nil { return nil, result.Error } return features, nil } // GetRoadsWithinRadius retrieves road features within a radius of a point func (r *GeographicalFeatureRepository) GetRoadsWithinRadius(ctx context.Context, lat, lng, radiusKm float64) ([]*domain.GeographicalFeature, error) { return r.GetFeaturesWithinRadius(ctx, domain.GeographicalFeatureTypeRoad, lat, lng, radiusKm) } // GetGreenSpacesWithinRadius retrieves green space features within a radius func (r *GeographicalFeatureRepository) GetGreenSpacesWithinRadius(ctx context.Context, lat, lng, radiusKm float64) ([]*domain.GeographicalFeature, error) { return r.GetFeaturesWithinRadius(ctx, domain.GeographicalFeatureTypeGreenSpace, lat, lng, radiusKm) } // GetTotalArea calculates total area for a feature type within bounds (for green spaces, etc.) func (r *GeographicalFeatureRepository) GetTotalArea(ctx context.Context, featureType domain.GeographicalFeatureType, minLat, minLng, maxLat, maxLng float64) (float64, error) { var totalArea float64 query := ` SELECT COALESCE(SUM(ST_Area(geometry::geography)), 0) FROM geographical_features WHERE feature_type = ? AND ST_Intersects( geometry, ST_MakeEnvelope(?, ?, ?, ?, 4326) ) ` result := r.DB().WithContext(ctx).Raw(query, featureType, minLng, minLat, maxLng, maxLat).Scan(&totalArea) if result.Error != nil { return 0, result.Error } return totalArea, nil } // GetRoadNetworkStatistics returns statistics about the road network func (r *GeographicalFeatureRepository) GetRoadNetworkStatistics(ctx context.Context) (map[string]interface{}, error) { var stats struct { TotalRoads int64 TotalLengthKm float64 AvgLengthKm float64 MaxLengthKm float64 } // Get basic road counts r.DB().Raw("SELECT COUNT(*) FROM geographical_features WHERE feature_type = 'road'").Scan(&stats.TotalRoads) // Get length statistics if we have roads if stats.TotalRoads > 0 { row := r.DB().Raw(` SELECT SUM(ST_Length(geometry::geography)) / 1000 as total_length_km, AVG(ST_Length(geometry::geography)) / 1000 as avg_length_km, MAX(ST_Length(geometry::geography)) / 1000 as max_length_km FROM geographical_features WHERE feature_type = 'road' AND ST_IsValid(geometry) `).Row() row.Scan(&stats.TotalLengthKm, &stats.AvgLengthKm, &stats.MaxLengthKm) } return map[string]interface{}{ "total_roads": stats.TotalRoads, "total_length_km": stats.TotalLengthKm, "avg_length_km": stats.AvgLengthKm, "max_length_km": stats.MaxLengthKm, }, nil } // Count returns the total number of geographical features func (r *GeographicalFeatureRepository) Count(ctx context.Context) (int64, error) { var count int64 result := r.DB().WithContext(ctx).Model(&domain.GeographicalFeature{}).Count(&count) return count, result.Error } // CountByFeatureType returns the count of features grouped by feature_type func (r *GeographicalFeatureRepository) CountByFeatureType(ctx context.Context) (map[domain.GeographicalFeatureType]int64, error) { var results []struct { FeatureType domain.GeographicalFeatureType Count int64 } err := r.DB().WithContext(ctx).Model(&domain.GeographicalFeature{}). Select("feature_type, COUNT(*) as count"). Group("feature_type"). Scan(&results).Error if err != nil { return nil, err } counts := make(map[domain.GeographicalFeatureType]int64) for _, res := range results { counts[res.FeatureType] = res.Count } return counts, nil }