mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
206 lines
7.2 KiB
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
|
|
}
|