Skip to content

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.

Prerequisites

This article requires some base-line knowledge before getting started. To be best prepared for this article, make sure you:

  1. 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.
  2. 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+

Project Setup

Start by setting up a new Node.js project.

  1. Create a new directory to contain the project.

  2. Create a new npm project within your new directory:

    Terminal window
    npm init

    Update the new package.json to be a “module” project; adjust main, add a type key, and update the scripts 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.

  3. 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 for graphql-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 ws

    Our 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"
    }
    }
  4. Add the initial script at index.mjs.

    index.mjs
    console.log('Hello, Worlds!');
  5. (Optional) Initialize git.

    Terminal window
    git init

    This .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.

  1. First, let’s create our .env file. An envfile is a standard format for specifying environment variables used by dotenv and other systems, including Docker. For this project, there will be four environment variables:

    .env
    GRAPHQL_ENDPOINT_HTTP=https://worlds.example.com/graphql
    GRAPHQL_ENDPOINT_WS=wss://worlds.example.com/graphql
    TOKEN_ID=your-token-id
    TOKEN_VALUE=your-token-value

    Replace the four values with the information you have received for your Worlds instance.

  2. Next, we can set up the variables to pass the authentication tokens by loading the .env file with dotenv. 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(),
    };
  3. 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 here
    const 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, while query and mutation requests will be sent over standard HTTP.

    • The inline function isSubscription recognizes subscription operations and returns true, which tells the split 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 standard headers object is used as connectionParams 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.
  4. 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.

index.mjs
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.

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.

index.mjs
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.

index.mjs
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.

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.

index.mjs
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 }
});

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 the id 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 geofence
    • egress, meaning that the detection was leaving the geofence
    • dwell, 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 of dwell events, followed by a final egress event. A track may re-enter the geofence, which will repeat the cycle. In the case of this application, only ingress and egress 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.

index.mjs
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:

index.mjs
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.

index.mjs
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:

index.mjs
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: 1
Current detections: car: 1, person: 1
Current detections: car: 1, person: 0
Current detections: car: 0, person: 0
Current detections: car: 1, person: 0
Current detections: car: 0, person: 0

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.