[#316] locode: Define the API of location database

Define structure of keys and records of the location database. Define the
interfaces of all components necessary for the formation of the database.
Implement the function of filling the database.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
This commit is contained in:
Leonard Lyubich 2021-02-08 20:27:19 +03:00 committed by Leonard Lyubich
parent 0be35859ed
commit cdd1274e1c
6 changed files with 523 additions and 0 deletions

View file

@ -0,0 +1,81 @@
package locodedb
// Continent is an enumeration of Earth's continent.
type Continent uint8
const (
// ContinentUnknown is an undefined Continent value.
ContinentUnknown = iota
// ContinentEurope corresponds to Europe.
ContinentEurope
// ContinentAfrica corresponds to Africa.
ContinentAfrica
// ContinentNorthAmerica corresponds to North America.
ContinentNorthAmerica
// ContinentSouthAmerica corresponds to South America.
ContinentSouthAmerica
// ContinentAsia corresponds to Asia.
ContinentAsia
// ContinentAntarctica corresponds to Antarctica.
ContinentAntarctica
// ContinentOceania corresponds to Oceania.
ContinentOceania
)
// Is checks if c is the same continent as c2.
func (c *Continent) Is(c2 Continent) bool {
return *c == c2
}
func (c Continent) String() string {
switch c {
case ContinentUnknown:
fallthrough
default:
return "Unknown"
case ContinentEurope:
return "Europe"
case ContinentAfrica:
return "Africa"
case ContinentNorthAmerica:
return "North America"
case ContinentSouthAmerica:
return "South America"
case ContinentAsia:
return "Asia"
case ContinentAntarctica:
return "Antarctica"
case ContinentOceania:
return "Oceania"
}
}
// ContinentFromString returns Continent value
// corresponding to passed string representation.
func ContinentFromString(str string) Continent {
switch str {
default:
return ContinentUnknown
case "Europe":
return ContinentEurope
case "Africa":
return ContinentAfrica
case "North America":
return ContinentNorthAmerica
case "South America":
return ContinentSouthAmerica
case "Asia":
return ContinentAsia
case "Antarctica":
return ContinentAntarctica
case "Oceania":
return ContinentOceania
}
}

View file

@ -0,0 +1,31 @@
package locodedb
import (
locodecolumn "github.com/nspcc-dev/neofs-node/pkg/util/locode/column"
"github.com/pkg/errors"
)
// CountryCode represents country code for
// storage in the NeoFS location database.
type CountryCode locodecolumn.CountryCode
// CountryCodeFromString parses string UN/LOCODE country code
// and returns CountryCode.
func CountryCodeFromString(s string) (*CountryCode, error) {
cc, err := locodecolumn.CountryCodeFromString(s)
if err != nil {
return nil, errors.Wrap(err, "could not parse country code")
}
return CountryFromColumn(cc)
}
// CountryFromColumn converts UN/LOCODE country code to CountryCode.
func CountryFromColumn(cc *locodecolumn.CountryCode) (*CountryCode, error) {
return (*CountryCode)(cc), nil
}
func (c *CountryCode) String() string {
syms := (*locodecolumn.CountryCode)(c).Symbols()
return string(syms[:])
}

167
pkg/util/locode/db/db.go Normal file
View file

@ -0,0 +1,167 @@
package locodedb
import (
"github.com/nspcc-dev/neofs-node/pkg/util/locode"
"github.com/pkg/errors"
)
// SourceTable is an interface of the UN/LOCODE table.
type SourceTable interface {
// Must iterate over all entries of the table
// and pass next entry to the handler.
//
// Must return handler's errors directly.
IterateAll(func(locode.Record) error) error
}
// DB is an interface of NeoFS location database.
type DB interface {
// Must save the record by key in the database.
Put(Key, Record) error
// Must return the record by key from the database.
Get(Key) (*Record, error)
}
// AirportRecord represents the entry in NeoFS airport database.
type AirportRecord struct {
// Name of the country where airport is located.
CountryName string
// Geo point where airport is located.
Point *Point
}
// ErrAirportNotFound is returned by AirportRecord readers
// when the required airport is not found.
var ErrAirportNotFound = errors.New("airport not found")
// AirportDB is an interface of NeoFS airport database.
type AirportDB interface {
// Must return the record by UN/LOCODE table record.
//
// Must return ErrAirportNotFound if there is no
// related airport in the database.
Get(locode.Record) (*AirportRecord, error)
}
// ContinentsDB is an interface of NeoFS continent database.
type ContinentsDB interface {
// Must return continent of the geo point.
PointContinent(*Point) (*Continent, error)
}
var ErrSubDivNotFound = errors.New("subdivision not found")
var ErrCountryNotFound = errors.New("country not found")
// NamesDB is an interface of the NeoFS location namespace.
type NamesDB interface {
// Must resolve country code to country name.
//
// Must return ErrCountryNotFound if there is no
// country with provided code.
CountryName(*CountryCode) (string, error)
// Must resolve (country code, subdivision code) to
// subdivision name.
//
// Must return ErrSubDivNotFound if either country or
// subdivision is not presented in database.
SubDivName(*CountryCode, string) (string, error)
}
// FillDatabase generates the NeoFS location database based on the UN/LOCODE table.
func FillDatabase(table SourceTable, airports AirportDB, continents ContinentsDB, names NamesDB, db DB) error {
return table.IterateAll(func(tableRecord locode.Record) error {
if tableRecord.LOCODE.LocationCode() == "" {
return nil
}
dbKey, err := NewKey(tableRecord.LOCODE)
if err != nil {
return err
}
dbRecord, err := NewRecord(tableRecord)
if err != nil {
if errors.Is(err, errParseCoordinates) {
return nil
}
return err
}
geoPoint := dbRecord.GeoPoint()
countryName := ""
if geoPoint == nil {
airportRecord, err := airports.Get(tableRecord)
if err != nil {
if errors.Is(err, ErrAirportNotFound) {
return nil
}
return err
}
geoPoint = airportRecord.Point
countryName = airportRecord.CountryName
}
dbRecord.SetGeoPoint(geoPoint)
if countryName == "" {
countryName, err = names.CountryName(dbKey.CountryCode())
if err != nil {
if errors.Is(err, ErrCountryNotFound) {
return nil
}
return err
}
}
dbRecord.SetCountryName(countryName)
if subDivCode := dbRecord.SubDivCode(); subDivCode != "" {
subDivName, err := names.SubDivName(dbKey.CountryCode(), subDivCode)
if err != nil {
if errors.Is(err, ErrSubDivNotFound) {
return nil
}
return err
}
dbRecord.SetSubDivName(subDivName)
}
continent, err := continents.PointContinent(geoPoint)
if err != nil {
return errors.Wrap(err, "could not calculate continent geo point")
} else if continent.Is(ContinentUnknown) {
return nil
}
dbRecord.SetContinent(continent)
return db.Put(*dbKey, *dbRecord)
})
}
// LocodeRecord returns record from the NeoFS location database
// corresponding to string representation of UN/LOCODE.
func LocodeRecord(db DB, sLocode string) (*Record, error) {
lc, err := locode.FromString(sLocode)
if err != nil {
return nil, errors.Wrap(err, "could not parse locode")
}
key, err := NewKey(*lc)
if err != nil {
return nil, err
}
return db.Get(*key)
}

View file

@ -0,0 +1,31 @@
package locodedb
import (
locodecolumn "github.com/nspcc-dev/neofs-node/pkg/util/locode/column"
"github.com/pkg/errors"
)
// LocationCode represents location code for
// storage in the NeoFS location database.
type LocationCode locodecolumn.LocationCode
// LocationCodeFromString parses string UN/LOCODE location code
// and returns LocationCode.
func LocationCodeFromString(s string) (*LocationCode, error) {
lc, err := locodecolumn.LocationCodeFromString(s)
if err != nil {
return nil, errors.Wrap(err, "could not parse location code")
}
return LocationFromColumn(lc)
}
// LocationFromColumn converts UN/LOCODE country code to LocationCode.
func LocationFromColumn(cc *locodecolumn.LocationCode) (*LocationCode, error) {
return (*LocationCode)(cc), nil
}
func (l *LocationCode) String() string {
syms := (*locodecolumn.LocationCode)(l).Symbols()
return string(syms[:])
}

View file

@ -0,0 +1,75 @@
package locodedb
import (
"strconv"
"strings"
locodecolumn "github.com/nspcc-dev/neofs-node/pkg/util/locode/column"
)
// Point represents 2D geographic point.
type Point struct {
lat, lng float64
}
// NewPoint creates, initializes and returns new Point.
func NewPoint(lat, lng float64) *Point {
return &Point{
lat: lat,
lng: lng,
}
}
// Latitude returns Point's latitude.
func (p Point) Latitude() float64 {
return p.lat
}
// Longitude returns Point's longitude.
func (p Point) Longitude() float64 {
return p.lng
}
// PointFromCoordinates converts UN/LOCODE coordinates to Point.
func PointFromCoordinates(crd *locodecolumn.Coordinates) (*Point, error) {
if crd == nil {
return nil, nil
}
cLat := crd.Latitude()
cLatDeg := cLat.Degrees()
cLatMnt := cLat.Minutes()
lat, err := strconv.ParseFloat(strings.Join([]string{
string(cLatDeg[:]),
string(cLatMnt[:]),
}, "."), 64)
if err != nil {
return nil, err
}
if !cLat.Hemisphere().North() {
lat = -lat
}
cLng := crd.Longitude()
cLngDeg := cLng.Degrees()
cLngMnt := cLng.Minutes()
lng, err := strconv.ParseFloat(strings.Join([]string{
string(cLngDeg[:]),
string(cLngMnt[:]),
}, "."), 64)
if err != nil {
return nil, err
}
if !cLng.Hemisphere().East() {
lng = -lng
}
return &Point{
lat: lat,
lng: lng,
}, nil
}

View file

@ -0,0 +1,138 @@
package locodedb
import (
"github.com/nspcc-dev/neofs-node/pkg/util/locode"
locodecolumn "github.com/nspcc-dev/neofs-node/pkg/util/locode/column"
"github.com/pkg/errors"
)
// Key represents the key in NeoFS location database.
type Key struct {
cc *CountryCode
lc *LocationCode
}
// NewKey calculates Key from LOCODE.
func NewKey(lc locode.LOCODE) (*Key, error) {
country, err := CountryCodeFromString(lc.CountryCode())
if err != nil {
return nil, errors.Wrap(err, "could not parse country")
}
location, err := LocationCodeFromString(lc.LocationCode())
if err != nil {
return nil, errors.Wrap(err, "could not parse location")
}
return &Key{
cc: country,
lc: location,
}, nil
}
// CountryCode returns location's country code.
func (k *Key) CountryCode() *CountryCode {
return k.cc
}
// LocationCode returns location code.
func (k *Key) LocationCode() *LocationCode {
return k.lc
}
// Record represents the entry in NeoFS location database.
type Record struct {
countryName string
cityName string
subDivName string
subDivCode string
p *Point
cont *Continent
}
var errParseCoordinates = errors.New("invalid coordinates")
// NewRecord calculates Record from UN/LOCODE table record.
func NewRecord(r locode.Record) (*Record, error) {
crd, err := locodecolumn.CoordinatesFromString(r.Coordinates)
if err != nil {
return nil, errors.Wrap(errParseCoordinates, err.Error())
}
point, err := PointFromCoordinates(crd)
if err != nil {
return nil, errors.Wrap(err, "could not parse geo point")
}
return &Record{
cityName: r.NameWoDiacritics,
subDivCode: r.SubDiv,
p: point,
}, nil
}
// CountryName returns country name.
func (r *Record) CountryName() string {
return r.countryName
}
// SetCountryName sets country name.
func (r *Record) SetCountryName(name string) {
r.countryName = name
}
// CityName returns city name.
func (r *Record) CityName() string {
return r.cityName
}
// SetCityName sets city name.
func (r *Record) SetCityName(name string) {
r.cityName = name
}
// SubDivCode returns subdivision code.
func (r *Record) SubDivCode() string {
return r.subDivCode
}
// SetSubDivCode sets subdivision code.
func (r *Record) SetSubDivCode(name string) {
r.subDivCode = name
}
// SubDivName returns subdivision name.
func (r *Record) SubDivName() string {
return r.subDivName
}
// SetSubDivName sets subdivision name.
func (r *Record) SetSubDivName(name string) {
r.subDivName = name
}
// GeoPoint returns geo point of the location.
func (r *Record) GeoPoint() *Point {
return r.p
}
// SetGeoPoint sets geo point of the location.
func (r *Record) SetGeoPoint(p *Point) {
r.p = p
}
// Continent returns location continent.
func (r *Record) Continent() *Continent {
return r.cont
}
// SetContinent sets location continent.
func (r *Record) SetContinent(c *Continent) {
r.cont = c
}