mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
298 lines
8.2 KiB
Go
298 lines
8.2 KiB
Go
package service
|
|
|
|
import (
|
|
"bugulma/backend/internal/domain"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
)
|
|
|
|
// GeocodingService provides geocoding capabilities using Google Maps API
|
|
type GeocodingService struct {
|
|
apiKey string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// GeocodeResponse represents the response from Google Geocoding API
|
|
type GeocodeResponse struct {
|
|
Results []GeocodeResult `json:"results"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// GeocodeResult represents a single geocoding result
|
|
type GeocodeResult struct {
|
|
AddressComponents []AddressComponent `json:"address_components"`
|
|
FormattedAddress string `json:"formatted_address"`
|
|
Geometry Geometry `json:"geometry"`
|
|
PlaceID string `json:"place_id"`
|
|
Types []string `json:"types"`
|
|
}
|
|
|
|
// AddressComponent represents an address component
|
|
type AddressComponent struct {
|
|
LongName string `json:"long_name"`
|
|
ShortName string `json:"short_name"`
|
|
Types []string `json:"types"`
|
|
}
|
|
|
|
// Geometry represents the geometry of a geocoding result
|
|
type Geometry struct {
|
|
Location Location `json:"location"`
|
|
LocationType string `json:"location_type"`
|
|
Viewport Bounds `json:"viewport"`
|
|
Bounds Bounds `json:"bounds"`
|
|
}
|
|
|
|
// Location represents lat/lng coordinates
|
|
type Location struct {
|
|
Lat float64 `json:"lat"`
|
|
Lng float64 `json:"lng"`
|
|
}
|
|
|
|
// Bounds represents a bounding box
|
|
type Bounds struct {
|
|
Northeast Location `json:"northeast"`
|
|
Southwest Location `json:"southwest"`
|
|
}
|
|
|
|
// GeocodeResult represents the result of geocoding
|
|
type GeocodeCoordinates struct {
|
|
Latitude float64
|
|
Longitude float64
|
|
Address string
|
|
PlaceID string
|
|
}
|
|
|
|
// NewGeocodingService creates a new geocoding service
|
|
func NewGeocodingService(apiKey string) *GeocodingService {
|
|
return &GeocodingService{
|
|
apiKey: apiKey,
|
|
httpClient: &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
// GeocodeAddress geocodes an address string to coordinates
|
|
func (s *GeocodingService) GeocodeAddress(ctx context.Context, address string) (*GeocodeCoordinates, error) {
|
|
if s.apiKey == "" {
|
|
return nil, fmt.Errorf("google maps API key not configured")
|
|
}
|
|
|
|
if address == "" {
|
|
return nil, fmt.Errorf("address cannot be empty")
|
|
}
|
|
|
|
// Build request URL
|
|
baseURL := "https://maps.googleapis.com/maps/api/geocode/json"
|
|
params := url.Values{}
|
|
params.Add("address", address)
|
|
params.Add("key", s.apiKey)
|
|
params.Add("language", "ru") // Russian language for results
|
|
|
|
reqURL := fmt.Sprintf("%s?%s", baseURL, params.Encode())
|
|
|
|
// Create request
|
|
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
// Make request
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to make request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("geocoding API returned status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
// Parse response
|
|
var geocodeResp GeocodeResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&geocodeResp); err != nil {
|
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
|
|
if geocodeResp.Status != "OK" && geocodeResp.Status != "ZERO_RESULTS" {
|
|
return nil, fmt.Errorf("geocoding API error: %s", geocodeResp.Status)
|
|
}
|
|
|
|
if len(geocodeResp.Results) == 0 {
|
|
return nil, fmt.Errorf("no results found for address: %s", address)
|
|
}
|
|
|
|
// Use first result
|
|
result := geocodeResp.Results[0]
|
|
return &GeocodeCoordinates{
|
|
Latitude: result.Geometry.Location.Lat,
|
|
Longitude: result.Geometry.Location.Lng,
|
|
Address: result.FormattedAddress,
|
|
PlaceID: result.PlaceID,
|
|
}, nil
|
|
}
|
|
|
|
// ReverseGeocode performs reverse geocoding (coordinates to address)
|
|
func (s *GeocodingService) ReverseGeocode(ctx context.Context, lat, lng float64) (*GeocodeCoordinates, error) {
|
|
if s.apiKey == "" {
|
|
return nil, fmt.Errorf("google maps API key not configured")
|
|
}
|
|
|
|
// Build request URL
|
|
baseURL := "https://maps.googleapis.com/maps/api/geocode/json"
|
|
params := url.Values{}
|
|
params.Add("latlng", fmt.Sprintf("%f,%f", lat, lng))
|
|
params.Add("key", s.apiKey)
|
|
params.Add("language", "ru") // Russian language for results
|
|
|
|
reqURL := fmt.Sprintf("%s?%s", baseURL, params.Encode())
|
|
|
|
// Create request
|
|
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
// Make request
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to make request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("reverse geocoding API returned status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
// Parse response
|
|
var geocodeResp GeocodeResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&geocodeResp); err != nil {
|
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
|
|
if geocodeResp.Status != "OK" && geocodeResp.Status != "ZERO_RESULTS" {
|
|
return nil, fmt.Errorf("reverse geocoding API error: %s", geocodeResp.Status)
|
|
}
|
|
|
|
if len(geocodeResp.Results) == 0 {
|
|
return nil, fmt.Errorf("no results found for coordinates: %f,%f", lat, lng)
|
|
}
|
|
|
|
// Use first result
|
|
result := geocodeResp.Results[0]
|
|
return &GeocodeCoordinates{
|
|
Latitude: lat,
|
|
Longitude: lng,
|
|
Address: result.FormattedAddress,
|
|
PlaceID: result.PlaceID,
|
|
}, nil
|
|
}
|
|
|
|
// EnrichAddressWithCoordinates enriches an address with coordinates if missing
|
|
func (s *GeocodingService) EnrichAddressWithCoordinates(ctx context.Context, address *domain.Address) error {
|
|
// Skip if already has coordinates
|
|
if address.Latitude != 0 && address.Longitude != 0 {
|
|
return nil
|
|
}
|
|
|
|
// Build address string from components
|
|
addressStr := address.FormattedRu
|
|
if addressStr == "" {
|
|
addressStr = address.FormattedEn
|
|
}
|
|
if addressStr == "" {
|
|
// Build from components
|
|
parts := []string{}
|
|
if address.Street != "" {
|
|
parts = append(parts, address.Street)
|
|
}
|
|
if address.City != "" {
|
|
parts = append(parts, address.City)
|
|
}
|
|
if address.Region != "" {
|
|
parts = append(parts, address.Region)
|
|
}
|
|
if len(parts) == 0 {
|
|
return fmt.Errorf("address has no usable components for geocoding")
|
|
}
|
|
addressStr = fmt.Sprintf("%s, Россия", fmt.Sprintf("%s", parts[0]))
|
|
if len(parts) > 1 {
|
|
addressStr = fmt.Sprintf("%s, %s, Россия", parts[0], parts[1])
|
|
}
|
|
}
|
|
|
|
// Geocode
|
|
coords, err := s.GeocodeAddress(ctx, addressStr)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to geocode address: %w", err)
|
|
}
|
|
|
|
// Update address
|
|
address.Latitude = coords.Latitude
|
|
address.Longitude = coords.Longitude
|
|
if address.FormattedRu == "" && coords.Address != "" {
|
|
address.FormattedRu = coords.Address
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// EnrichOrganizationWithCoordinates enriches an organization with coordinates from its addresses
|
|
func (s *GeocodingService) EnrichOrganizationWithCoordinates(ctx context.Context, org *domain.Organization) error {
|
|
// Skip if already has coordinates
|
|
if org.Latitude != 0 && org.Longitude != 0 {
|
|
return nil
|
|
}
|
|
|
|
// Try to get coordinates from primary address
|
|
if len(org.Addresses) > 0 {
|
|
// Find headquarters address first
|
|
for i := range org.Addresses {
|
|
addr := &org.Addresses[i]
|
|
if addr.AddressType == domain.AddressTypeHeadquarters {
|
|
if addr.Latitude != 0 && addr.Longitude != 0 {
|
|
org.Latitude = addr.Latitude
|
|
org.Longitude = addr.Longitude
|
|
return nil
|
|
}
|
|
// Try to geocode it
|
|
if err := s.EnrichAddressWithCoordinates(ctx, addr); err == nil {
|
|
org.Latitude = addr.Latitude
|
|
org.Longitude = addr.Longitude
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to first address with coordinates
|
|
for i := range org.Addresses {
|
|
addr := &org.Addresses[i]
|
|
if addr.Latitude != 0 && addr.Longitude != 0 {
|
|
org.Latitude = addr.Latitude
|
|
org.Longitude = addr.Longitude
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Try to geocode first address
|
|
if len(org.Addresses) > 0 {
|
|
addr := &org.Addresses[0]
|
|
if err := s.EnrichAddressWithCoordinates(ctx, addr); err == nil {
|
|
org.Latitude = addr.Latitude
|
|
org.Longitude = addr.Longitude
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("no addresses available for organization %s", org.ID)
|
|
}
|
|
|