[#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:
parent
0be35859ed
commit
cdd1274e1c
6 changed files with 523 additions and 0 deletions
81
pkg/util/locode/db/continent.go
Normal file
81
pkg/util/locode/db/continent.go
Normal 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
|
||||
}
|
||||
}
|
31
pkg/util/locode/db/country.go
Normal file
31
pkg/util/locode/db/country.go
Normal 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
167
pkg/util/locode/db/db.go
Normal 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)
|
||||
}
|
31
pkg/util/locode/db/location.go
Normal file
31
pkg/util/locode/db/location.go
Normal 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[:])
|
||||
}
|
75
pkg/util/locode/db/point.go
Normal file
75
pkg/util/locode/db/point.go
Normal 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
|
||||
}
|
138
pkg/util/locode/db/record.go
Normal file
138
pkg/util/locode/db/record.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue