From d8c3a0e4f58acef3bf8ff3dd0946d2c7da639cb0 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 8 Feb 2021 20:53:34 +0300 Subject: [PATCH] [#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 --- pkg/util/locode/db/airports/calls.go | 186 +++++++++++++++++++++++++++ pkg/util/locode/db/airports/db.go | 76 +++++++++++ pkg/util/locode/db/airports/opts.go | 19 +++ 3 files changed, 281 insertions(+) create mode 100644 pkg/util/locode/db/airports/calls.go create mode 100644 pkg/util/locode/db/airports/db.go create mode 100644 pkg/util/locode/db/airports/opts.go diff --git a/pkg/util/locode/db/airports/calls.go b/pkg/util/locode/db/airports/calls.go new file mode 100644 index 00000000..f908f9dc --- /dev/null +++ b/pkg/util/locode/db/airports/calls.go @@ -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 +} diff --git a/pkg/util/locode/db/airports/db.go b/pkg/util/locode/db/airports/db.go new file mode 100644 index 00000000..046d9aac --- /dev/null +++ b/pkg/util/locode/db/airports/db.go @@ -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, + }, + } +} diff --git a/pkg/util/locode/db/airports/opts.go b/pkg/util/locode/db/airports/opts.go new file mode 100644 index 00000000..1f5a3ea6 --- /dev/null +++ b/pkg/util/locode/db/airports/opts.go @@ -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 + } +}