Files
s01e02/internal/domain/location.go
2026-03-12 02:10:57 +01:00

181 lines
4.5 KiB
Go

package domain
import (
"context"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"time"
)
// Location represents a location with coordinates
type Location struct {
Name string `json:"name"`
Latitude float64 `json:"lat"`
Longitude float64 `json:"lon"`
}
// PersonLocation represents a person's location
type PersonLocation struct {
Name string `json:"name"`
Surname string `json:"surname"`
Latitude float64 `json:"lat"`
Longitude float64 `json:"lon"`
}
// DistanceResult represents the distance calculation result
type DistanceResult struct {
Person string `json:"person"`
Location string `json:"location"`
DistanceKm float64 `json:"distance_km"`
}
// Haversine calculates the distance between two points on Earth (in kilometers)
// using the Haversine formula
func Haversine(lat1, lon1, lat2, lon2 float64) float64 {
const earthRadiusKm = 6371.0
// Convert degrees to radians
lat1Rad := lat1 * math.Pi / 180
lat2Rad := lat2 * math.Pi / 180
deltaLat := (lat2 - lat1) * math.Pi / 180
deltaLon := (lon2 - lon1) * math.Pi / 180
// Haversine formula
a := math.Sin(deltaLat/2)*math.Sin(deltaLat/2) +
math.Cos(lat1Rad)*math.Cos(lat2Rad)*
math.Sin(deltaLon/2)*math.Sin(deltaLon/2)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return earthRadiusKm * c
}
// CalculateDistance calculates distance between person and location
func CalculateDistance(person PersonLocation, location Location) DistanceResult {
distance := Haversine(person.Latitude, person.Longitude, location.Latitude, location.Longitude)
return DistanceResult{
Person: person.Name + " " + person.Surname,
Location: location.Name,
DistanceKm: distance,
}
}
// FindClosestLocation finds the closest location to a person
func FindClosestLocation(person PersonLocation, locations []Location) *DistanceResult {
if len(locations) == 0 {
return nil
}
var closest *DistanceResult
minDistance := math.MaxFloat64
for _, loc := range locations {
result := CalculateDistance(person, loc)
if result.DistanceKm < minDistance {
minDistance = result.DistanceKm
closest = &result
}
}
return closest
}
// NominatimResponse represents OpenStreetMap Nominatim API response
type NominatimResponse struct {
Lat string `json:"lat"`
Lon string `json:"lon"`
}
// GetPlantGeolocation fetches accurate coordinates for a city using geocoding API
func GetPlantGeolocation(ctx context.Context, cityName string) (float64, float64, error) {
url := fmt.Sprintf("https://nominatim.openstreetmap.org/search?city=%s&country=Poland&format=json&limit=1", cityName)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return 0, 0, fmt.Errorf("creating request: %w", err)
}
// OpenStreetMap requires User-Agent header
req.Header.Set("User-Agent", "PersonProcessor/1.0")
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return 0, 0, fmt.Errorf("making request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, 0, fmt.Errorf("reading response: %w", err)
}
var results []NominatimResponse
if err := json.Unmarshal(body, &results); err != nil {
return 0, 0, fmt.Errorf("parsing JSON: %w", err)
}
if len(results) == 0 {
return 0, 0, fmt.Errorf("no results found for city: %s", cityName)
}
// Parse coordinates
var lat, lon float64
if _, err := fmt.Sscanf(results[0].Lat, "%f", &lat); err != nil {
return 0, 0, fmt.Errorf("parsing latitude: %w", err)
}
if _, err := fmt.Sscanf(results[0].Lon, "%f", &lon); err != nil {
return 0, 0, fmt.Errorf("parsing longitude: %w", err)
}
return lat, lon, nil
}
// NearestPointResult represents result of finding nearest point
type NearestPointResult struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
DistanceKm float64 `json:"distance_km"`
Index int `json:"index"`
}
// FindNearestPoint finds the nearest point from a list to a reference point
func FindNearestPoint(referenceLat, referenceLon float64, points [][]float64) *NearestPointResult {
if len(points) == 0 {
return nil
}
var nearest *NearestPointResult
minDistance := math.MaxFloat64
for i, point := range points {
if len(point) < 2 {
continue // Skip invalid points
}
lat := point[0]
lon := point[1]
distance := Haversine(referenceLat, referenceLon, lat, lon)
if distance < minDistance {
minDistance = distance
nearest = &NearestPointResult{
Latitude: lat,
Longitude: lon,
DistanceKm: distance,
Index: i,
}
}
}
return nearest
}