diff --git a/go.mod b/go.mod index b385b76e..a8540b8d 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/nspcc-dev/neofs-crypto v0.3.0 github.com/nspcc-dev/tzhash v1.4.0 github.com/panjf2000/ants/v2 v2.3.0 + github.com/paulmach/orb v0.2.1 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.6.0 github.com/spf13/cobra v1.0.0 @@ -36,7 +37,7 @@ require ( golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5 // indirect golang.org/x/tools v0.0.0-20200123022218-593de606220b // indirect google.golang.org/grpc v1.29.1 - google.golang.org/protobuf v1.23.0 + google.golang.org/protobuf v1.25.0 ) // Used for debug reasons diff --git a/go.sum b/go.sum index 244dd631..533160e5 100644 Binary files a/go.sum and b/go.sum differ diff --git a/pkg/util/locode/db/continents/geojson/calls.go b/pkg/util/locode/db/continents/geojson/calls.go new file mode 100644 index 00000000..7a7edb56 --- /dev/null +++ b/pkg/util/locode/db/continents/geojson/calls.go @@ -0,0 +1,90 @@ +package continentsdb + +import ( + "io/ioutil" + + locodedb "github.com/nspcc-dev/neofs-node/pkg/util/locode/db" + "github.com/paulmach/orb" + "github.com/paulmach/orb/geojson" + "github.com/paulmach/orb/planar" + "github.com/pkg/errors" +) + +const continentProperty = "Continent" + +// PointContinent goes through all polygons, and returns continent +// in which point is located. +// +// Returns locodedb.ContinentUnknown if no entry matches. +// +// All GeoJSON feature are parsed from file once and stored in memory. +func (db *DB) PointContinent(point *locodedb.Point) (*locodedb.Continent, error) { + var err error + + db.once.Do(func() { + err = db.init() + }) + + if err != nil { + return nil, err + } + + planarPoint := orb.Point{point.Longitude(), point.Latitude()} + + var continent string + + for _, feature := range db.features { + if multiPolygon, ok := feature.Geometry.(orb.MultiPolygon); ok { + if planar.MultiPolygonContains(multiPolygon, planarPoint) { + continent = feature.Properties.MustString(continentProperty) + break + } + } else if polygon, ok := feature.Geometry.(orb.Polygon); ok { + if planar.PolygonContains(polygon, planarPoint) { + continent = feature.Properties.MustString(continentProperty) + break + } + } + } + + c := continentFromString(continent) + + return &c, nil +} + +func (db *DB) init() error { + data, err := ioutil.ReadFile(db.path) + if err != nil { + return errors.Wrap(err, "could not read data file") + } + + features, err := geojson.UnmarshalFeatureCollection(data) + if err != nil { + return errors.Wrap(err, "could not unmarshal GeoJSON feature collection") + } + + db.features = features.Features + + return nil +} + +func continentFromString(c string) locodedb.Continent { + switch c { + default: + return locodedb.ContinentUnknown + case "Africa": + return locodedb.ContinentAfrica + case "Asia": + return locodedb.ContinentAsia + case "Europe": + return locodedb.ContinentEurope + case "North America": + return locodedb.ContinentNorthAmerica + case "South America": + return locodedb.ContinentSouthAmerica + case "Antarctica": + return locodedb.ContinentAntarctica + case "Australia", "Oceania": + return locodedb.ContinentOceania + } +} diff --git a/pkg/util/locode/db/continents/geojson/db.go b/pkg/util/locode/db/continents/geojson/db.go new file mode 100644 index 00000000..a5b8b76a --- /dev/null +++ b/pkg/util/locode/db/continents/geojson/db.go @@ -0,0 +1,63 @@ +package continentsdb + +import ( + "fmt" + "sync" + + "github.com/paulmach/orb/geojson" +) + +// 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 polygons of Earth's continents in GeoJSON format. + // + // Must not be empty. + Path string +} + +// DB is a descriptor of the Earth's polygons in GeoJSON 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 { + path string + + once sync.Once + + features []*geojson.Feature +} + +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.Path == "": + panicOnPrmValue("Path", prm.Path) + } + + o := defaultOpts() + + for i := range opts { + opts[i](o) + } + + return &DB{ + path: prm.Path, + } +} diff --git a/pkg/util/locode/db/continents/geojson/opts.go b/pkg/util/locode/db/continents/geojson/opts.go new file mode 100644 index 00000000..59831fcc --- /dev/null +++ b/pkg/util/locode/db/continents/geojson/opts.go @@ -0,0 +1,10 @@ +package continentsdb + +// Option sets an optional parameter of DB. +type Option func(*options) + +type options struct{} + +func defaultOpts() *options { + return &options{} +}