Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions internal/hub/metadata_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -698,22 +698,23 @@ func fenceMatchesFloor(fence gen.Fence, location gen.Location) bool {

func fenceBoundingRect(fence gen.Fence) (rtreego.Rect, bool) {
if p, err := fence.Region.AsPoint(); err == nil {
center, err := point2D(p)
if err != nil {
return rtreego.Rect{}, false
}
radius := 0.0
if fence.Radius != nil {
radius = float64(*fence.Radius)
}
rect, err := rtreego.NewRect(
rtreego.Point{center[0] - radius, center[1] - radius},
[]float64{radius * 2, radius * 2},
)
if err != nil {
return rtreego.Rect{}, false
if p.Type == "Point" {
center, err := point2D(p)
if err == nil {
radius := 0.0
if fence.Radius != nil {
radius = float64(*fence.Radius)
}
rect, err := rtreego.NewRect(
rtreego.Point{center[0] - radius, center[1] - radius},
[]float64{radius * 2, radius * 2},
)
if err != nil {
return rtreego.Rect{}, false
}
return rect, true
}
}
return rect, true
}
polygon, err := fence.Region.AsPolygon()
if err != nil || len(polygon.Coordinates) == 0 || len(polygon.Coordinates[0]) == 0 {
Expand Down
140 changes: 140 additions & 0 deletions internal/hub/metadata_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,146 @@ func TestMetadataCacheFenceCandidatesMatchLocationFloor(t *testing.T) {
}
}

func TestMetadataCacheFenceCandidatesMatchWGS84PolygonFence(t *testing.T) {
t.Parallel()

fenceID, err := uuid.Parse("019eab42-3d91-7b8a-8b64-1ca8e2c6055d")
if err != nil {
t.Fatalf("parse fence id failed: %v", err)
}
fence := productionAreaFenceFixture(t, fenceID)

cache, err := NewMetadataCache(context.Background(), fakeQueries{
listZonesFn: metadataZoneList(t),
listFencesFn: metadataFenceList(t, fence),
listTrackablesFn: metadataTrackableList(t),
listProvidersFn: metadataProviderList(t),
})
if err != nil {
t.Fatalf("NewMetadataCache failed: %v", err)
}

location := testLocationWithCoordinates(t, stringPtrValueRef("EPSG:4326"), "simulated-tag-1", [2]float32{8.90318, 52.01776})
scopeKey, ok, err := cache.current().locationFenceScopeKey(location)
if err != nil {
t.Fatalf("locationFenceScopeKey failed: %v", err)
}
if !ok {
t.Fatal("expected location fence scope key")
}
fenceKey, ok := fenceScopeKey(fence)
if !ok {
t.Fatal("expected fence scope key")
}
if scopeKey != fenceKey {
t.Fatalf("scope mismatch: location=%s fence=%s", scopeKey, fenceKey)
}
rect, ok := fenceBoundingRect(fence)
if !ok {
t.Fatal("expected fence bounding rect")
}
point, err := point2D(location.Position)
if err != nil {
t.Fatalf("point2D failed: %v", err)
}
minX := rect.PointCoord(0)
minY := rect.PointCoord(1)
maxX := minX + rect.LengthsCoord(0)
maxY := minY + rect.LengthsCoord(1)
if point[0] < minX || point[0] > maxX || point[1] < minY || point[1] > maxY {
t.Fatalf("point %v outside rect [%f,%f]-[%f,%f]", point, minX, minY, maxX, maxY)
}
candidates, err := cache.FenceCandidates(location)
if err != nil {
t.Fatalf("FenceCandidates failed: %v", err)
}
if len(candidates) != 1 || candidates[0].Id != fence.Id {
t.Fatalf("unexpected WGS84 polygon fence candidates: %+v", candidates)
}
}

func TestMetadataCacheFenceCandidatesSupportPointAndPolygonAcrossScopes(t *testing.T) {
t.Parallel()

zone := testZoneWithForeignID(t, uuid.New(), "uwb", "zone-a", [2]float32{0, 0}, nil, nil)
localCRS := "local"
wgsCRS := "EPSG:4326"

localPointFence := testPointFence(t, uuid.New(), [2]float32{5, 5}, 2)
localPointFence.Crs = &localCRS
localPointFence.ZoneId = stringPtrValueRef(zone.Id.String())

localPolygonFence := testPolygonFence(t, uuid.New(), [][2]float32{
{4, 4}, {6, 4}, {6, 6}, {4, 6}, {4, 4},
})
localPolygonFence.Crs = &localCRS
localPolygonFence.ZoneId = stringPtrValueRef(zone.Id.String())

wgsPointFence := testPointFence(t, uuid.New(), [2]float32{8.5411, 47.3744}, 0.0002)
wgsPointFence.Crs = &wgsCRS

wgsPolygonFence := testPolygonFence(t, uuid.New(), [][2]float32{
{8.5410, 47.3743}, {8.5412, 47.3743}, {8.5412, 47.3745}, {8.5410, 47.3745}, {8.5410, 47.3743},
})
wgsPolygonFence.Crs = &wgsCRS

cache, err := NewMetadataCache(context.Background(), fakeQueries{
listZonesFn: metadataZoneList(t, zone),
listFencesFn: metadataFenceList(t, localPointFence, localPolygonFence, wgsPointFence, wgsPolygonFence),
listTrackablesFn: metadataTrackableList(t),
listProvidersFn: metadataProviderList(t),
})
if err != nil {
t.Fatalf("NewMetadataCache failed: %v", err)
}

tests := []struct {
name string
location gen.Location
expectedID [16]byte
}{
{
name: "local point",
location: testLocationWithCoordinates(t, &localCRS, zone.Id.String(), [2]float32{5, 5}),
expectedID: localPointFence.Id,
},
{
name: "local polygon",
location: testLocationWithCoordinates(t, &localCRS, zone.Id.String(), [2]float32{5, 5}),
expectedID: localPolygonFence.Id,
},
{
name: "wgs point",
location: testLocationWithCoordinates(t, &wgsCRS, "provider-a", [2]float32{8.5411, 47.3744}),
expectedID: wgsPointFence.Id,
},
{
name: "wgs polygon",
location: testLocationWithCoordinates(t, &wgsCRS, "provider-a", [2]float32{8.5411, 47.3744}),
expectedID: wgsPolygonFence.Id,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
candidates, err := cache.FenceCandidates(tc.location)
if err != nil {
t.Fatalf("FenceCandidates failed: %v", err)
}
found := false
for _, candidate := range candidates {
if candidate.Id == tc.expectedID {
found = true
break
}
}
if !found {
t.Fatalf("expected fence %s in candidates, got %+v", uuid.UUID(tc.expectedID).String(), candidates)
}
})
}
}

func TestMetadataCacheReconcileDiffsCreateUpdateDelete(t *testing.T) {
t.Parallel()

Expand Down
42 changes: 28 additions & 14 deletions internal/hub/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,9 @@ func (s *Service) processDerivedLocation(ctx context.Context, location gen.Locat
case ScopeLocal:
wgs84Location, ok, err := view.WGS84(ctx)
if err == nil && ok {
if err := s.publishFenceEvents(ctx, *wgs84Location); err != nil {
return err
}
if emit {
if err := s.publishLocationScope(ctx, *wgs84Location, ScopeEPSG4326); err != nil {
return err
Expand Down Expand Up @@ -1212,13 +1215,23 @@ func (s *Service) processDerivedLocation(ctx context.Context, location gen.Locat
if emit {
localLocation, ok, err := view.Local(ctx)
if err == nil && ok {
if err := s.publishFenceEvents(ctx, *localLocation); err != nil {
return err
}
if err := s.publishLocationScope(ctx, *localLocation, ScopeLocal); err != nil {
return err
}
if _, err := s.publishTrackableMotionsForLocation(ctx, *localLocation, ScopeLocal); err != nil {
return err
}
}
} else {
localLocation, ok, err := view.Local(ctx)
if err == nil && ok {
if err := s.publishFenceEvents(ctx, *localLocation); err != nil {
return err
}
}
}
}
return nil
Expand Down Expand Up @@ -2903,21 +2916,22 @@ func point2D(point gen.Point) ([2]float64, error) {

func fenceContainmentForPoint(fence gen.Fence, point [2]float64) (fenceContainment, error) {
if p, err := fence.Region.AsPoint(); err == nil {
center, err := point2D(p)
if err != nil {
return fenceContainment{}, err
}
radius := 0.0
if fence.Radius != nil {
radius = float64(*fence.Radius)
}
dx := point[0] - center[0]
dy := point[1] - center[1]
distance := math.Sqrt(dx*dx + dy*dy)
if distance <= radius {
return fenceContainment{Inside: true}, nil
if p.Type == "Point" {
center, err := point2D(p)
if err == nil {
radius := 0.0
if fence.Radius != nil {
radius = float64(*fence.Radius)
}
dx := point[0] - center[0]
dy := point[1] - center[1]
distance := math.Sqrt(dx*dx + dy*dy)
if distance <= radius {
return fenceContainment{Inside: true}, nil
}
return fenceContainment{OutsideDistance: distance - radius}, nil
}
}
return fenceContainment{OutsideDistance: distance - radius}, nil
}
polygon, err := fence.Region.AsPolygon()
if err != nil {
Expand Down
Loading