mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-22 22:21:27 +00:00
193 lines
4.6 KiB
Go
193 lines
4.6 KiB
Go
// Copyright 2026 Beacon Contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
// gen generates internal/iatadb/db.go from the OurAirports airports.csv dataset.
|
|
//
|
|
// Usage (from repo root):
|
|
//
|
|
// go run ./internal/iatadb/gen
|
|
//
|
|
// The source CSV is read from internal/iatadb/gen/airports.csv by default.
|
|
// To use a different file, set the AIRPORTS_CSV environment variable.
|
|
// To fetch a fresh copy from OurAirports, delete airports.csv first:
|
|
//
|
|
// rm internal/iatadb/gen/airports.csv && go run ./internal/iatadb/gen
|
|
//
|
|
// The upstream dataset is published at:
|
|
//
|
|
// https://raw.githubusercontent.com/davidmegginson/ourairports-data/main/airports.csv
|
|
package main
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
sourceURL = "https://raw.githubusercontent.com/davidmegginson/ourairports-data/main/airports.csv"
|
|
localCSV = "internal/iatadb/gen/airports.csv"
|
|
outputPath = "internal/iatadb/db.go"
|
|
|
|
colContinent = 7
|
|
colCountry = 8
|
|
colIATACode = 13
|
|
)
|
|
|
|
type entry struct {
|
|
IATA string
|
|
Country string
|
|
Continent string
|
|
}
|
|
|
|
func main() {
|
|
// ensure we run from the repo root
|
|
if _, err := os.Stat("go.mod"); err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: run from repo root (go.mod not found)\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
var r io.Reader
|
|
|
|
csvPath := os.Getenv("AIRPORTS_CSV")
|
|
if csvPath == "" {
|
|
csvPath = localCSV
|
|
}
|
|
|
|
if _, err := os.Stat(csvPath); err == nil {
|
|
f, err := os.Open(csvPath)
|
|
if err != nil {
|
|
log.Fatalf("open %s: %v", csvPath, err)
|
|
}
|
|
defer f.Close()
|
|
r = f
|
|
log.Printf("gen: reading from %s", csvPath)
|
|
} else {
|
|
log.Printf("gen: %s not found, fetching from %s", csvPath, sourceURL)
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Get(sourceURL)
|
|
if err != nil {
|
|
log.Fatalf("fetch: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
log.Fatalf("fetch: HTTP %d", resp.StatusCode)
|
|
}
|
|
// save locally for future runs
|
|
if err := saveLocal(resp.Body, csvPath); err != nil {
|
|
log.Printf("gen: warning: could not save local copy: %v", err)
|
|
}
|
|
f, err := os.Open(csvPath)
|
|
if err != nil {
|
|
log.Fatalf("re-open %s: %v", csvPath, err)
|
|
}
|
|
defer f.Close()
|
|
r = f
|
|
}
|
|
|
|
cr := csv.NewReader(r)
|
|
cr.LazyQuotes = true
|
|
|
|
if _, err := cr.Read(); err != nil { // skip header
|
|
log.Fatalf("read header: %v", err)
|
|
}
|
|
|
|
var entries []entry
|
|
for {
|
|
row, err := cr.Read()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
log.Printf("gen: skipping malformed row: %v", err)
|
|
continue
|
|
}
|
|
if len(row) <= colIATACode {
|
|
continue
|
|
}
|
|
iata := strings.TrimSpace(row[colIATACode])
|
|
if len(iata) != 3 {
|
|
continue
|
|
}
|
|
country := strings.TrimSpace(row[colCountry])
|
|
continent := strings.TrimSpace(row[colContinent])
|
|
if country == "" || continent == "" {
|
|
continue
|
|
}
|
|
entries = append(entries, entry{IATA: iata, Country: country, Continent: continent})
|
|
}
|
|
|
|
sort.Slice(entries, func(i, j int) bool { return entries[i].IATA < entries[j].IATA })
|
|
|
|
// deduplicate
|
|
seen := make(map[string]struct{})
|
|
unique := entries[:0]
|
|
for _, e := range entries {
|
|
if _, ok := seen[e.IATA]; !ok {
|
|
seen[e.IATA] = struct{}{}
|
|
unique = append(unique, e)
|
|
}
|
|
}
|
|
entries = unique
|
|
|
|
log.Printf("gen: %d valid IATA codes", len(entries))
|
|
|
|
out, err := os.Create(outputPath)
|
|
if err != nil {
|
|
log.Fatalf("create %s: %v", outputPath, err)
|
|
}
|
|
defer out.Close()
|
|
|
|
if err := tmpl.Execute(out, struct {
|
|
Source string
|
|
Entries []entry
|
|
}{Source: sourceURL, Entries: entries}); err != nil {
|
|
log.Fatalf("render: %v", err)
|
|
}
|
|
log.Printf("gen: wrote %s", outputPath)
|
|
}
|
|
|
|
func saveLocal(r io.Reader, path string) error {
|
|
data, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, data, 0644)
|
|
}
|
|
|
|
var tmpl = template.Must(template.New("").Parse(`// Code generated by internal/iatadb/gen; DO NOT EDIT.
|
|
// Source: {{ .Source }}
|
|
// To regenerate: go run ./internal/iatadb/gen
|
|
|
|
package iatadb
|
|
|
|
// Entry holds geographic metadata for a single IATA airport code.
|
|
type Entry struct {
|
|
Country string // ISO 3166-1 alpha-2 country code e.g. "CA"
|
|
Continent string // two-letter continent code: AF, AN, AS, EU, NA, OC, SA
|
|
}
|
|
|
|
// DB maps 3-letter IATA codes to geographic metadata.
|
|
// Generated from the OurAirports dataset; run go generate to refresh.
|
|
var DB = map[string]Entry{
|
|
{{ range .Entries }} "{{ .IATA }}": {Country: "{{ .Country }}", Continent: "{{ .Continent }}"},
|
|
{{ end }}}
|
|
|
|
// CountryContinent maps ISO 3166-1 alpha-2 country codes to continent codes.
|
|
// Derived from the same OurAirports dataset as DB.
|
|
var CountryContinent = func() map[string]string {
|
|
m := make(map[string]string, 250)
|
|
for _, e := range DB {
|
|
m[e.Country] = e.Continent
|
|
}
|
|
return m
|
|
}()
|
|
`))
|