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 }