[#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:
Leonard Lyubich 2021-02-08 20:53:34 +03:00 committed by Leonard Lyubich
parent 6829048124
commit d8c3a0e4f5
3 changed files with 281 additions and 0 deletions

View 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
}

View 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,
},
}
}

View 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
}
}