turash/bugulma/backend/internal/service/spatial_resource_matcher.go

234 lines
7.6 KiB
Go

package service
import (
"context"
"encoding/json"
"fmt"
"math"
"bugulma/backend/internal/domain"
"bugulma/backend/internal/geospatial"
)
// SpatialResourceMatcher enhances resource matching with geographical intelligence
type SpatialResourceMatcher struct {
geoRepo domain.GeographicalFeatureRepository
siteRepo domain.SiteRepository
resourceFlowRepo domain.ResourceFlowRepository
geospatialSvc *GeospatialService
transportSvc *TransportationService
geoCalc geospatial.Calculator
}
// NewSpatialResourceMatcher creates a new spatial resource matcher
func NewSpatialResourceMatcher(
geoRepo domain.GeographicalFeatureRepository,
siteRepo domain.SiteRepository,
resourceFlowRepo domain.ResourceFlowRepository,
geospatialSvc *GeospatialService,
transportSvc *TransportationService,
geoCalc geospatial.Calculator,
) *SpatialResourceMatcher {
return &SpatialResourceMatcher{
geoRepo: geoRepo,
siteRepo: siteRepo,
resourceFlowRepo: resourceFlowRepo,
geospatialSvc: geospatialSvc,
transportSvc: transportSvc,
geoCalc: geoCalc,
}
}
// SpatialMatchResult represents a resource match with spatial metadata
type SpatialMatchResult struct {
ResourceFlow *domain.ResourceFlow `json:"resource_flow"`
ProviderSite *domain.Site `json:"provider_site"`
RequesterSite *domain.Site `json:"requester_site"`
SpatialMetrics *SpatialMetrics `json:"spatial_metrics"`
MatchScore float64 `json:"match_score"`
}
// SpatialMetrics contains geographical analysis for a match
type SpatialMetrics struct {
StraightLineDistance float64 `json:"straight_line_distance_km"`
RoadDistance float64 `json:"road_distance_km,omitempty"`
TransportCost float64 `json:"transport_cost_eur_month"`
EnvironmentalScore float64 `json:"environmental_score"`
InfrastructureScore float64 `json:"infrastructure_score"`
TimeToDeliver float64 `json:"time_to_deliver_hours,omitempty"`
}
// FindNearbyResourceProviders finds resource providers within geographical constraints
func (m *SpatialResourceMatcher) FindNearbyResourceProviders(
ctx context.Context,
resourceType domain.ResourceType,
requesterLat, requesterLng float64,
maxDistanceKm float64,
preferredTransport domain.TransportMode,
) ([]*SpatialMatchResult, error) {
// Find sites within radius that offer the requested resource
nearbySites, err := m.siteRepo.GetWithinRadius(ctx, requesterLat, requesterLng, maxDistanceKm)
if err != nil {
return nil, fmt.Errorf("failed to find nearby sites: %w", err)
}
var results []*SpatialMatchResult
// Process each site and check for matching resource flows
for _, site := range nearbySites {
if site == nil {
continue
}
// Get resource flows for this site
allFlows, err := m.resourceFlowRepo.GetBySiteID(ctx, site.ID)
if err != nil {
continue // Skip if no flows found or error
}
// Filter for output flows of the requested resource type
var flows []*domain.ResourceFlow
for _, flow := range allFlows {
if flow.Direction == domain.DirectionOutput && flow.Type == resourceType {
flows = append(flows, flow)
}
}
// Skip sites that don't have matching flows
if len(flows) == 0 {
continue
}
// Calculate spatial metrics once per site (reused for all flows)
metrics, err := m.calculateSpatialMetrics(ctx, requesterLat, requesterLng, site, preferredTransport)
if err != nil {
continue // Skip sites where we can't calculate metrics
}
// Create match results for each matching flow
for _, flow := range flows {
matchScore := m.calculateMatchScore(metrics, flow)
result := &SpatialMatchResult{
ResourceFlow: flow,
ProviderSite: site,
SpatialMetrics: metrics,
MatchScore: matchScore,
}
results = append(results, result)
}
}
return results, nil
}
// siteProvidesResource checks if a site provides a specific resource type
// This method is deprecated - resource flow checking is now done directly in FindNearbyResourceProviders
// for better efficiency. Kept for backward compatibility if needed elsewhere.
func (m *SpatialResourceMatcher) siteProvidesResource(ctx context.Context, site *domain.Site, resourceType domain.ResourceType) (bool, error) {
// Check if site has output flows of the requested resource type
flows, err := m.resourceFlowRepo.GetBySiteID(ctx, site.ID)
if err != nil {
return false, err
}
for _, flow := range flows {
if flow.Direction == domain.DirectionOutput && flow.Type == resourceType {
return true, nil
}
}
return false, nil
}
// calculateSpatialMetrics calculates spatial metrics between requester and provider
func (m *SpatialResourceMatcher) calculateSpatialMetrics(
ctx context.Context,
fromLat, fromLng float64,
toSite *domain.Site,
preferredTransport domain.TransportMode,
) (*SpatialMetrics, error) {
if toSite == nil {
return nil, fmt.Errorf("toSite cannot be nil")
}
metrics := &SpatialMetrics{}
// Calculate straight-line distance
result, err := m.geoCalc.CalculateDistance(
geospatial.Point{Latitude: fromLat, Longitude: fromLng},
geospatial.Point{Latitude: toSite.Latitude, Longitude: toSite.Longitude},
)
if err != nil {
return nil, fmt.Errorf("failed to calculate distance: %w", err)
}
metrics.StraightLineDistance = result.DistanceKm
// Estimate road distance (simplified approximation)
metrics.RoadDistance = metrics.StraightLineDistance * 1.3 // 30% longer due to roads
// Calculate transportation cost using dedicated service
transportCost, err := m.transportSvc.CalculateTransportCost(
fromLat, fromLng, toSite.Latitude, toSite.Longitude,
preferredTransport, 10.0, // Assume 10 tons for cost calculation
)
if err != nil {
// Use fallback calculation if transport service fails
metrics.TransportCost = metrics.RoadDistance * 0.1 // €0.10 per km fallback
metrics.TimeToDeliver = metrics.RoadDistance / 50.0 // 50 km/h fallback
} else {
metrics.TransportCost = transportCost.CostEur
metrics.TimeToDeliver = transportCost.TimeHours
}
// Environmental score for the destination
envScore, err := m.geospatialSvc.CalculateSiteEnvironmentalScore(ctx, toSite.Latitude, toSite.Longitude)
if err != nil {
metrics.EnvironmentalScore = 5.0 // Default neutral score
} else {
metrics.EnvironmentalScore = envScore
}
// Infrastructure score (simplified)
metrics.InfrastructureScore = m.calculateInfrastructureScore(toSite)
return metrics, nil
}
// calculateMatchScore calculates an overall match score
func (m *SpatialResourceMatcher) calculateMatchScore(metrics *SpatialMetrics, flow *domain.ResourceFlow) float64 {
// Multi-criteria scoring
distanceScore := math.Max(0, 10.0-(metrics.StraightLineDistance/10.0)) // Better closer, max 10km
costScore := math.Max(0, 10.0-(metrics.TransportCost/100.0)) // Better cheaper, max €100
envScore := metrics.EnvironmentalScore // 0-10 scale
infraScore := metrics.InfrastructureScore // 0-10 scale
// Weighted average
return (distanceScore*0.3 + costScore*0.3 + envScore*0.2 + infraScore*0.2)
}
// calculateInfrastructureScore calculates infrastructure quality score
func (m *SpatialResourceMatcher) calculateInfrastructureScore(site *domain.Site) float64 {
score := 5.0 // Base score
// Check available utilities
if len(site.AvailableUtilities) > 0 {
var utilities []string
if err := json.Unmarshal(site.AvailableUtilities, &utilities); err == nil {
score += float64(len(utilities)) * 0.5 // +0.5 per utility
}
}
// Check parking spaces
if site.ParkingSpaces > 0 {
score += 1.0
}
// Check loading docks
if site.LoadingDocks > 0 {
score += 1.0
}
return math.Min(10.0, score) // Cap at 10
}