turash/bugulma/backend/internal/geospatial/calculator.go
Damir Mukimov 000eab4740
Major repository reorganization and missing backend endpoints implementation
Repository Structure:
- Move files from cluttered root directory into organized structure
- Create archive/ for archived data and scraper results
- Create bugulma/ for the complete application (frontend + backend)
- Create data/ for sample datasets and reference materials
- Create docs/ for comprehensive documentation structure
- Create scripts/ for utility scripts and API tools

Backend Implementation:
- Implement 3 missing backend endpoints identified in gap analysis:
  * GET /api/v1/organizations/{id}/matching/direct - Direct symbiosis matches
  * GET /api/v1/users/me/organizations - User organizations
  * POST /api/v1/proposals/{id}/status - Update proposal status
- Add complete proposal domain model, repository, and service layers
- Create database migration for proposals table
- Fix CLI server command registration issue

API Documentation:
- Add comprehensive proposals.md API documentation
- Update README.md with Users and Proposals API sections
- Document all request/response formats, error codes, and business rules

Code Quality:
- Follow existing Go backend architecture patterns
- Add proper error handling and validation
- Match frontend expected response schemas
- Maintain clean separation of concerns (handler -> service -> repository)
2025-11-25 06:01:16 +01:00

176 lines
4.9 KiB
Go

package geospatial
import (
"fmt"
)
// GeospatialCalculator implements the main Calculator interface
type GeospatialCalculator struct {
config *Config
distanceCalc DistanceCalculator
bearingCalc BearingCalculator
boundingBoxCalc BoundingBoxCalculator
coordinateTransformer CoordinateTransformer
validator SpatialValidator
}
// NewGeospatialCalculator creates a new geospatial calculator with all dependencies
func NewGeospatialCalculator(
config *Config,
distanceCalc DistanceCalculator,
bearingCalc BearingCalculator,
boundingBoxCalc BoundingBoxCalculator,
coordinateTransformer CoordinateTransformer,
validator SpatialValidator,
) Calculator {
return &GeospatialCalculator{
config: config,
distanceCalc: distanceCalc,
bearingCalc: bearingCalc,
boundingBoxCalc: boundingBoxCalc,
coordinateTransformer: coordinateTransformer,
validator: validator,
}
}
// CalculateDistance calculates distance between two points
func (gc *GeospatialCalculator) CalculateDistance(p1, p2 Point) (DistanceResult, error) {
var distanceKm float64
var err error
switch gc.config.DefaultDistanceMethod {
case "vincenty":
if vincentyCalc, ok := gc.distanceCalc.(*DistanceCalculatorImpl); ok {
distanceKm, err = vincentyCalc.VincentyDistance(p1, p2)
} else {
distanceKm, err = gc.distanceCalc.HaversineDistance(p1, p2)
}
default:
distanceKm, err = gc.distanceCalc.HaversineDistance(p1, p2)
}
if err != nil {
return DistanceResult{}, err
}
bearingDeg, err := gc.bearingCalc.CalculateBearing(p1, p2)
if err != nil {
return DistanceResult{}, err
}
return DistanceResult{
DistanceKm: distanceKm,
DistanceMeters: distanceKm * 1000,
Bearing: bearingDeg,
BearingRadians: bearingDeg * 3.14159265359 / 180,
}, nil
}
// CalculateDistanceMatrix calculates distances between multiple points
func (gc *GeospatialCalculator) CalculateDistanceMatrix(points []Point) (*DistanceMatrix, error) {
if len(points) == 0 {
return nil, ErrEmptyPointList
}
if len(points) > gc.config.MaxDistanceMatrixSize {
return nil, ErrMatrixTooLarge
}
matrix := make([][]float64, len(points))
for i := range matrix {
matrix[i] = make([]float64, len(points))
}
for i := 0; i < len(points); i++ {
for j := 0; j < len(points); j++ {
if i == j {
matrix[i][j] = 0
} else {
distance, err := gc.distanceCalc.HaversineDistance(points[i], points[j])
if err != nil {
return nil, fmt.Errorf("failed to calculate distance between points %d and %d: %w", i, j, err)
}
matrix[i][j] = distance
}
}
}
return &DistanceMatrix{
Points: points,
Distances: matrix,
}, nil
}
// CalculateBearing calculates bearing between two points
func (gc *GeospatialCalculator) CalculateBearing(from, to Point) (float64, error) {
return gc.bearingCalc.CalculateBearing(from, to)
}
// CalculateBoundingBox calculates bounding box for points
func (gc *GeospatialCalculator) CalculateBoundingBox(points []Point) (*BoundingBox, error) {
return gc.boundingBoxCalc.CalculateBoundingBox(points)
}
// IsPointInBoundingBox checks if point is in bounding box
func (gc *GeospatialCalculator) IsPointInBoundingBox(point Point, bbox BoundingBox) bool {
return gc.boundingBoxCalc.IsPointInBoundingBox(point, bbox)
}
// CalculateRoute calculates a route through multiple points
func (gc *GeospatialCalculator) CalculateRoute(points []Point) (*Route, error) {
if len(points) < 2 {
return nil, ErrInvalidRoute
}
var totalDistance float64
var segments []RouteSegment
for i := 0; i < len(points)-1; i++ {
distanceResult, err := gc.CalculateDistance(points[i], points[i+1])
if err != nil {
return nil, fmt.Errorf("failed to calculate segment %d: %w", i, err)
}
totalDistance += distanceResult.DistanceKm
// Estimate time if average speed is configured
var timeMinutes *int
if gc.config.AverageSpeedKmh > 0 {
minutes := int((distanceResult.DistanceKm / gc.config.AverageSpeedKmh) * 60)
timeMinutes = &minutes
}
segments = append(segments, RouteSegment{
From: points[i],
To: points[i+1],
DistanceKm: distanceResult.DistanceKm,
Bearing: distanceResult.Bearing,
TimeMinutes: timeMinutes,
})
}
// Calculate total time
var totalTimeMinutes *int
if gc.config.AverageSpeedKmh > 0 {
totalMinutes := int((totalDistance / gc.config.AverageSpeedKmh) * 60)
totalTimeMinutes = &totalMinutes
}
return &Route{
Points: points,
TotalDistanceKm: totalDistance,
Segments: segments,
EstimatedTimeMinutes: totalTimeMinutes,
}, nil
}
// ValidatePoint validates a point
func (gc *GeospatialCalculator) ValidatePoint(point Point) error {
return gc.validator.ValidatePoint(point)
}
// ValidateBoundingBox validates a bounding box
func (gc *GeospatialCalculator) ValidateBoundingBox(bbox BoundingBox) error {
return gc.validator.ValidateBoundingBox(bbox)
}