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.
593 lines
18 KiB
Go
593 lines
18 KiB
Go
package service
|
|
|
|
import (
|
|
"bugulma/backend/internal/domain"
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// GeospatialService provides advanced geospatial operations for sites and geographical features
|
|
type GeospatialService struct {
|
|
db *gorm.DB
|
|
geoFeatureRepo domain.GeographicalFeatureRepository
|
|
}
|
|
|
|
// NewGeospatialService creates a new geospatial service
|
|
func NewGeospatialService(db *gorm.DB, geoFeatureRepo domain.GeographicalFeatureRepository) *GeospatialService {
|
|
return &GeospatialService{
|
|
db: db,
|
|
geoFeatureRepo: geoFeatureRepo,
|
|
}
|
|
}
|
|
|
|
// SpatialQuery represents a spatial query with various criteria
|
|
type SpatialQuery struct {
|
|
CenterLat float64
|
|
CenterLng float64
|
|
RadiusKm float64
|
|
SiteTypes []domain.SiteType
|
|
ResourceTypes []domain.ResourceType
|
|
MaxResults int
|
|
}
|
|
|
|
// SpatialResult represents a site with spatial metadata
|
|
type SpatialResult struct {
|
|
Site *domain.Site
|
|
DistanceKm float64
|
|
Bearing float64 // Direction from center point
|
|
DrivingTime *int // Estimated driving time in minutes (optional)
|
|
}
|
|
|
|
// SpatialCluster represents a cluster of spatially close sites
|
|
type SpatialCluster struct {
|
|
ID int `json:"id"`
|
|
CentroidLat float64 `json:"centroid_lat"`
|
|
CentroidLng float64 `json:"centroid_lng"`
|
|
SiteCount int `json:"site_count"`
|
|
Sites []SpatialResult `json:"sites"`
|
|
RadiusKm float64 `json:"radius_km"`
|
|
}
|
|
|
|
// FindNearbySites finds sites within radius with advanced filtering
|
|
func (gs *GeospatialService) FindNearbySites(ctx context.Context, query SpatialQuery) ([]SpatialResult, error) {
|
|
var results []SpatialResult
|
|
|
|
// Build the base query with PostGIS
|
|
baseQuery := `
|
|
SELECT
|
|
s.*,
|
|
ST_Distance(s.location_geometry::geography, ST_GeogFromText('POINT(? ?)')) / 1000 as distance_km,
|
|
ST_Azimuth(ST_GeogFromText('POINT(? ?)'), s.location_geometry) as bearing
|
|
FROM sites s
|
|
WHERE s.location_geometry IS NOT NULL
|
|
AND ST_DWithin(
|
|
s.location_geometry::geography,
|
|
ST_GeogFromText('POINT(? ?)'),
|
|
? * 1000
|
|
)
|
|
`
|
|
|
|
args := []interface{}{query.CenterLng, query.CenterLat, query.CenterLng, query.CenterLat, query.CenterLng, query.CenterLat, query.RadiusKm}
|
|
|
|
// Add site type filter
|
|
if len(query.SiteTypes) > 0 {
|
|
placeholders := ""
|
|
for i, siteType := range query.SiteTypes {
|
|
if i > 0 {
|
|
placeholders += ","
|
|
}
|
|
placeholders += "?"
|
|
args = append(args, siteType)
|
|
}
|
|
baseQuery += fmt.Sprintf(" AND s.site_type IN (%s)", placeholders)
|
|
}
|
|
|
|
// Order by distance and limit results
|
|
baseQuery += " ORDER BY s.location_geometry <-> ST_GeogFromText('POINT(? ?)')"
|
|
|
|
if query.MaxResults > 0 {
|
|
baseQuery += " LIMIT ?"
|
|
args = append(args, query.CenterLng, query.CenterLat, query.MaxResults)
|
|
} else {
|
|
args = append(args, query.CenterLng, query.CenterLat)
|
|
}
|
|
|
|
// Execute query
|
|
rows, err := gs.db.Raw(baseQuery, args...).Rows()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var site domain.Site
|
|
var distanceKm, bearing float64
|
|
var locationGeometry interface{} // Geometry column - we don't use it in the struct
|
|
|
|
// Scan the row - need to be careful with column order
|
|
// Note: location_geometry is scanned but not stored in the struct
|
|
err := rows.Scan(
|
|
&site.ID, &site.Name, &site.Latitude, &site.Longitude,
|
|
&locationGeometry, &site.SiteType, &site.FloorAreaM2, &site.Ownership,
|
|
&site.OwnerOrganizationID, &site.AvailableUtilities, &site.ParkingSpaces,
|
|
&site.LoadingDocks, &site.CraneCapacityTonnes, &site.EnergyRating,
|
|
&site.WasteManagement, &site.EnvironmentalImpact, &site.YearBuilt,
|
|
&site.BuilderOwner, &site.Architect, &site.OriginalPurpose, &site.CurrentUse,
|
|
&site.Style, &site.Materials, &site.Storeys, &site.HeritageStatus,
|
|
&site.Notes, &site.Sources, &site.CreatedAt, &site.UpdatedAt,
|
|
&distanceKm, &bearing,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
results = append(results, SpatialResult{
|
|
Site: &site,
|
|
DistanceKm: distanceKm,
|
|
Bearing: bearing,
|
|
})
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// CalculateDistanceMatrix calculates distances between multiple points efficiently
|
|
func (gs *GeospatialService) CalculateDistanceMatrix(ctx context.Context, points []domain.Site) ([][]float64, error) {
|
|
if len(points) == 0 {
|
|
return [][]float64{}, nil
|
|
}
|
|
|
|
matrix := make([][]float64, len(points))
|
|
for i := range matrix {
|
|
matrix[i] = make([]float64, len(points))
|
|
}
|
|
|
|
// For small matrices, calculate directly
|
|
// For larger matrices, could use PostGIS functions
|
|
for i := 0; i < len(points); i++ {
|
|
for j := 0; j < len(points); j++ {
|
|
if i == j {
|
|
matrix[i][j] = 0
|
|
} else {
|
|
matrix[i][j] = gs.calculateHaversineDistance(
|
|
points[i].Latitude, points[i].Longitude,
|
|
points[j].Latitude, points[j].Longitude,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
return matrix, nil
|
|
}
|
|
|
|
// calculateHaversineDistance calculates distance using Haversine formula
|
|
func (gs *GeospatialService) calculateHaversineDistance(lat1, lon1, lat2, lon2 float64) float64 {
|
|
const R = 6371 // Earth radius in km
|
|
|
|
dLat := (lat2 - lat1) * math.Pi / 180
|
|
dLon := (lon2 - lon1) * math.Pi / 180
|
|
|
|
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
|
|
math.Cos(lat1*math.Pi/180)*math.Cos(lat2*math.Pi/180)*
|
|
math.Sin(dLon/2)*math.Sin(dLon/2)
|
|
|
|
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
|
|
|
return R * c
|
|
}
|
|
|
|
// ValidateGeometry validates PostGIS geometry
|
|
func (gs *GeospatialService) ValidateGeometry(ctx context.Context, siteID string) error {
|
|
var count int64
|
|
err := gs.db.Raw(`
|
|
SELECT COUNT(*) FROM sites
|
|
WHERE id = ? AND ST_IsValid(location_geometry)
|
|
`, siteID).Scan(&count).Error
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if count == 0 {
|
|
return fmt.Errorf("invalid geometry for site %s", siteID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetSpatialStatistics returns spatial statistics for sites
|
|
func (gs *GeospatialService) GetSpatialStatistics(ctx context.Context) (map[string]interface{}, error) {
|
|
var stats struct {
|
|
TotalSites int64
|
|
SitesWithGeometry int64
|
|
AvgDistance float64
|
|
MaxDistance float64
|
|
MedianLatitude float64
|
|
MedianLongitude float64
|
|
}
|
|
|
|
// Get basic counts
|
|
gs.db.Raw("SELECT COUNT(*) as total_sites FROM sites").Scan(&stats.TotalSites)
|
|
gs.db.Raw("SELECT COUNT(*) as sites_with_geometry FROM sites WHERE location_geometry IS NOT NULL").Scan(&stats.SitesWithGeometry)
|
|
|
|
// Calculate spatial statistics if we have geometry data
|
|
if stats.SitesWithGeometry > 1 {
|
|
row := gs.db.Raw(`
|
|
SELECT
|
|
AVG(ST_Distance(a.location_geometry::geography, b.location_geometry::geography)) / 1000 as avg_distance,
|
|
MAX(ST_Distance(a.location_geometry::geography, b.location_geometry::geography)) / 1000 as max_distance
|
|
FROM sites a
|
|
CROSS JOIN sites b
|
|
WHERE a.id < b.id
|
|
AND a.location_geometry IS NOT NULL
|
|
AND b.location_geometry IS NOT NULL
|
|
`).Row()
|
|
|
|
row.Scan(&stats.AvgDistance, &stats.MaxDistance)
|
|
|
|
// Calculate median coordinates
|
|
row2 := gs.db.Raw(`
|
|
SELECT
|
|
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY latitude) as median_latitude,
|
|
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY longitude) as median_longitude
|
|
FROM sites
|
|
WHERE location_geometry IS NOT NULL
|
|
`).Row()
|
|
|
|
row2.Scan(&stats.MedianLatitude, &stats.MedianLongitude)
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"total_sites": stats.TotalSites,
|
|
"sites_with_geometry": stats.SitesWithGeometry,
|
|
"avg_distance_km": stats.AvgDistance,
|
|
"max_distance_km": stats.MaxDistance,
|
|
"median_latitude": stats.MedianLatitude,
|
|
"median_longitude": stats.MedianLongitude,
|
|
}, nil
|
|
}
|
|
|
|
// FindSpatialClusters finds clusters of spatially close sites using DBSCAN algorithm
|
|
func (gs *GeospatialService) FindSpatialClusters(ctx context.Context, minPoints int, radiusKm float64) ([]SpatialCluster, error) {
|
|
var clusters []SpatialCluster
|
|
|
|
// Use PostGIS ST_ClusterDBSCAN to group nearby sites
|
|
// Convert radius from km to degrees (approximate)
|
|
radiusDegrees := radiusKm / 111.32 // Rough conversion: 1 degree ≈ 111.32 km
|
|
|
|
query := `
|
|
WITH clustered_sites AS (
|
|
SELECT
|
|
s.*,
|
|
ST_ClusterDBSCAN(s.location_geometry, ?, ?) OVER () as cluster_id,
|
|
ST_Centroid(ST_Collect(s.location_geometry)) OVER (PARTITION BY ST_ClusterDBSCAN(s.location_geometry, ?, ?) OVER ()) as cluster_centroid
|
|
FROM sites s
|
|
WHERE s.location_geometry IS NOT NULL
|
|
),
|
|
cluster_stats AS (
|
|
SELECT
|
|
cluster_id,
|
|
COUNT(*) as site_count,
|
|
ST_Y(cluster_centroid) as centroid_lat,
|
|
ST_X(cluster_centroid) as centroid_lng,
|
|
MAX(ST_Distance(cluster_centroid::geography, location_geometry::geography)) / 1000 as cluster_radius_km
|
|
FROM clustered_sites
|
|
WHERE cluster_id IS NOT NULL
|
|
GROUP BY cluster_id, cluster_centroid
|
|
HAVING COUNT(*) >= ?
|
|
)
|
|
SELECT
|
|
cs.cluster_id,
|
|
cstats.centroid_lat,
|
|
cstats.centroid_lng,
|
|
cstats.site_count,
|
|
cstats.cluster_radius_km,
|
|
cs.id as site_id,
|
|
cs.name,
|
|
cs.latitude,
|
|
cs.longitude,
|
|
cs.location_geometry,
|
|
cs.site_type,
|
|
cs.floor_area_m2,
|
|
cs.ownership,
|
|
cs.owner_organization_id,
|
|
cs.available_utilities,
|
|
cs.parking_spaces,
|
|
cs.loading_docks,
|
|
cs.crane_capacity_tonnes,
|
|
cs.energy_rating,
|
|
cs.waste_management,
|
|
cs.environmental_impact,
|
|
cs.year_built,
|
|
cs.builder_owner,
|
|
cs.architect,
|
|
cs.original_purpose,
|
|
cs.current_use,
|
|
cs.style,
|
|
cs.materials,
|
|
cs.storeys,
|
|
cs.heritage_status,
|
|
cs.notes,
|
|
cs.sources,
|
|
cs.created_at,
|
|
cs.updated_at,
|
|
ST_Distance(cstats.cluster_centroid::geography, cs.location_geometry::geography) / 1000 as distance_from_centroid
|
|
FROM clustered_sites cs
|
|
JOIN cluster_stats cstats ON cs.cluster_id = cstats.cluster_id
|
|
ORDER BY cs.cluster_id, distance_from_centroid
|
|
`
|
|
|
|
rows, err := gs.db.Raw(query, radiusDegrees, minPoints, radiusDegrees, minPoints, minPoints).Rows()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to execute clustering query: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
clusterMap := make(map[int]*SpatialCluster)
|
|
|
|
for rows.Next() {
|
|
var (
|
|
clusterID int
|
|
centroidLat float64
|
|
centroidLng float64
|
|
siteCount int
|
|
clusterRadiusKm float64
|
|
site domain.Site
|
|
distanceFromCentroid float64
|
|
locationGeometry interface{} // Geometry column - we don't use it in the struct
|
|
)
|
|
|
|
err := rows.Scan(
|
|
&clusterID, ¢roidLat, ¢roidLng, &siteCount, &clusterRadiusKm,
|
|
&site.ID, &site.Name, &site.Latitude, &site.Longitude,
|
|
&locationGeometry, &site.SiteType, &site.FloorAreaM2, &site.Ownership,
|
|
&site.OwnerOrganizationID, &site.AvailableUtilities, &site.ParkingSpaces,
|
|
&site.LoadingDocks, &site.CraneCapacityTonnes, &site.EnergyRating,
|
|
&site.WasteManagement, &site.EnvironmentalImpact, &site.YearBuilt,
|
|
&site.BuilderOwner, &site.Architect, &site.OriginalPurpose, &site.CurrentUse,
|
|
&site.Style, &site.Materials, &site.Storeys, &site.HeritageStatus,
|
|
&site.Notes, &site.Sources, &site.CreatedAt, &site.UpdatedAt,
|
|
&distanceFromCentroid,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan cluster row: %w", err)
|
|
}
|
|
|
|
// Get or create cluster
|
|
cluster, exists := clusterMap[clusterID]
|
|
if !exists {
|
|
cluster = &SpatialCluster{
|
|
ID: clusterID,
|
|
CentroidLat: centroidLat,
|
|
CentroidLng: centroidLng,
|
|
SiteCount: siteCount,
|
|
Sites: []SpatialResult{},
|
|
RadiusKm: clusterRadiusKm,
|
|
}
|
|
clusterMap[clusterID] = cluster
|
|
}
|
|
|
|
// Add site to cluster
|
|
cluster.Sites = append(cluster.Sites, SpatialResult{
|
|
Site: &site,
|
|
DistanceKm: distanceFromCentroid,
|
|
Bearing: 0, // Could calculate bearing from centroid if needed
|
|
})
|
|
}
|
|
|
|
// Convert map to slice
|
|
for _, cluster := range clusterMap {
|
|
clusters = append(clusters, *cluster)
|
|
}
|
|
|
|
return clusters, nil
|
|
}
|
|
|
|
// Geographical Feature Methods
|
|
|
|
// FindNearbyGeographicalFeatures finds geographical features within radius
|
|
func (gs *GeospatialService) FindNearbyGeographicalFeatures(ctx context.Context, featureType domain.GeographicalFeatureType, lat, lng, radiusKm float64) ([]*domain.GeographicalFeature, error) {
|
|
switch featureType {
|
|
case domain.GeographicalFeatureTypeRoad:
|
|
return gs.geoFeatureRepo.GetRoadsWithinRadius(ctx, lat, lng, radiusKm)
|
|
case domain.GeographicalFeatureTypeGreenSpace:
|
|
return gs.geoFeatureRepo.GetGreenSpacesWithinRadius(ctx, lat, lng, radiusKm)
|
|
default:
|
|
// For other feature types, use general spatial query
|
|
return gs.findGeographicalFeaturesWithinRadius(ctx, featureType, lat, lng, radiusKm)
|
|
}
|
|
}
|
|
|
|
// findGeographicalFeaturesWithinRadius is a helper for general feature type queries
|
|
func (gs *GeospatialService) findGeographicalFeaturesWithinRadius(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 := gs.db.WithContext(ctx).Raw(query, featureType, lng, lat, radiusKm, lng, lat).Scan(&features)
|
|
if result.Error != nil {
|
|
return nil, result.Error
|
|
}
|
|
|
|
return features, nil
|
|
}
|
|
|
|
// CalculateSiteEnvironmentalScore calculates environmental score based on nearby green spaces
|
|
func (gs *GeospatialService) CalculateSiteEnvironmentalScore(ctx context.Context, siteLat, siteLng float64) (float64, error) {
|
|
// Get green spaces within 2km
|
|
greenSpaces, err := gs.geoFeatureRepo.GetGreenSpacesWithinRadius(ctx, siteLat, siteLng, 2.0)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Calculate score based on proximity and size of green spaces
|
|
var totalScore float64
|
|
for range greenSpaces {
|
|
// Calculate distance to green space (we'd need to add distance calculation)
|
|
// For now, use a simple scoring based on count
|
|
totalScore += 1.0
|
|
}
|
|
|
|
// Normalize score (max 10 points for environmental rating)
|
|
if totalScore > 10 {
|
|
totalScore = 10
|
|
}
|
|
|
|
return totalScore, nil
|
|
}
|
|
|
|
// CalculateTransportationAccessibility calculates accessibility score based on road network
|
|
func (gs *GeospatialService) CalculateTransportationAccessibility(ctx context.Context, siteLat, siteLng float64) (float64, error) {
|
|
// Get roads within 1km
|
|
roads, err := gs.geoFeatureRepo.GetRoadsWithinRadius(ctx, siteLat, siteLng, 1.0)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Calculate accessibility based on road density
|
|
roadCount := len(roads)
|
|
|
|
// Simple scoring: more roads = better accessibility
|
|
var score float64
|
|
switch {
|
|
case roadCount >= 10:
|
|
score = 10.0
|
|
case roadCount >= 5:
|
|
score = 7.5
|
|
case roadCount >= 2:
|
|
score = 5.0
|
|
case roadCount >= 1:
|
|
score = 2.5
|
|
default:
|
|
score = 0.0
|
|
}
|
|
|
|
return score, nil
|
|
}
|
|
|
|
// GetGeographicalFeatureStatistics returns comprehensive statistics about geographical features
|
|
func (gs *GeospatialService) GetGeographicalFeatureStatistics(ctx context.Context) (map[string]interface{}, error) {
|
|
stats := make(map[string]interface{})
|
|
|
|
// Get counts by feature type
|
|
featureTypes := []domain.GeographicalFeatureType{
|
|
domain.GeographicalFeatureTypeRoad,
|
|
domain.GeographicalFeatureTypeGreenSpace,
|
|
domain.GeographicalFeatureTypePOI,
|
|
domain.GeographicalFeatureTypeRailway,
|
|
domain.GeographicalFeatureTypeWater,
|
|
domain.GeographicalFeatureTypeLandUse,
|
|
}
|
|
|
|
for _, featureType := range featureTypes {
|
|
features, err := gs.geoFeatureRepo.GetByType(ctx, featureType)
|
|
if err == nil {
|
|
stats[string(featureType)] = map[string]interface{}{
|
|
"count": len(features),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get road network statistics
|
|
roadStats, err := gs.geoFeatureRepo.GetRoadNetworkStatistics(ctx)
|
|
if err == nil {
|
|
stats["road_network"] = roadStats
|
|
}
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
// FindOptimalFacilityLocations finds optimal locations for new facilities based on criteria
|
|
func (gs *GeospatialService) FindOptimalFacilityLocations(ctx context.Context, criteria FacilityLocationCriteria) ([]FacilityLocation, error) {
|
|
// This would be a complex algorithm considering:
|
|
// - Proximity to existing industrial sites
|
|
// - Access to road network
|
|
// - Distance from residential areas
|
|
// - Environmental constraints
|
|
// - Available utilities
|
|
|
|
// For now, return a placeholder implementation
|
|
var locations []FacilityLocation
|
|
|
|
// Query for areas with good road access and proximity to existing sites
|
|
query := `
|
|
WITH candidate_areas AS (
|
|
SELECT
|
|
ST_Buffer(s.location_geometry, 1000) as area,
|
|
s.id as nearby_site_id,
|
|
s.latitude,
|
|
s.longitude
|
|
FROM sites s
|
|
WHERE s.location_geometry IS NOT NULL
|
|
LIMIT 10
|
|
)
|
|
SELECT
|
|
ST_AsText(ST_Centroid(area)) as center_point,
|
|
COUNT(*) as nearby_sites,
|
|
ST_Y(ST_Centroid(area)) as lat,
|
|
ST_X(ST_Centroid(area)) as lng
|
|
FROM candidate_areas
|
|
GROUP BY area
|
|
HAVING COUNT(*) >= ?
|
|
LIMIT ?
|
|
`
|
|
|
|
rows, err := gs.db.WithContext(ctx).Raw(query, criteria.MinNearbySites, criteria.MaxResults).Rows()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var loc FacilityLocation
|
|
var centerPoint string
|
|
err := rows.Scan(¢erPoint, &loc.NearbySites, &loc.Latitude, &loc.Longitude)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Calculate scores for this location
|
|
envScore, _ := gs.CalculateSiteEnvironmentalScore(ctx, loc.Latitude, loc.Longitude)
|
|
transportScore, _ := gs.CalculateTransportationAccessibility(ctx, loc.Latitude, loc.Longitude)
|
|
|
|
loc.EnvironmentalScore = envScore
|
|
loc.TransportationScore = transportScore
|
|
loc.OverallScore = (envScore + transportScore) / 2.0
|
|
|
|
locations = append(locations, loc)
|
|
}
|
|
|
|
return locations, nil
|
|
}
|
|
|
|
// FacilityLocationCriteria defines criteria for optimal facility location search
|
|
type FacilityLocationCriteria struct {
|
|
MinNearbySites int `json:"min_nearby_sites"`
|
|
MaxDistanceKm float64 `json:"max_distance_km"`
|
|
RequireRoadAccess bool `json:"require_road_access"`
|
|
MinEnvironmentalScore float64 `json:"min_environmental_score"`
|
|
MaxResults int `json:"max_results"`
|
|
}
|
|
|
|
// FacilityLocation represents a potential facility location with scores
|
|
type FacilityLocation struct {
|
|
Latitude float64 `json:"latitude"`
|
|
Longitude float64 `json:"longitude"`
|
|
NearbySites int `json:"nearby_sites"`
|
|
EnvironmentalScore float64 `json:"environmental_score"`
|
|
TransportationScore float64 `json:"transportation_score"`
|
|
OverallScore float64 `json:"overall_score"`
|
|
}
|