[#316] locode: Implement polygons DB based on GeoJSON polygons

Define database of Earth's polygons in GeoJSON format. Implement resolving
of geo point to continent in which it is located through point-in-polygon
calculation.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
This commit is contained in:
Leonard Lyubich 2021-02-08 21:11:51 +03:00 committed by Leonard Lyubich
parent d8c3a0e4f5
commit 9669afdfc7
5 changed files with 165 additions and 1 deletions

3
go.mod
View file

@ -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

BIN
go.sum

Binary file not shown.

View file

@ -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
}
}

View file

@ -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,
}
}

View file

@ -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{}
}