turash/bugulma/backend/internal/repository/geographical_feature_repository.go
Damir Mukimov 0df4812c82
feat: Complete geographical features implementation with full test coverage
- Add comprehensive geographical data models (GeographicalFeature, TransportMode, TransportProfile, TransportOption)
- Implement geographical feature repository with PostGIS support and spatial queries
- Create transportation service for cost calculation and route optimization
- Build spatial resource matcher for geographical resource matching
- Develop environmental impact service for site environmental scoring
- Implement facility location optimizer with multi-criteria analysis
- Add geographical data migration service for SQLite to PostgreSQL migration
- Create database migrations for geographical features and site footprints
- Update geospatial service integration and server initialization
- Add CLI command for geographical data synchronization
- Implement complete test coverage for all geographical components (28 test cases)
- Update test infrastructure for geographical table creation and PostGIS handling

This implements advanced geospatial capabilities including transportation cost modeling, environmental impact assessment, and facility location optimization for the Turash platform.
2025-11-25 06:42:18 +01:00

207 lines
6.8 KiB
Go

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
}