turash/bugulma/backend/internal/service/facility_location_optimizer.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

568 lines
17 KiB
Go

package service
import (
"context"
"encoding/json"
"fmt"
"math"
"sort"
"bugulma/backend/internal/domain"
)
// FacilityLocationOptimizer provides optimal facility location recommendations
type FacilityLocationOptimizer struct {
geoRepo domain.GeographicalFeatureRepository
siteRepo domain.SiteRepository
geospatialSvc *GeospatialService
spatialMatcher *SpatialResourceMatcher
environmentalSvc *EnvironmentalImpactService
transportSvc *TransportationService
}
// NewFacilityLocationOptimizer creates a new facility location optimizer
func NewFacilityLocationOptimizer(
geoRepo domain.GeographicalFeatureRepository,
siteRepo domain.SiteRepository,
geospatialSvc *GeospatialService,
spatialMatcher *SpatialResourceMatcher,
environmentalSvc *EnvironmentalImpactService,
transportSvc *TransportationService,
) *FacilityLocationOptimizer {
return &FacilityLocationOptimizer{
geoRepo: geoRepo,
siteRepo: siteRepo,
geospatialSvc: geospatialSvc,
spatialMatcher: spatialMatcher,
environmentalSvc: environmentalSvc,
transportSvc: transportSvc,
}
}
// LocationCriteria defines the requirements for optimal facility location
type LocationCriteria struct {
// Required resources/facilities nearby
RequiredResources []domain.ResourceType `json:"required_resources"`
ResourceRadiusKm float64 `json:"resource_radius_km"`
// Transportation preferences
PreferredTransport domain.TransportMode `json:"preferred_transport"`
MaxTransportCost float64 `json:"max_transport_cost_eur_month"`
// Environmental constraints
MinEnvironmentalScore float64 `json:"min_environmental_score"`
EnvironmentalWeight float64 `json:"environmental_weight"`
// Infrastructure requirements
RequiredUtilities []string `json:"required_utilities"`
MinFloorAreaM2 float64 `json:"min_floor_area_m2"`
// Cost constraints
MaxDevelopmentCost float64 `json:"max_development_cost_eur"`
BudgetWeight float64 `json:"budget_weight"`
// Strategic factors
ProximityToExistingSites bool `json:"proximity_to_existing_sites"`
ClusterFormationBonus float64 `json:"cluster_formation_bonus"`
// Result limits
MaxResults int `json:"max_results"`
// Scoring weights (should sum to 1.0)
Weights LocationWeights `json:"weights"`
}
// LocationWeights defines how different factors are weighted in scoring
type LocationWeights struct {
Transportation float64 `json:"transportation"`
Environmental float64 `json:"environmental"`
Infrastructure float64 `json:"infrastructure"`
Cost float64 `json:"cost"`
Strategic float64 `json:"strategic"`
}
// DefaultWeights provides balanced default scoring weights
var DefaultWeights = LocationWeights{
Transportation: 0.30,
Environmental: 0.25,
Infrastructure: 0.20,
Cost: 0.15,
Strategic: 0.10,
}
// OptimalLocation represents a potential facility location with comprehensive analysis
type OptimalLocation struct {
Location *LocationCandidate `json:"location"`
Score *LocationScore `json:"score"`
ResourceAccess []*ResourceAccess `json:"resource_access"`
TransportOptions []*domain.TransportOption `json:"transport_options"`
EnvironmentalData *EnvironmentalScore `json:"environmental_data"`
DevelopmentCost *CostEstimate `json:"development_cost"`
}
// LocationCandidate represents a potential location
type LocationCandidate struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Name string `json:"name"`
Description string `json:"description"`
ExistingSite *domain.Site `json:"existing_site,omitempty"` // If based on existing site
}
// LocationScore provides detailed scoring breakdown
type LocationScore struct {
TransportationScore float64 `json:"transportation_score"`
EnvironmentalScore float64 `json:"environmental_score"`
InfrastructureScore float64 `json:"infrastructure_score"`
CostScore float64 `json:"cost_score"`
StrategicScore float64 `json:"strategic_score"`
OverallScore float64 `json:"overall_score"`
Confidence float64 `json:"confidence"` // 0-100, how reliable the scoring is
}
// ResourceAccess describes access to required resources
type ResourceAccess struct {
ResourceType domain.ResourceType `json:"resource_type"`
DistanceKm float64 `json:"distance_km"`
TransportCost float64 `json:"transport_cost_eur_month"`
Availability float64 `json:"availability_score"` // 0-10
ProviderCount int `json:"provider_count"`
}
// CostEstimate provides development cost breakdown
type CostEstimate struct {
LandAcquisition float64 `json:"land_acquisition_eur"`
Infrastructure float64 `json:"infrastructure_eur"`
Utilities float64 `json:"utilities_eur"`
Environmental float64 `json:"environmental_mitigation_eur"`
Total float64 `json:"total_eur"`
TimeMonths int `json:"time_months"`
}
// FindOptimalLocations finds the best facility locations based on criteria
func (f *FacilityLocationOptimizer) FindOptimalLocations(
ctx context.Context,
criteria LocationCriteria,
) ([]*OptimalLocation, error) {
// Set default weights if not provided
if criteria.Weights == (LocationWeights{}) {
criteria.Weights = DefaultWeights
}
// Generate candidate locations
candidates, err := f.generateLocationCandidates(ctx, criteria)
if err != nil {
return nil, fmt.Errorf("failed to generate candidates: %w", err)
}
var optimalLocations []*OptimalLocation
for _, candidate := range candidates {
// Analyze each candidate comprehensively
location, err := f.analyzeLocationCandidate(ctx, candidate, criteria)
if err != nil {
// Log error but continue with other candidates
continue
}
// Apply minimum criteria filters
if f.meetsMinimumCriteria(location, criteria) {
optimalLocations = append(optimalLocations, location)
}
}
// Sort by overall score (highest first)
sort.Slice(optimalLocations, func(i, j int) bool {
return optimalLocations[i].Score.OverallScore > optimalLocations[j].Score.OverallScore
})
// Limit results
if len(optimalLocations) > criteria.MaxResults {
optimalLocations = optimalLocations[:criteria.MaxResults]
}
return optimalLocations, nil
}
// generateLocationCandidates creates potential location options
func (f *FacilityLocationOptimizer) generateLocationCandidates(
ctx context.Context,
criteria LocationCriteria,
) ([]*LocationCandidate, error) {
var candidates []*LocationCandidate
// Strategy 1: Use existing industrial sites as starting points
if criteria.ProximityToExistingSites {
existingSites, err := f.siteRepo.GetBySiteType(ctx, domain.SiteTypeIndustrial)
if err == nil {
for _, site := range existingSites {
// Generate variations around existing sites
candidates = append(candidates, &LocationCandidate{
Latitude: site.Latitude + (math.Mod(float64(len(candidates)), 0.01) - 0.005),
Longitude: site.Longitude + (math.Mod(float64(len(candidates)), 0.01) - 0.005),
Name: fmt.Sprintf("Near %s", site.Name),
Description: "Location near existing industrial facility",
ExistingSite: site,
})
}
}
}
// Strategy 2: Generate grid-based candidates in industrial areas
// Focus on Bugulma's industrial zones
industrialZones := []struct {
name string
lat float64
lng float64
radius float64
}{
{"Bugulma Industrial District", 54.538, 52.802, 2.0},
{"Northern Industrial Zone", 54.550, 52.790, 1.5},
{"Southern Logistics Hub", 54.520, 52.810, 1.5},
}
for _, zone := range industrialZones {
// Generate 5-10 candidates per zone
for i := 0; i < 8; i++ {
// Random distribution within zone
angle := (float64(i) / 8.0) * 2 * math.Pi
distance := zone.radius * math.Sqrt(math.Mod(float64(i+1), 1.0))
lat := zone.lat + (distance/111.0)*math.Cos(angle) // ~111km per degree latitude
lng := zone.lng + (distance/111.0)*math.Sin(angle) / math.Cos(zone.lat*math.Pi/180.0)
candidates = append(candidates, &LocationCandidate{
Latitude: lat,
Longitude: lng,
Name: fmt.Sprintf("%s Site %d", zone.name, i+1),
Description: fmt.Sprintf("Generated location in %s", zone.name),
})
}
}
return candidates, nil
}
// analyzeLocationCandidate performs comprehensive analysis of a location
func (f *FacilityLocationOptimizer) analyzeLocationCandidate(
ctx context.Context,
candidate *LocationCandidate,
criteria LocationCriteria,
) (*OptimalLocation, error) {
location := &OptimalLocation{
Location: candidate,
Score: &LocationScore{},
}
// Analyze resource access
resourceAccess, transportScore, err := f.analyzeResourceAccess(ctx, candidate, criteria)
if err != nil {
return nil, fmt.Errorf("resource access analysis failed: %w", err)
}
location.ResourceAccess = resourceAccess
location.Score.TransportationScore = transportScore
// Analyze environmental factors
envData, envScore, err := f.analyzeEnvironmentalFactors(ctx, candidate, criteria)
if err != nil {
return nil, fmt.Errorf("environmental analysis failed: %w", err)
}
location.EnvironmentalData = envData
location.Score.EnvironmentalScore = envScore
// Analyze infrastructure
infraScore := f.analyzeInfrastructure(ctx, candidate, criteria)
location.Score.InfrastructureScore = infraScore
// Estimate costs
costEstimate, costScore := f.estimateDevelopmentCost(candidate, criteria)
location.DevelopmentCost = costEstimate
location.Score.CostScore = costScore
// Calculate strategic score
strategicScore := f.calculateStrategicScore(candidate, criteria)
location.Score.StrategicScore = strategicScore
// Calculate overall score
location.Score.OverallScore = f.calculateOverallScore(location.Score, criteria.Weights)
// Estimate confidence in scoring
location.Score.Confidence = f.estimateScoringConfidence(location)
// Get transport options
transportOptions, err := f.transportSvc.FindOptimalTransportRoutes(
candidate.Latitude, candidate.Longitude, 54.538, 52.802, 50.0, // To city center
)
if err == nil && len(transportOptions) > 0 {
location.TransportOptions = transportOptions[:min(3, len(transportOptions))] // Top 3 options
}
return location, nil
}
// analyzeResourceAccess evaluates access to required resources
func (f *FacilityLocationOptimizer) analyzeResourceAccess(
ctx context.Context,
candidate *LocationCandidate,
criteria LocationCriteria,
) ([]*ResourceAccess, float64, error) {
var resourceAccess []*ResourceAccess
totalTransportScore := 0.0
for _, resourceType := range criteria.RequiredResources {
// Find nearby providers
results, err := f.spatialMatcher.FindNearbyResourceProviders(
ctx, resourceType, candidate.Latitude, candidate.Longitude, criteria.ResourceRadiusKm, criteria.PreferredTransport,
)
if err != nil {
continue
}
access := &ResourceAccess{
ResourceType: resourceType,
ProviderCount: len(results),
Availability: math.Min(float64(len(results)), 10.0), // Cap at 10
}
if len(results) > 0 {
// Use the best match for distance/cost
bestMatch := results[0]
access.DistanceKm = bestMatch.SpatialMetrics.StraightLineDistance
access.TransportCost = bestMatch.SpatialMetrics.TransportCost
// Add to transport score
distanceScore := math.Max(0, 1.0-(access.DistanceKm/criteria.ResourceRadiusKm))
costScore := math.Max(0, 1.0-(access.TransportCost/criteria.MaxTransportCost))
totalTransportScore += (distanceScore + costScore) / 2.0
}
resourceAccess = append(resourceAccess, access)
}
// Average transport score across all resources
if len(criteria.RequiredResources) > 0 {
totalTransportScore /= float64(len(criteria.RequiredResources))
totalTransportScore *= 10.0 // Scale to 0-10
}
return resourceAccess, totalTransportScore, nil
}
// analyzeEnvironmentalFactors evaluates environmental suitability
func (f *FacilityLocationOptimizer) analyzeEnvironmentalFactors(
ctx context.Context,
candidate *LocationCandidate,
criteria LocationCriteria,
) (*EnvironmentalScore, float64, error) {
envScore, err := f.environmentalSvc.CalculateFacilityEnvironmentalScore(
ctx, candidate.Latitude, candidate.Longitude,
)
if err != nil {
return nil, 0, err
}
// Convert to 0-10 scale for consistency
scaledScore := envScore.OverallScore
return envScore, scaledScore, nil
}
// analyzeInfrastructure evaluates infrastructure availability
func (f *FacilityLocationOptimizer) analyzeInfrastructure(
ctx context.Context,
candidate *LocationCandidate,
criteria LocationCriteria,
) float64 {
score := 0.0
// If based on existing site, use its infrastructure
if candidate.ExistingSite != nil {
site := candidate.ExistingSite
// Utilities scoring
for _, required := range criteria.RequiredUtilities {
if f.siteHasUtility(site, required) {
score += 2.0
}
}
// Floor area
if site.FloorAreaM2 >= criteria.MinFloorAreaM2 {
score += 2.0
}
// Loading and parking
if site.LoadingDocks > 0 {
score += 1.0
}
if site.ParkingSpaces > 0 {
score += 1.0
}
} else {
// For generated locations, estimate based on proximity to infrastructure
// Simplified scoring - in production, would check actual infrastructure data
score = 5.0 // Neutral score for generated locations
}
return math.Min(score, 10.0)
}
// estimateDevelopmentCost provides cost estimation
func (f *FacilityLocationOptimizer) estimateDevelopmentCost(
candidate *LocationCandidate,
criteria LocationCriteria,
) (*CostEstimate, float64) {
estimate := &CostEstimate{
TimeMonths: 12, // Default 12 months
}
if candidate.ExistingSite != nil {
// Renovation costs for existing site
site := candidate.ExistingSite
estimate.LandAcquisition = 0 // Already owned
// Infrastructure upgrades
estimate.Infrastructure = site.FloorAreaM2 * 100.0 // €100/m² renovation
// Utilities connection
estimate.Utilities = 50000.0 // €50k for utility connections
// Environmental mitigation
estimate.Environmental = 25000.0 // €25k for environmental compliance
} else {
// New construction costs
estimate.LandAcquisition = 100000.0 // €100k for land
estimate.Infrastructure = criteria.MinFloorAreaM2 * 200.0 // €200/m² construction
estimate.Utilities = 75000.0 // €75k for new utilities
estimate.Environmental = 50000.0 // €50k for environmental impact
}
estimate.Total = estimate.LandAcquisition + estimate.Infrastructure +
estimate.Utilities + estimate.Environmental
// Cost score (higher is better/cheaper)
costScore := 10.0
if criteria.MaxDevelopmentCost > 0 {
costScore = math.Max(0, 10.0-(estimate.Total/criteria.MaxDevelopmentCost)*10.0)
}
return estimate, costScore
}
// calculateStrategicScore evaluates long-term strategic value
func (f *FacilityLocationOptimizer) calculateStrategicScore(
candidate *LocationCandidate,
criteria LocationCriteria,
) float64 {
score := 0.0
// Proximity to existing industrial sites (cluster formation)
if criteria.ProximityToExistingSites && candidate.ExistingSite != nil {
score += criteria.ClusterFormationBonus
}
// Future expansion potential
score += 2.0 // Baseline strategic value
return math.Min(score, 10.0)
}
// calculateOverallScore computes weighted final score
func (f *FacilityLocationOptimizer) calculateOverallScore(
score *LocationScore,
weights LocationWeights,
) float64 {
return (score.TransportationScore * weights.Transportation) +
(score.EnvironmentalScore * weights.Environmental) +
(score.InfrastructureScore * weights.Infrastructure) +
(score.CostScore * weights.Cost) +
(score.StrategicScore * weights.Strategic)
}
// estimateScoringConfidence provides confidence level in the scoring
func (f *FacilityLocationOptimizer) estimateScoringConfidence(location *OptimalLocation) float64 {
// Base confidence
confidence := 70.0
// Increase confidence with more data
if location.EnvironmentalData != nil {
confidence += 10.0
}
if len(location.ResourceAccess) > 0 {
confidence += 10.0
}
if len(location.TransportOptions) > 0 {
confidence += 5.0
}
if location.DevelopmentCost != nil {
confidence += 5.0
}
return math.Min(confidence, 100.0)
}
// meetsMinimumCriteria checks if location meets basic requirements
func (f *FacilityLocationOptimizer) meetsMinimumCriteria(location *OptimalLocation, criteria LocationCriteria) bool {
// Environmental minimum
if criteria.MinEnvironmentalScore > 0 &&
location.Score.EnvironmentalScore < criteria.MinEnvironmentalScore {
return false
}
// Cost maximum
if criteria.MaxDevelopmentCost > 0 &&
location.DevelopmentCost != nil &&
location.DevelopmentCost.Total > criteria.MaxDevelopmentCost {
return false
}
// Transport cost maximum
if criteria.MaxTransportCost > 0 {
for _, resource := range location.ResourceAccess {
if resource.TransportCost > criteria.MaxTransportCost {
return false
}
}
}
return true
}
// Helper functions
func min(a, b int) int {
if a < b {
return a
}
return b
}
// siteHasUtility checks if a site has a specific utility available
func (f *FacilityLocationOptimizer) siteHasUtility(site *domain.Site, utility string) bool {
if len(site.AvailableUtilities) == 0 {
return false
}
var utilities []string
if err := json.Unmarshal(site.AvailableUtilities, &utilities); err != nil {
return false
}
for _, u := range utilities {
if u == utility {
return true
}
}
return false
}