124 lines
3.4 KiB
Go
124 lines
3.4 KiB
Go
package geo
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"git.leaktechnologies.dev/Leak_Technologies/Skyfeed/internal/config"
|
|
)
|
|
|
|
const (
|
|
// WORKING GeoGratis endpoint for all populated places (PPL)
|
|
gnbcAPIURL = "https://geogratis.gc.ca/services/geoname/en/geonames.json?feature_code=PPL"
|
|
townsFile = "towns.json"
|
|
maxFetchTime = 5 * time.Minute
|
|
)
|
|
|
|
// EnsureTownIndexUpToDate checks if the towns index needs updating (monthly).
|
|
func EnsureTownIndexUpToDate() error {
|
|
dest := filepath.Join(config.DataDir, townsFile)
|
|
|
|
info, err := os.Stat(dest)
|
|
if os.IsNotExist(err) {
|
|
fmt.Println("[geo] No town index found, downloading...")
|
|
return downloadTownIndex(dest)
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("unable to check town index: %w", err)
|
|
}
|
|
|
|
modTime := info.ModTime().UTC()
|
|
now := time.Now().UTC()
|
|
firstOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
if modTime.Before(firstOfMonth) {
|
|
fmt.Println("[geo] Town index is older than this month, refreshing...")
|
|
return downloadTownIndex(dest)
|
|
}
|
|
|
|
fmt.Println("[geo] Town index is current.")
|
|
return nil
|
|
}
|
|
|
|
// ForceUpdateTownIndex forces an immediate rebuild.
|
|
func ForceUpdateTownIndex() error {
|
|
dest := filepath.Join(config.DataDir, townsFile)
|
|
fmt.Println("[geo] Forcing town index update...")
|
|
return downloadTownIndex(dest)
|
|
}
|
|
|
|
func downloadTownIndex(dest string) error {
|
|
client := &http.Client{Timeout: maxFetchTime}
|
|
|
|
fmt.Println("[geo] Fetching town data from GeoGratis...")
|
|
resp, err := client.Get(gnbcAPIURL)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to fetch town dataset: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("unexpected HTTP status: %s", resp.Status)
|
|
}
|
|
|
|
raw, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read GeoGratis response: %w", err)
|
|
}
|
|
|
|
towns, err := parseGNBCJSON(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse GeoGratis JSON: %w", err)
|
|
}
|
|
|
|
data, err := json.MarshalIndent(towns, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to encode towns.json: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(dest, data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write %s: %w", dest, err)
|
|
}
|
|
|
|
fmt.Printf("[geo] Town index updated → %s (%d towns)\n", dest, len(towns))
|
|
return nil
|
|
}
|
|
|
|
// Uses GeoGratis geonames.json structure
|
|
func parseGNBCJSON(data []byte) ([]Town, error) {
|
|
var response struct {
|
|
Items []struct {
|
|
Name string `json:"name"`
|
|
ProvinceCode string `json:"province_code"`
|
|
Latitude float64 `json:"latitude"`
|
|
Longitude float64 `json:"longitude"`
|
|
} `json:"items"`
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &response); err != nil {
|
|
return nil, fmt.Errorf("invalid GeoGratis JSON: %w", err)
|
|
}
|
|
|
|
var towns []Town
|
|
for _, item := range response.Items {
|
|
if item.Name == "" || item.ProvinceCode == "" {
|
|
continue
|
|
}
|
|
|
|
towns = append(towns, Town{
|
|
Name: item.Name,
|
|
Province: item.ProvinceCode,
|
|
Lat: item.Latitude,
|
|
Lon: item.Longitude,
|
|
})
|
|
}
|
|
|
|
return towns, nil
|
|
}
|