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
28 changes: 27 additions & 1 deletion client/src/lib/mapservices/MapPopoverOverlay.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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)
}
Expand Down
60 changes: 59 additions & 1 deletion client/tests/unit/lib/mapservices/MapPopoverOverlay.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}),
}))
Expand All @@ -23,7 +25,11 @@ vi.mock('ol/Overlay.js', () => ({
describe('MapPopoverOverlay', () => {
let popupElement: HTMLElement
let sut: MapPopoverOverlay
let mockMap: { getPixelFromCoordinate: ReturnType<typeof vi.fn>; getCoordinateFromPixel: ReturnType<typeof vi.fn> }
let mockMap: {
getPixelFromCoordinate: ReturnType<typeof vi.fn>
getCoordinateFromPixel: ReturnType<typeof vi.fn>
getSize: ReturnType<typeof vi.fn>
}

beforeEach(() => {
vi.clearAllMocks()
Expand All @@ -39,6 +45,7 @@ describe('MapPopoverOverlay', () => {
mockMap = {
getPixelFromCoordinate: vi.fn(),
getCoordinateFromPixel: vi.fn((p: number[]) => p),
getSize: vi.fn(() => [1000, 800]),
}
})

Expand Down Expand Up @@ -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')
})
})
})