[#316] locode: Implement csv OpenFlights airport database
Implement airport database based on csv OpenFlights table. Implement UN/LOCODE entry matcher. Implement country namespace. Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
This commit is contained in:
parent
6829048124
commit
d8c3a0e4f5
3 changed files with 281 additions and 0 deletions
186
pkg/util/locode/db/airports/calls.go
Normal file
186
pkg/util/locode/db/airports/calls.go
Normal file
|
@ -0,0 +1,186 @@
|
|||
package airportsdb
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/nspcc-dev/neofs-node/pkg/util/locode"
|
||||
locodedb "github.com/nspcc-dev/neofs-node/pkg/util/locode/db"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
_ = iota - 1
|
||||
|
||||
_ // Airport ID
|
||||
_ // Name
|
||||
airportCity
|
||||
airportCountry
|
||||
airportIATA
|
||||
_ // ICAO
|
||||
airportLatitude
|
||||
airportLongitude
|
||||
_ // Altitude
|
||||
_ // Timezone
|
||||
_ // DST
|
||||
_ // Tz database time zone
|
||||
_ // Type
|
||||
_ // Source
|
||||
|
||||
airportFldNum
|
||||
)
|
||||
|
||||
type record struct {
|
||||
city,
|
||||
country,
|
||||
iata,
|
||||
lat,
|
||||
lng string
|
||||
}
|
||||
|
||||
// Get scans records of the OpenFlights Airport table one-by-one, and
|
||||
// returns entry that matches passed UN/LOCODE record.
|
||||
//
|
||||
// Records are matched if they have the same country code and either
|
||||
// same IATA code or same city name (location name in UN/LOCODE).
|
||||
//
|
||||
// Returns locodedb.ErrAirportNotFound if no entry matches.
|
||||
func (db *DB) Get(locodeRecord locode.Record) (rec *locodedb.AirportRecord, err error) {
|
||||
err = db.scanWords(db.airports, airportFldNum, func(words []string) error {
|
||||
airportRecord := record{
|
||||
city: words[airportCity],
|
||||
country: words[airportCountry],
|
||||
iata: words[airportIATA],
|
||||
lat: words[airportLatitude],
|
||||
lng: words[airportLongitude],
|
||||
}
|
||||
|
||||
if related, err := db.isRelated(locodeRecord, airportRecord); err != nil || !related {
|
||||
return err
|
||||
}
|
||||
|
||||
lat, err := strconv.ParseFloat(airportRecord.lat, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lng, err := strconv.ParseFloat(airportRecord.lng, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rec = &locodedb.AirportRecord{
|
||||
CountryName: airportRecord.country,
|
||||
Point: locodedb.NewPoint(lat, lng),
|
||||
}
|
||||
|
||||
return errScanInt
|
||||
})
|
||||
|
||||
if err == nil && rec == nil {
|
||||
err = locodedb.ErrAirportNotFound
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) isRelated(locodeRecord locode.Record, airportRecord record) (bool, error) {
|
||||
countryCode, err := db.countryCode(airportRecord.country)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "could not read country code of airport")
|
||||
}
|
||||
|
||||
sameCountry := locodeRecord.LOCODE.CountryCode() == countryCode
|
||||
sameIATA := locodeRecord.LOCODE.LocationCode() == airportRecord.iata
|
||||
|
||||
if sameCountry && sameIATA {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
sameCity := locodeRecord.Name == airportRecord.city
|
||||
|
||||
return sameCountry && sameCity, nil
|
||||
}
|
||||
|
||||
const (
|
||||
_ = iota - 1
|
||||
|
||||
countryName
|
||||
countryISOCode
|
||||
_ // dafif_code
|
||||
|
||||
countryFldNum
|
||||
)
|
||||
|
||||
// CountryName scans records of the OpenFlights Country table, and returns
|
||||
// name of the country by code.
|
||||
//
|
||||
// Returns locodedb.ErrCountryNotFound if no entry matches.
|
||||
func (db *DB) CountryName(code *locodedb.CountryCode) (name string, err error) {
|
||||
err = db.scanWords(db.countries, countryFldNum, func(words []string) error {
|
||||
if words[countryISOCode] == code.String() {
|
||||
name = words[countryName]
|
||||
return errScanInt
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil && name == "" {
|
||||
err = locodedb.ErrCountryNotFound
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) countryCode(country string) (code string, err error) {
|
||||
err = db.scanWords(db.countries, countryFldNum, func(words []string) error {
|
||||
if words[countryName] == country {
|
||||
code = words[countryISOCode]
|
||||
return errScanInt
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var errScanInt = errors.New("interrupt scan")
|
||||
|
||||
func (db *DB) scanWords(pm pathMode, num int, wordsHandler func([]string) error) error {
|
||||
tableFile, err := os.OpenFile(pm.path, os.O_RDONLY, pm.mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer tableFile.Close()
|
||||
|
||||
r := csv.NewReader(tableFile)
|
||||
r.ReuseRecord = true
|
||||
|
||||
for {
|
||||
words, err := r.Read()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
|
||||
return err
|
||||
} else if ln := len(words); ln != num {
|
||||
return errors.Errorf("unexpected number of words %d", ln)
|
||||
}
|
||||
|
||||
if err := wordsHandler(words); err != nil {
|
||||
if errors.Is(err, errScanInt) {
|
||||
break
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
76
pkg/util/locode/db/airports/db.go
Normal file
76
pkg/util/locode/db/airports/db.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package airportsdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Prm groups the required parameters of the DB's constructor.
|
||||
//
|
||||
// All values must comply with the requirements imposed on them.
|
||||
// Passing incorrect parameter values will result in constructor
|
||||
// failure (error or panic depending on the implementation).
|
||||
type Prm struct {
|
||||
// Path to OpenFlights Airport csv table.
|
||||
//
|
||||
// Must not be empty.
|
||||
AirportsPath string
|
||||
|
||||
// Path to OpenFlights Countries csv table.
|
||||
//
|
||||
// Must not be empty.
|
||||
CountriesPath string
|
||||
}
|
||||
|
||||
// DB is a descriptor of the OpenFlights database in csv format.
|
||||
//
|
||||
// For correct operation, DB must be created
|
||||
// using the constructor (New) based on the required parameters
|
||||
// and optional components. After successful creation,
|
||||
// The DB is immediately ready to work through API.
|
||||
type DB struct {
|
||||
airports, countries pathMode
|
||||
}
|
||||
|
||||
type pathMode struct {
|
||||
path string
|
||||
mode os.FileMode
|
||||
}
|
||||
|
||||
const invalidPrmValFmt = "invalid parameter %s (%T):%v"
|
||||
|
||||
func panicOnPrmValue(n string, v interface{}) {
|
||||
panic(fmt.Sprintf(invalidPrmValFmt, n, v, v))
|
||||
}
|
||||
|
||||
// New creates a new instance of the DB.
|
||||
//
|
||||
// Panics if at least one value of the parameters is invalid.
|
||||
//
|
||||
// The created DB does not require additional
|
||||
// initialization and is completely ready for work.
|
||||
func New(prm Prm, opts ...Option) *DB {
|
||||
switch {
|
||||
case prm.AirportsPath == "":
|
||||
panicOnPrmValue("AirportsPath", prm.AirportsPath)
|
||||
case prm.CountriesPath == "":
|
||||
panicOnPrmValue("CountriesPath", prm.CountriesPath)
|
||||
}
|
||||
|
||||
o := defaultOpts()
|
||||
|
||||
for i := range opts {
|
||||
opts[i](o)
|
||||
}
|
||||
|
||||
return &DB{
|
||||
airports: pathMode{
|
||||
path: prm.AirportsPath,
|
||||
mode: o.airportMode,
|
||||
},
|
||||
countries: pathMode{
|
||||
path: prm.CountriesPath,
|
||||
mode: o.countryMode,
|
||||
},
|
||||
}
|
||||
}
|
19
pkg/util/locode/db/airports/opts.go
Normal file
19
pkg/util/locode/db/airports/opts.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package airportsdb
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// Option sets an optional parameter of DB.
|
||||
type Option func(*options)
|
||||
|
||||
type options struct {
|
||||
airportMode, countryMode os.FileMode
|
||||
}
|
||||
|
||||
func defaultOpts() *options {
|
||||
return &options{
|
||||
airportMode: os.ModePerm, // 0777
|
||||
countryMode: os.ModePerm, // 0777
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue