diff --git a/pkg/util/locode/db/continent.go b/pkg/util/locode/db/continent.go new file mode 100644 index 000000000..3b05aa79b --- /dev/null +++ b/pkg/util/locode/db/continent.go @@ -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 + } +} diff --git a/pkg/util/locode/db/country.go b/pkg/util/locode/db/country.go new file mode 100644 index 000000000..d2679bf0b --- /dev/null +++ b/pkg/util/locode/db/country.go @@ -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[:]) +} diff --git a/pkg/util/locode/db/db.go b/pkg/util/locode/db/db.go new file mode 100644 index 000000000..1c47dba7d --- /dev/null +++ b/pkg/util/locode/db/db.go @@ -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) +} diff --git a/pkg/util/locode/db/location.go b/pkg/util/locode/db/location.go new file mode 100644 index 000000000..e353f3927 --- /dev/null +++ b/pkg/util/locode/db/location.go @@ -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[:]) +} diff --git a/pkg/util/locode/db/point.go b/pkg/util/locode/db/point.go new file mode 100644 index 000000000..d1371bf19 --- /dev/null +++ b/pkg/util/locode/db/point.go @@ -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 +} diff --git a/pkg/util/locode/db/record.go b/pkg/util/locode/db/record.go new file mode 100644 index 000000000..6fb9dea8c --- /dev/null +++ b/pkg/util/locode/db/record.go @@ -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 +}