Geofence enter delay trigger?

bbollard 21 days ago

Our geofence for one of our ambulance stations sits right on one of our main roads. I am trying to take advantage of geofence alerts for enter and exit to denote units entering/leaving quarters but when units pass by to/from calls the events are inadvertently firing. I looked for a way to delay a geofence enter alert until continuously inside the fence for x seconds but could not find. Also wonder how geofence crossed event can help here.

Any suggestions would be appreciated.

Anton Tananaev 21 days ago

Currently we don't have a delay trigger for geofences.

Juan Pablo Garcia 3 days ago

Standard Traccar doesn't have a "wait until continuously inside for X seconds" option for geofence enter — that's why you couldn't find it. And geofenceCrossed won't help here: it's the opposite tool. It's enabled with event.geofence.segmentCrossing=true and fires when the straight line between two consecutive positions crosses a geofence even if no position landed inside (so visits aren't missed with long reporting intervals). That makes detection more aggressive, so it would fire on more drive-bys, not fewer.

Two things that solve it:

1) Reshape the geofence tighter around the station apron/parking so it doesn't overlap the road. This alone removes most of the spurious clipping and needs no code.

2) Add a small "dwell" event handler if you build from source. It starts a timer when a unit enters and only raises an event once it's been continuously inside for a configured number of seconds; a unit passing by never reaches the limit, so nothing fires. It resets when the unit leaves, so it re-arms for the next real arrival.

Steps:

a. Add the event type in src/main/java/org/traccar/model/Event.java:

public static final String TYPE_GEOFENCE_DWELL = "geofenceDwell";

b. Create src/main/java/org/traccar/handler/events/GeofenceDwellHandler.java:

package org.traccar.handler.events;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.traccar.model.Event;
import org.traccar.model.Geofence;
import org.traccar.model.Position;
import org.traccar.session.cache.CacheManager;

import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

@Singleton
public class GeofenceDwellHandler extends BasePositionEventHandler {

    public static final String ATTRIBUTE_DWELL_LIMIT = "dwellLimit"; // seconds, set on the geofence
    public static final String ATTRIBUTE_DWELL_TIME = "dwellTime";   // reported on the event

    // per device: geofenceId -> entry time (+ whether we already fired for this stay)
    private final Map<Long, Map<Long, DwellEntry>> insideStates = new ConcurrentHashMap<>();

    private static final class DwellEntry {
        private final Date startTime;
        private boolean notified;
        private DwellEntry(Date startTime) {
            this.startTime = startTime;
        }
    }

    @Inject
    public GeofenceDwellHandler(CacheManager cacheManager) {
        super(cacheManager);
    }

    @Override
    protected void onPosition(Position position, Position lastPosition, Callback callback) {

        Set<Long> current = new HashSet<>();
        if (position.getGeofenceIds() != null) {
            current.addAll(position.getGeofenceIds());
        }

        Map<Long, DwellEntry> deviceState =
                insideStates.computeIfAbsent(position.getDeviceId(), key -> new ConcurrentHashMap<>());

        // reset timers for geofences we're no longer inside (re-arms for the next visit)
        deviceState.keySet().removeIf(geofenceId -> !current.contains(geofenceId));

        for (long geofenceId : current) {
            Geofence geofence = cacheManager.getObject(Geofence.class, geofenceId);
            if (geofence == null) {
                continue;
            }
            long limit = geofence.getLong(ATTRIBUTE_DWELL_LIMIT); // seconds
            if (limit <= 0) {
                continue; // no dwell configured on this geofence
            }

            DwellEntry entry = deviceState.get(geofenceId);
            if (entry == null) {
                deviceState.put(geofenceId, new DwellEntry(position.getFixTime())); // start timer
                continue;
            }
            if (entry.notified) {
                continue; // already fired for this continuous stay
            }

            long elapsed = position.getFixTime().getTime() - entry.startTime.getTime();
            if (elapsed >= limit * 1000L) {
                Event event = new Event(Event.TYPE_GEOFENCE_DWELL, position);
                event.setGeofenceId(geofenceId);
                event.set(ATTRIBUTE_DWELL_TIME, elapsed / 1000L);
                callback.eventDetected(event);
                entry.notified = true;
            }
        }
    }
}

c. Register it in src/main/java/org/traccar/ProcessingHandler.java, in the eventHandlers list, right after GeofenceEventHandler.class:

                GeofenceEventHandler.class,
                GeofenceDwellHandler.class,

d. (optional, for a nice label in the web app) add to the web translations, e.g. en.json:

"eventGeofenceDwell": "Geofence dwell",

e. On the station geofence, add an attribute dwellLimit with the number of seconds a unit must stay before it counts as "in quarters" (e.g. 20). Then create a Notification for the geofenceDwell event type.

Now a unit that just drives past (inside for a couple of seconds) never triggers, while one that actually pulls into quarters and stays does. It uses the timestamps on the positions, fires once per stay, and re-arms after the unit leaves.

If you also want a "left quarters" event that ignores brief clips, the same pattern works in reverse: start a timer on a real exit and fire only after the unit has been continuously outside for X seconds.