turash/bugulma/backend/internal/repository/geographical_feature_repository.go

206 lines
7.2 KiB
Go

package repository
import (
"context"
"fmt"
"bugulma/backend/internal/domain"
"bugulma/backend/internal/geospatial"
"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
geo := geospatial.NewGeoHelper(r.DB())
query := `SELECT * FROM geographical_features
WHERE feature_type = ? AND ` + geo.DWithinExpr("geometry") + `
ORDER BY ST_Distance(geometry::geography, ` + geo.PointExpr() + `::geography)`
// The SQL uses the parameterized point expression twice (within ST_DWithin and in ORDER BY)
// so we must include the repeated lng/lat args for the ORDER BY usage.
args := append([]interface{}{featureType}, geo.PointRadiusArgs(lng, lat, radiusKm, true)...)
// we want ST_DWithin args: lng, lat, radius (and no second point for ORDER BY in this specific query)
result := r.DB().WithContext(ctx).Raw(query, args...).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
}