diff --git a/client/src/lib/mapservices/MapPopoverOverlay.ts b/client/src/lib/mapservices/MapPopoverOverlay.ts index b9fbe20..6b0d32b 100644 --- a/client/src/lib/mapservices/MapPopoverOverlay.ts +++ b/client/src/lib/mapservices/MapPopoverOverlay.ts @@ -1,6 +1,10 @@ import Overlay from 'ol/Overlay.js' +import type { Positioning } from 'ol/Overlay.js' import type { Map as OlMap } from 'ol' -import type { Coordinate } from 'ol/coordinate'; +import type { Coordinate } from 'ol/coordinate' + +const FLIP_MARGIN_X = 250 +const FLIP_MARGIN_Y = 200 /** * MapPopoverOverlay wraps an OpenLayers Overlay and manages drag-to-reposition interaction. @@ -34,9 +38,31 @@ class MapPopoverOverlay { } setPosition(coord: Coordinate) { + this.popupOverlay.setPositioning(this.computePositioning(coord)) this.popupOverlay.setPosition(coord) } + private computePositioning(coord: Coordinate): Positioning { + if (!this.map) return 'top-left' + + const pixel = this.map.getPixelFromCoordinate(coord) + const size = this.map.getSize() + if (!pixel || !size) return 'top-left' + + const px = pixel[0]! + const py = pixel[1]! + const width = size[0]! + const height = size[1]! + + const flipX = px > width - FLIP_MARGIN_X + const flipY = py > height - FLIP_MARGIN_Y + + if (flipX && flipY) return 'bottom-right' + if (flipX) return 'top-right' + if (flipY) return 'bottom-left' + return 'top-left' + } + dispose() { this.popupOverlay.setPosition(undefined) } diff --git a/client/tests/unit/lib/mapservices/MapPopoverOverlay.spec.ts b/client/tests/unit/lib/mapservices/MapPopoverOverlay.spec.ts index 204a32b..1579062 100644 --- a/client/tests/unit/lib/mapservices/MapPopoverOverlay.spec.ts +++ b/client/tests/unit/lib/mapservices/MapPopoverOverlay.spec.ts @@ -7,12 +7,14 @@ import type { Map as OlMap } from 'ol' // --------------------------------------------------------------------------- const mockOlOverlay = vi.hoisted(() => ({ setPosition: vi.fn(), + setPositioning: vi.fn(), getPosition: vi.fn(), })) vi.mock('ol/Overlay.js', () => ({ default: vi.fn(function (this: typeof mockOlOverlay) { this.setPosition = mockOlOverlay.setPosition + this.setPositioning = mockOlOverlay.setPositioning this.getPosition = mockOlOverlay.getPosition }), })) @@ -23,7 +25,11 @@ vi.mock('ol/Overlay.js', () => ({ describe('MapPopoverOverlay', () => { let popupElement: HTMLElement let sut: MapPopoverOverlay - let mockMap: { getPixelFromCoordinate: ReturnType; getCoordinateFromPixel: ReturnType } + let mockMap: { + getPixelFromCoordinate: ReturnType + getCoordinateFromPixel: ReturnType + getSize: ReturnType + } beforeEach(() => { vi.clearAllMocks() @@ -39,6 +45,7 @@ describe('MapPopoverOverlay', () => { mockMap = { getPixelFromCoordinate: vi.fn(), getCoordinateFromPixel: vi.fn((p: number[]) => p), + getSize: vi.fn(() => [1000, 800]), } }) @@ -252,4 +259,55 @@ describe('MapPopoverOverlay', () => { }) }) }) + + // ------------------------------------------------------------------------- + // Smart positioning + // ------------------------------------------------------------------------- + describe('smart positioning', () => { + test('uses top-left when click is far from edges', () => { + sut.initDrag(mockMap as unknown as OlMap) + // map is 1000x800, click at pixel 100,100 — far from edges + mockMap.getPixelFromCoordinate.mockReturnValue([100, 100]) + + sut.setPosition([500, 500]) + + expect(mockOlOverlay.setPositioning).toHaveBeenCalledWith('top-left') + }) + + test('uses top-right when click is near right edge', () => { + sut.initDrag(mockMap as unknown as OlMap) + // pixel x=800, within 250px of right edge (1000) + mockMap.getPixelFromCoordinate.mockReturnValue([800, 100]) + + sut.setPosition([500, 500]) + + expect(mockOlOverlay.setPositioning).toHaveBeenCalledWith('top-right') + }) + + test('uses bottom-left when click is near bottom edge', () => { + sut.initDrag(mockMap as unknown as OlMap) + // pixel y=650, within 200px of bottom edge (800) + mockMap.getPixelFromCoordinate.mockReturnValue([100, 650]) + + sut.setPosition([500, 500]) + + expect(mockOlOverlay.setPositioning).toHaveBeenCalledWith('bottom-left') + }) + + test('uses bottom-right when click is near bottom-right corner', () => { + sut.initDrag(mockMap as unknown as OlMap) + mockMap.getPixelFromCoordinate.mockReturnValue([800, 650]) + + sut.setPosition([500, 500]) + + expect(mockOlOverlay.setPositioning).toHaveBeenCalledWith('bottom-right') + }) + + test('defaults to top-left when map is not initialized', () => { + // no initDrag called — map is null + sut.setPosition([500, 500]) + + expect(mockOlOverlay.setPositioning).toHaveBeenCalledWith('top-left') + }) + }) })