Skip to main content

Dwell Thresholds

GPS receivers near boundaries produce noisy readings. A vehicle stopped at a warehouse loading bay can appear to cross the boundary multiple times in a few seconds, generating spurious event pairs. Without debouncing, each crossing triggers downstream workflows — notifications, billing events, dispatch assignments — incorrectly.

Dwell thresholds require an entity to remain inside or outside a geometry for a minimum duration before an event fires. Both polygon zones and circles support dwell.

How each threshold works

minInsideMs: The entity must remain continuously inside for at least this many milliseconds before the entry event fires. If the entity exits before the threshold is reached, no event fires and the inside timer resets.

minOutsideMs: Once inside, the entity must remain continuously outside for at least this many milliseconds before the exit event fires. If the entity re-enters before the threshold is reached, no event fires and the outside timer resets.

Both fields default to 0, which means instant transitions — the same behaviour as omitting the dwell option entirely.

ZoneOptions — polygon zones

Pass dwell thresholds as the third argument to registerZone:

engine.registerZone(
'loading-bay',
{ type: 'Polygon', coordinates: [[[0, 0], [2, 0], [2, 2], [0, 2], [0, 0]]] },
{
dwell: {
minInsideMs: 5_000, // entity must be continuously inside >= 5 s before 'enter' fires
minOutsideMs: 3_000, // entity must stay outside >= 3 s before 'exit' fires
},
},
)

CircleOptions — circles

Pass dwell thresholds as the fifth argument to registerCircle:

engine.registerCircle(
'depot-beacon',
7, 7, 1.5,
{
dwell: {
minInsideMs: 4_000, // entity must be continuously inside >= 4 s before 'approach' fires
minOutsideMs: 2_000, // entity must stay outside >= 2 s before 'recede' fires
},
},
)

Example

import { GeoEngine } from '@jamesholcombe/geo-stream'

const engine = new GeoEngine()

engine.registerZone(
'loading-bay',
{
type: 'Polygon',
coordinates: [[[0, 0], [2, 0], [2, 2], [0, 2], [0, 0]]],
},
{
dwell: {
minInsideMs: 5_000, // must dwell inside >= 5 s before 'enter' fires
minOutsideMs: 3_000, // must stay outside >= 3 s before 'exit' fires
},
},
)

const t0 = 1_700_000_000_000

// Entity 1: brief incursion (2 s inside — below the 5 s threshold)
// No events fire.
const briefEvents = engine.ingest([
{ id: 'van-1', x: 1.0, y: 1.0, tMs: t0 },
{ id: 'van-1', x: 5.0, y: 5.0, tMs: t0 + 2_000 }, // exits after 2 s
])
console.log(briefEvents.length) // 0

// Entity 2: sustained stay (10 s inside, then 5 s outside)
// 'enter' fires when the update at t0+5000 confirms 5 s elapsed inside.
// 'exit' fires when the update at t0+13000 confirms 3 s elapsed outside.
const sustainedEvents = engine.ingest([
{ id: 'van-2', x: 1.0, y: 1.0, tMs: t0 },
{ id: 'van-2', x: 1.0, y: 1.0, tMs: t0 + 5_000 }, // still inside at 5 s → enter fires
{ id: 'van-2', x: 5.0, y: 5.0, tMs: t0 + 10_000 }, // moves outside
{ id: 'van-2', x: 5.0, y: 5.0, tMs: t0 + 13_000 }, // still outside at 3 s → exit fires
])

for (const ev of sustainedEvents) {
console.log(ev)
}
// { kind: 'enter', id: 'van-2', zone: 'loading-bay', t_ms: 1700000005000 }
// { kind: 'exit', id: 'van-2', zone: 'loading-bay', t_ms: 1700000013000 }

t_ms reflects the timestamp of the update that triggered the event — not when the entity first crossed the boundary.

Practical values

Use caseminInsideMsminOutsideMs
Vehicle GPS (5 Hz)5 000 – 10 0003 000 – 5 000
Pedestrian GPS (1 Hz)2 000 – 5 0001 000 – 3 000
No debouncing needed0 (default)0 (default)