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