Tutorial: Geofences
A geofence is a user-defined region in world coordinates tracked by Worlds. When Worlds detects an object entering, leaving, or remaining within a geofence, a geofence event is emitted. In combination with the Worlds API, geofences can enable complex interactions within your own applications. In this guide, you’ll learn how to set up a basic application to monitor detections and geofence events from Worlds.
Some common scenarios that leverage geofences include:
- Detecting when a person enters a restricted area
- Detecting vehicles in a parking lot
- Detecting a person waiting for a crosswalk
- Detecting when a person without protective equipment and a hazard are present in the same area
All of these scenarios will require custom logic integrated with your own systems but the approach through Worlds will be similar. Let’s get started!
At the end of this guide, you will have:
- A high-level understanding of geofences and the kinds of scenarios they enable
- A few sample GraphQL query and subscription operations for use with geofences
- A brief understanding of Apollo and how to use it to issue GraphQL queries and subscriptions;
- A basic Node.js script that subscribes to the Worlds API via GraphQL; and
- A simple state system to assist in business logic decisions.
At the end of this guide, you will have:
- A high-level understanding of geofences and the kinds of scenarios they enable
- A few sample GraphQL query and subscription operations for use with geofences
Prerequisites
This article requires some base-line knowledge before getting started. To be best prepared for this article, make sure you:
-
Have system administrator access to Worlds and be familiar with setting up geofences. Alternatively, you can work with a system admin to get access to the security tokens and the Geofence ID with which you wish to work.
Values to get from your Worlds installation include:
- GraphQL HTTPS endpoint URL. This should start with
https
and end with/graphql
. - GraphQL secure web-socket endpoint URL. This should start with
wss
and end with/graphql
. - Token ID and Token Value. See authentication for details.
- Camera device ID. For this guide, we’ll only be listening to detections on a single device.
- Geofence ID. For this guide, we’ll only be listening to events from a single geofence.
- GraphQL HTTPS endpoint URL. This should start with
-
Have a working understanding with your chosen programming language and platform and the related tools installed on your machine.
You should have:
- Knowledge of typical Node.js projects with npm, including package management and scripts.
- Installed Node.js software, including:
- Node.js 20+
- npm 10+
You can leverage the GraphiQL Explorer to test queries while working with your own application. The queries provided herein will use variables to make it easy to copy-paste into the explorer, but be sure to provide the correct object via the Variables tab.
Project Setup
Start by setting up a new Node.js project.
-
Create a new directory to contain the project.
-
Create a new npm project within your new directory:
Terminal window npm initUpdate the new package.json to be a “module” project; adjust
main
, add atype
key, and update thescripts
section as follows:package.json {"main": "index.mjs","type": "module","scripts": {"start": "node index.mjs"}}Setting the “type” setting to “module” allows Node.js to natively support the modern
import
statement, top-level async/await, etc. For more information, see the Node.js documentation on ESM. -
Add the following dependencies:
@apollo/client
- Apollo is a popular GraphQL client for JavaScript environments, including both the Node.js and browser environments.dotenv
- Loads environment variables from a configuration file so that security tokens are not held directly within the source code.graphql
- Node.js library for parsing GraphQL into abstract syntax trees, leveraged by Apollo as a peer dependency.graphql-ws
- Add support forgraphql-transport-ws
, the protocol Worlds uses for GraphQL subscriptions.ws
- Web socket implementation for Node.js.
All dependencies can be installed with the following command:
Terminal window npm install @apollo/client dotenv graphql graphql-ws wsOur
package.json
was updated to the following contents:package.json {"main": "index.mjs","type": "module","scripts": {"start": "node index.mjs"},"dependencies": {"@apollo/client": "^3.10.8","dotenv": "^16.4.5","graphql": "^16.9.0","graphql-ws": "^5.16.0","ws": "^8.17.1"}} -
Add the initial script at
index.mjs
.index.mjs console.log('Hello, Worlds!'); -
(Optional) Initialize git.
Terminal window git initThis
.gitignore
will suffice for this guide:.gitignore node_modules/.env.DS_Store
At this point, you should be able to run npm start
and see the response
“Hello, Worlds!”
Set up Apollo
Apollo has a large set of configuration options with which to work. For this guide, we’ll be keeping it simple, using Worlds-specific authentication and an in-memory cache.
-
First, let’s create our
.env
file. An envfile is a standard format for specifying environment variables used bydotenv
and other systems, including Docker. For this project, there will be four environment variables:.env GRAPHQL_ENDPOINT_HTTP=https://worlds.example.com/graphqlGRAPHQL_ENDPOINT_WS=wss://worlds.example.com/graphqlTOKEN_ID=your-token-idTOKEN_VALUE=your-token-valueReplace the four values with the information you have received for your Worlds instance.
-
Next, we can set up the variables to pass the authentication tokens by loading the
.env
file withdotenv
. The same header object can be used for both the HTTP and web socket protocols.index.mjs import 'dotenv/config';const headers = {"x-token-id": process.env.TOKEN_ID.trim(),"x-token-value": process.env.TOKEN_VALUE.trim(),}; -
Apollo uses a “link” configuration option to specify the protocol with which to communicate with the server; to use multiple protocols, a “split” link must be created. Worlds supports both HTTP and graphql-transport-ws protocols, and while HTTP is much more efficient for queries and mutations, it does not support subscriptions. To set up our link for full support and the best performance, add the following imports and declarations to
index.mjs
:index.mjs import { split, HttpLink, ApolloClient, InMemoryCache, gql } from '@apollo/client/core/index.js';import { getMainDefinition } from '@apollo/client/utilities/index.js';import { GraphQLWsLink } from '@apollo/client/link/subscriptions/index.js';import { createClient } from 'graphql-ws';import { WebSocket } from 'ws';// previous `const headers =` content should go hereconst splitLink = split(function isSubscription({ query }) {const definition = getMainDefinition(query);return (definition.kind === 'OperationDefinition' &&definition.operation === 'subscription');},new GraphQLWsLink(createClient({webSocketImpl: WebSocket,url: process.env.GRAPHQL_ENDPOINT_WS.trim(),connectionParams: headers,})),new HttpLink({uri: process.env.GRAPHQL_ENDPOINT_HTTP.trim(),headers,}),);The above split link will send
subscription
requests to the GraphQL web socket protocol, whilequery
andmutation
requests will be sent over standard HTTP.- The inline function
isSubscription
recognizes subscription operations and returnstrue
, which tells thesplit
function to use the first link provided. - The first link provided is the web-socket link to support the
graphql-transport-ws protocol. (The
webSocketImpl
property is not required in browser environments.) The standardheaders
object is used asconnectionParams
to provide authentication information. - The second link is for the HTTP protocol. Again, the standard
headers
object is used, this time for HTTP headers on the initial request.
- The inline function
-
The last part of setup is to create the actual Apollo client. Add the following declaration at the end of the file.
index.mjs const client = new ApolloClient({link: splitLink,cache: new InMemoryCache(),});A caching implementation is required by Apollo, but there are many more options; for more information, see Configuring the Apollo Client Cache.
Congratulations! You now have your Apollo client set up. You may issue a basic
query to verify your geofence’s details. One of the most basic queries involves
looking up an object by its unique identifier via a corresponding field on the
Query
type, such as the geofence field. See the
below example for verifying your client; be sure to adjust the geofence ID to
match your environment.
const geofenceId = 2; // Change this value to match your environment
const result = await client.query({ query: gql`query($geofenceId: ID!) { geofence(id: $geofenceId){ id name bounds { polygon { coordinates } height } } }`, variables: { geofenceId }});
console.log(result.data.geofence);
This leverages GraphQL variables to issue a query to the geofence field; any data related to the geofence ID specified should be available via the API. This can be used to verify that your connection to Worlds is successful and that you are leveraging the correct geofence ID.
Set up your application and client per your platform’s guidance. To test your client, you may use the following query to verify your connection.
query($geofenceId: ID!) { geofence(id: $geofenceId){ id name bounds { polygon { coordinates } height } }}
Add GraphQL Subscription
Now that your initial client is set up and authenticating with Worlds, we are ready to create a subscription. A subscription in GraphQL is an ongoing connection to the server to receive events over time. For this guide, we are interested in detection activity events and the related geofence data.
With Apollo, a subscription is created via the ApolloClient.subscribe
function. The subscription invocation accepts arguments including the GraphQL
subscription operation and any variables that are required. We can get much of
the relevant information we are looking for in the following subscription.
const observable = client.subscribe({ query: gql`subscription Subscription { detectionActivity(filter: { }){ globalTrackId tag timestamp position { coordinates } }}`});
Apollo’s subscribe
function returns a typical observable structure from the
zen-observable
library; we have not actually subscribed to
the events yet. Observables returned from Apollo support the following events:
next
provides each element of the subscription: in this case, each detection.complete
informs observers that the subscription was terminated under expected conditions by the server.error
informs observers that the subscription was terminated with an error by the server.
We want to listen to the next
event and capture any error
that occurs. For
now, we’ll push those to the console as raw data.
const subscription = observable.subscribe({ error: (v) => console.error(v), next: (v) => console.log(v.data.detectionActivity)});
The resulting subscription
has an unsubscribe()
function to halt incoming
events and terminate the server connection; this may be used in conjunction with
other business rules. For example, if you only are checking for vehicles in a
parking lot outside normal business hours, you may wish to terminate the
subscription to Worlds during business hours, reconnecting and disconnecting
regularly.
With the addition of this subscription, you can run this script to access realtime detections data from Worlds within your Node.js application.
An initial subscription to receive all detection activity on a specified camera is as follows:
subscription Subscription { detectionActivity(filter: { }){ globalTrackId tag timestamp position { coordinates } }}
You will receive an update every time a detection occurs, including the new position data.
Filter to Camera
Receiving all detections is quite noisy. The Worlds API allows for more in-depth filtering of queries and subscriptions via “filters”. Our next step is to a filter to limit the results to our selected device.
Update your subscription to specify the deviceId
. While we’re at it, we should
also change which fields we receive; we don’t need the exact position for this
example. Be sure to adjust your deviceId based on your actual environment.
const deviceId = 4; // Change this value to match your environment
const observable = client.subscribe({ query: gql`subscription Subscription($deviceId: ID!) { detectionActivity(filter: { deviceId: { eq: $deviceId } }){ globalTrackId timestamp tag geofenceEvents { geofence { id }, type } }}`, variables: { deviceId }});
subscription Subscription($deviceId: ID!) { detectionActivity(filter: { deviceId: { eq: $deviceId } }){ globalTrackId timestamp tag geofenceEvents { geofence { id }, type } }}
This new subscription uses filters to only receive detections from the specified camera. This produces fewer events and has exactly the data we’ll need to track which objects are within the geofence.
The geofenceEvents
field returns an array of events for each geofence
related to the detection. Each geofence event has many fields
available; in this case, we are accessing
the following fields:
-
geofence
, specifically theid
field. GraphQL gives tremendous flexibility in selecting details, but in this case we want to make sure the event we handle matches the original geofence requested, even if multiple interactions are detected. -
type
, which will be one of the following values:ingress
, meaning that the detection was entering the geofenceegress
, meaning that the detection was leaving the geofencedwell
, meaning that the detection has remained within the geofence for some time.
Geofence events for a track will typically follow a single
ingress
event, any number ofdwell
events, followed by a finalegress
event. A track may re-enter the geofence, which will repeat the cycle. In the case of this application, onlyingress
andegress
events are useful, but tracking objects that have remained within the geofence for an extended period may be useful for certain applications.
Track State
Once we’re receiving all the ingress/egress events, we can start tracking state of active tracks within the geofence.
For this sample Node.js Apollo service, we can track most state in memory within the JavaScript environment. Take care to use a combination of queries and subscriptions to account for initial state and connectivity gaps for real-world scenarios.
We’ll start by setting up our in-memory state to hold the list of tracks of each type are currently within the geofence.
const current = {};function updateState(type, tag, id) { current[tag] ??= []; if (type === 'egress') { if (current[tag].includes(id)) { current[tag] = current[tag].filter(v => v !== id); return true; } } else if (!current[tag].includes(id)) { current[tag].push(id); return true; } return false;}
This mutable state structure stored in the current
variable will give us
access to the current ids of the tracks within the geofence, grouped by the type
of object. Egress functions will remove the tracks from their corresponding tag,
while all other events will add them (just in case we subscribe when something
is still within the geofence).
Next, add a simple function to display the current state:
function logCurrentState() { console.log( 'Current detections: ' + Object.entries(current) .map(([key, value]) => `${key}: ${value.length}`) .join(', ') );}
Next, let’s write the logic to keep the state up-to-date with each detection
activity. This function will accept the result from the
detectionActivity
field, which
is a Detection
. Again, be sure to change the
geofenceId
to match your environment.
const geofenceId = 2; // Change this value to match your environment
function handleDetectionActivity(detection) { let changed = false; for (const event of detection.geofenceEvents) { if (event.geofence.id == geofenceId) changed = updateState(event.type, detection.tag, detection.globalTrackId) || true; } if (changed) { logCurrentState(); }}
Finally, to call the new state tracking, update the
observable.subscribe
invocation to use the latest function:
const subscription = observable.subscribe({ error: (v) => console.error(v), next: (v) => handleDetectionActivity(v.data.detectionActivity),});
Now, when you run your application, you’ll receive a series of log messages whenever the list of objects in the geofence changes.
Current detections: car: 0, person: 1Current detections: car: 1, person: 1Current detections: car: 1, person: 0Current detections: car: 0, person: 0Current detections: car: 1, person: 0Current detections: car: 0, person: 0
With a combination of ingress, egress, and dwell events, you can store the state in any way you choose. See the reference section of this site for more details on detection activity events.
Try to create an application that tracks the number of each type of track active within a geofence using the subscription above. Keep it simple while you’re learning, but in real-world scenarios, take care to use a combination of queries and subscriptions to account for initial state and connectivity gaps.
Next Steps
As you have seen from this guide, the geofences are a powerful feature provided by the Worlds API for detecting the presence of objects in real-world areas. Geofence activity, when combined with your business data, can enable creative solutions to your business needs.
This guide sets up only the most basic application; additional handling should be done to handle the initial state when launching the service, handle errors, network connectivity issues, etc.