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 }