package placement import ( "fmt" "math" "strings" "sync" "git.frostfs.info/TrueCloudLab/frostfs-locode-db/pkg/locode" locodedb "git.frostfs.info/TrueCloudLab/frostfs-locode-db/pkg/locode/db" locodebolt "git.frostfs.info/TrueCloudLab/frostfs-locode-db/pkg/locode/db/boltdb" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" ) const ( attrPrefix = "$attribute:" geoDistance = "$geoDistance" ) type Metric interface { CalculateValue(*netmap.NodeInfo, *netmap.NodeInfo) (int, error) } func ParseMetric(raw string, locodeDB *locodebolt.DB, nodeLocode string) (Metric, error) { if attr, found := strings.CutPrefix(raw, attrPrefix); found { return NewAttributeMetric(attr), nil } else if raw == geoDistance && len(nodeLocode) > 0 && locodeDB != nil { return NewGeoDistanceMetric(locodeDB, nodeLocode) } return nil, fmt.Errorf("unsupported priority metric %s", raw) } // attributeMetric describes priority metric based on attribute. type attributeMetric struct { attribute string } // CalculateValue return [0] if from and to contains attribute attributeMetric.attribute and // the value of attribute is the same. In other case return [1]. func (am *attributeMetric) CalculateValue(from *netmap.NodeInfo, to *netmap.NodeInfo) (int, error) { fromAttr := from.Attribute(am.attribute) toAttr := to.Attribute(am.attribute) if len(fromAttr) > 0 && len(toAttr) > 0 && fromAttr == toAttr { return 0, nil } return 1, nil } func NewAttributeMetric(attr string) Metric { return &attributeMetric{attribute: attr} } // geoDistanceMetric describes priority metric based on attribute. type geoDistanceMetric struct { locodeDB *locodebolt.DB distance map[string]int mtx sync.Mutex lat float64 long float64 } func NewGeoDistanceMetric(locodeDB *locodebolt.DB, nodeLocode string) (Metric, error) { point, err := getPoint(locodeDB, nodeLocode) if err != nil { return nil, fmt.Errorf("geo point for locode %s: %w", nodeLocode, err) } gm := &geoDistanceMetric{ locodeDB: locodeDB, lat: point.Latitude(), long: point.Longitude(), distance: make(map[string]int), } return gm, nil } // CalculateValue return [0] if from and to contains attribute attributeMetric.attribute and // the value of attribute is the same. In other case return [1]. func (gm *geoDistanceMetric) CalculateValue(_ *netmap.NodeInfo, to *netmap.NodeInfo) (int, error) { tl := to.LOCODE() if v, ok := gm.distance[tl]; ok { return v, nil } return gm.calculateDistance(tl) } func (gm *geoDistanceMetric) calculateDistance(to string) (int, error) { gm.mtx.Lock() defer gm.mtx.Unlock() if v, ok := gm.distance[to]; ok { return v, nil } pointTo, err := getPoint(gm.locodeDB, to) if err != nil { return 0, fmt.Errorf("geo point for locode %s: %w", to, err) } gm.distance[to] = int(distance(gm.lat, gm.long, pointTo.Latitude(), pointTo.Longitude())) return gm.distance[to], nil } func getPoint(db *locodebolt.DB, raw string) (*locodedb.Point, error) { lc, err := locode.FromString(raw) if err != nil { return nil, fmt.Errorf("invalid locode value: %w", err) } key, err := locodedb.NewKey(*lc) if err != nil { return nil, fmt.Errorf("create key from locode: %w", err) } record, err := db.Get(*key) if err != nil { return nil, fmt.Errorf("could not get locode record from DB: %w", err) } return record.GeoPoint(), nil } // distance return amount of KM between two points. // Parameters are latitude and longitude of point 1 and 2 in decimal degrees. func distance(lt1 float64, ln1 float64, lt2 float64, ln2 float64) float64 { radLat1 := math.Pi * lt1 / 180 radLat2 := math.Pi * lt2 / 180 radTheta := math.Pi * (ln1 - ln2) / 180 dist := math.Sin(radLat1)*math.Sin(radLat2) + math.Cos(radLat1)*math.Cos(radLat2)*math.Cos(radTheta) if dist > 1 { dist = 1 } dist = math.Acos(dist) dist = dist * 180 / math.Pi dist = dist * 60 * 1.1515 * 1.609344 return dist }