From 9669afdfc770be818618ee55e53572fb661223d3 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 8 Feb 2021 21:11:51 +0300 Subject: [PATCH] [#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 --- go.mod | 3 +- go.sum | Bin 59137 -> 60763 bytes .../locode/db/continents/geojson/calls.go | 90 ++++++++++++++++++ pkg/util/locode/db/continents/geojson/db.go | 63 ++++++++++++ pkg/util/locode/db/continents/geojson/opts.go | 10 ++ 5 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 pkg/util/locode/db/continents/geojson/calls.go create mode 100644 pkg/util/locode/db/continents/geojson/db.go create mode 100644 pkg/util/locode/db/continents/geojson/opts.go 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 244dd631f893c0332047b0c50ec30a8b36477e43..533160e595bd911cd5eeb4d7c403118a3b45d1b6 100644 GIT binary patch delta 1070 zcmZ{jO{?Qn9LBkG9UMl+nNbU(i@74GP}-c6J$xaab2qRg!aHR`Zv$--1b1#S=z>WI>Tc(SOx;mTx`JLbY;W=MEJ-&W%{KWX= z5iEf+2_O#v$&dhX0F}^`GYyg((=K0%qIVkx$@|$8_)h*cnBHa&0V+2Q7Oe<-YZQ#4b_2Qz zy%OWIMX=U-#Kpye=~ZJ+o%uSq17v`HGyfC)_1b9(d%?*9lu;S%M2Mczl7#VyWCVdy zlsEzBOBaG#fQu`85T?GT>A)Y=1ZJ6i@cG*r;NLhx`8TIO6>5_x!uABuPZqFDmchYx zgwWE3;-=Mf7~oX6txO~KRw)71>JaNOo%Wh?aaJRn0A#;+3VFNx#fOiL)b$K(6Mkgw z{Kzug`E>(cO|?LE&o27&J7*Z%CQ`f9it{tcg@$7i zAzm7N2a0AWj9@f}e8izvTb*)}Ay{^54YP+=A9em0c92=yRP0HuHk4%6p`6vGLB%sO zRL-ihcDY*D^^xh;sfil57yg!6$R0g^J%9S__AQ?MM>j%qJKga*tEgwNb!VekQn5b~ zrW-jDhcHmuY(p0m&M*h_J?2h+_58)HBQiU>`7|qBJ;=Vl{%(KX-~Yc?{*Xs-8wLl* zCe0aRz+;>p-<^k1F&Hzv*ait2$SD61+!r>W(Bh#H}V9txjb(bvw~85BeVZ@&``5H d$So3+P~8W!Q`-dsv##CR0+Zt4kh7QK#0q-nA;16t 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{} +}