turash/bugulma/backend/internal/geospatial/bounding_box.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

141 lines
4.0 KiB
Go

package geospatial
import (
"math"
)
// BoundingBoxCalculatorImpl implements BoundingBoxCalculator interface
type BoundingBoxCalculatorImpl struct {
config *Config
}
// NewBoundingBoxCalculator creates a new bounding box calculator
func NewBoundingBoxCalculator(config *Config) BoundingBoxCalculator {
return &BoundingBoxCalculatorImpl{
config: config,
}
}
// CalculateBoundingBox calculates the bounding box that contains all given points
func (bbc *BoundingBoxCalculatorImpl) CalculateBoundingBox(points []Point) (*BoundingBox, error) {
if len(points) == 0 {
return nil, ErrEmptyPointList
}
minLat := points[0].Latitude
maxLat := points[0].Latitude
minLon := points[0].Longitude
maxLon := points[0].Longitude
for _, point := range points {
if err := validatePoint(point); err != nil {
return nil, err
}
if point.Latitude < minLat {
minLat = point.Latitude
}
if point.Latitude > maxLat {
maxLat = point.Latitude
}
if point.Longitude < minLon {
minLon = point.Longitude
}
if point.Longitude > maxLon {
maxLon = point.Longitude
}
}
return &BoundingBox{
NorthEast: Point{
Latitude: maxLat,
Longitude: maxLon,
},
SouthWest: Point{
Latitude: minLat,
Longitude: minLon,
},
}, nil
}
// ExpandBoundingBox expands a bounding box by a specified distance in kilometers
func (bbc *BoundingBoxCalculatorImpl) ExpandBoundingBox(bbox BoundingBox, expansionKm float64) (*BoundingBox, error) {
if err := bbc.ValidateBoundingBox(bbox); err != nil {
return nil, err
}
// Convert expansion from km to approximate degrees
// At equator: 1 degree latitude ≈ 111 km
// Longitude varies by latitude: 1 degree ≈ 111 km * cos(latitude)
latExpansion := expansionKm / 111.0
avgLat := (bbox.NorthEast.Latitude + bbox.SouthWest.Latitude) / 2
lonExpansion := expansionKm / (111.0 * math.Cos(avgLat*math.Pi/180))
expanded := BoundingBox{
NorthEast: Point{
Latitude: math.Min(bbox.NorthEast.Latitude+latExpansion, 90),
Longitude: math.Min(bbox.NorthEast.Longitude+lonExpansion, 180),
},
SouthWest: Point{
Latitude: math.Max(bbox.SouthWest.Latitude-latExpansion, -90),
Longitude: math.Max(bbox.SouthWest.Longitude-lonExpansion, -180),
},
}
return &expanded, nil
}
// IsPointInBoundingBox checks if a point is within a bounding box
func (bbc *BoundingBoxCalculatorImpl) IsPointInBoundingBox(point Point, bbox BoundingBox) bool {
if err := validatePoint(point); err != nil {
return false
}
if err := bbc.ValidateBoundingBox(bbox); err != nil {
return false
}
return point.Latitude >= bbox.SouthWest.Latitude &&
point.Latitude <= bbox.NorthEast.Latitude &&
point.Longitude >= bbox.SouthWest.Longitude &&
point.Longitude <= bbox.NorthEast.Longitude
}
// BoundingBoxArea calculates the area of a bounding box in square kilometers
func (bbc *BoundingBoxCalculatorImpl) BoundingBoxArea(bbox BoundingBox) (float64, error) {
if err := bbc.ValidateBoundingBox(bbox); err != nil {
return 0, err
}
// Calculate area using spherical approximation
latDiff := (bbox.NorthEast.Latitude - bbox.SouthWest.Latitude) * math.Pi / 180
avgLat := (bbox.NorthEast.Latitude + bbox.SouthWest.Latitude) / 2 * math.Pi / 180
lonDiff := (bbox.NorthEast.Longitude - bbox.SouthWest.Longitude) * math.Pi / 180
// Area = R² * lat_diff * lon_diff * cos(avg_lat)
area := bbc.config.EarthRadiusKm * bbc.config.EarthRadiusKm * latDiff * lonDiff * math.Cos(avgLat)
return math.Abs(area), nil
}
// ValidateBoundingBox validates a bounding box
func (bbc *BoundingBoxCalculatorImpl) ValidateBoundingBox(bbox BoundingBox) error {
if err := validatePoint(bbox.NorthEast); err != nil {
return err
}
if err := validatePoint(bbox.SouthWest); err != nil {
return err
}
if bbox.NorthEast.Latitude < bbox.SouthWest.Latitude {
return ErrInvalidBoundingBox
}
// Handle longitude wrap-around (e.g., -180 to 180)
if bbox.NorthEast.Longitude < bbox.SouthWest.Longitude {
// This is valid if the box crosses the date line
// For simplicity, we'll allow it but note it requires special handling
}
return nil
}