Michal Zalecki
Michal Zalecki
software development, testing, JavaScript,
TypeScript, Node.js, React, and other stuff

Implementing Geofencing with HERE

Geofencing allows for locating points within defined geographic areas. Areas can be defined with geographic points, consisting of latitude and longitude, forming any shape. HERE provides Geofencing Extension API for that purpose.

To get started with HERE you need an account. There’s a free 90-day trial which should be plenty for the beginning. On developer.here.com you can also find documentation and API explorer, but don’t get too excited, many of the examples are still using version 1 of the Geofencing Extension API, we are going to use version 2.

Creating a map

To render a map only mapsjs-core.js and mapsjs-service.js are required. To add some interactivity to the map we need mapsjs-events.js. Events module makes it possible to add listeners for events like tap or drag. Events module, except standard set of events, lets us add a default behavior to the map. That's why map is not only a static image on a canvas but also supports pan interaction, zooming and more.

<script src="https://js.api.here.com/v3/3.0/mapsjs-core.js"></script>
<script src="https://js.api.here.com/v3/3.0/mapsjs-service.js"></script>
<script src="https://js.api.here.com/v3/3.0/mapsjs-mapevents.js"></script>

To create a map, or use HERE services in general, you need App ID, App Code for JavaScript/REST API and create platform instance. There are many map types to choose from. Map constructor accepts an HTML element to render a map inside it and a basic set of parameters like map’s zoom and center position.

const platform = new H.service.Platform({
  app_id: process.env.APP_ID,
  app_code: process.env.APP_CODE,
  useHTTPS: true
});

const defaultLayers = platform.createDefaultLayers();

const map = new H.Map(
  document.querySelector(“#map”),
  defaultLayers.satellite.map,
  {
    zoom: 17.4,
    center: { lat: 52.514480, lng: 13.239491 }
  },
);

To give a map ability to move, zoom in and out we need to create map events and attach them a default behavior.

const mapEvents = new H.mapevents.MapEvents(map);
const behavior = new H.mapevents.Behavior(mapEvents);

Defining areas

As areas are defined with geographic points we need to get those points as tuples of latitude and longitude values. If you require high precision, like detecting the correct side of the street, then forget about taking those points from a different map service. It’s tempting as Google Maps allows you to do that without any additional setup, but there’s a possibility that grids are not perfectly aligned across the services.

Latitude and longitude of a given point aren't available directly on the event object. Using pointer viewportX and viewportY position we can calculate coordinates. Function screenToGeo is provided to us on a map object.

map.addEventListener("tap", (e) => {
  const { viewportX, viewportY } = e.currentPointer;
  const cords = map.screenToGeo(viewportX, viewportY);
  console.log(`${cords.lng} ${cords.lat}`);
});

With a set of points building an area, we define polygons which we can upload to HERE service.

ID	NAME	ABBR	WKT
1	Sector A	SEC_A	POLYGON((13.237612232533337 52.514680421323064, 13.237649783459545 52.51476856436084, 13.238301560250164 52.514639614301224, 13.238272055951 52.5145857489744, 13.237617596951367 52.51467878904291))
2	Sector B	SEC_B	POLYGON((13.238292909249964 52.514639045133485, 13.237653202399912 52.51477044361, 13.237710869893732 52.51486674783397, 13.238323754653635 52.514692094255146))
3	Sector C	SEC_C	POLYGON((13.238327777967157 52.51468964583558, 13.23771221099824 52.514869196243666, 13.237818158254328 52.51495570663296, 13.238353258952799 52.514740246479356))
4	Sector D	SEC_D	POLYGON((13.237805343723096 52.514971283632484, 13.237885809993543 52.51503983889004, 13.238426275110044 52.514793365917456, 13.238364584302701 52.51474521375147, 13.237794614887036 52.51496883522846))
5	Sector Z	SEC_Z	POLYGON((13.241086924988508 52.51474124523777, 13.241709197479963 52.514831020428126, 13.241719926316023 52.51472329017769, 13.241103018242597 52.514689012315344, 13.241097653824568 52.51474124523777))

The first row consists of an arbitrary set of column names, only WKT column is mandatory. Be careful and separate columns with tabs instead of spaces. If you use spaces upload will fail with Illegal column name error response. When you are done with adding fences, zip a wkt file.

zip areas.wkt.zip areas.wkt

Areas are uploaded via sending a zipped wkt text file you've just created. We are going to use curl. Upload is a full upload which means that each time you send a new file to HERE, old shapes are removed.

curl \
--request \
-i \
-X POST \
-H "Content-Type: multipart/form-data" \
-F "[email protected]" \
  "https://gfe.cit.api.here.com/2/layers/upload.json?layer_id=4711&app_id=<APP_ID>&app_code=<APP_CODE>

If you did everything correctly the response should be:

{
  "storedTilesCount": 6,
  "response_code": "201 Created"
}

Checking position

Now we are done with the difficult part of defining and uploading geographic areas to HERE. To check the point against geofence polygons we issue a request.

fetch(`
  https://gfe.cit.api.here.com/2/search/proximity.json
  ?app_id=${process.env.APP_ID}
  &app_code=${process.env.APP_CODE}
  &layer_ids=4711
  &key_attribute=NAME
  &proximity=${lat},${lng}
`)

The response contains an array of matched geometries where each of them has a few values. We are particularly interested in two of them. To read data by column names associated with matching polygon we can access attribues. Polygons shape is defined under a geometry, unfortunately, as a string instead of a set of points.

{
  "geometries": [
    {
      "attributes": {
        "ID": "3",
        "GEOMETRY_ID": "2",
        "NAME": "Sector C",
        "ABBR": "SEC_C"
      },
      "distance": -99999999,
      "nearestLat": 0,
      "nearestLon": 0,
      "layerId": "4711",
      "geometry": "MULTIPOLYGON(((13.23833 52.51469,13.23771 52.51487,13.23782 52.51496,13.23835 52.51474,13.23833 52.51469)))"
    }
  ],
  "response_code": "200 OK"
}"

Drawing fence on a map

If your use case requires you to display matching fence you have two options. One is to use a fence ID from wkt file to match with the list of predefined polygons. The flaw in this approach is having two datasets of polygons which we must keep in sync. The second option is to reuse polygon definition from the response. Keeping files in sync doesn’t sound like fun and parsing a string to set of points is a simple task allowing us to have one source of truth for our fences.

function pairToLatLng(pair: string) {
  const [lng, lat] = pair.split(" ").map(parseFloat);
  return { lng, lat };
}

function geometryToPoints(geometry) {
  return geometry.replace(/[A-Z()]*/g, "").split(",").map(pairToLatLng);
}

First of all, we have to get rid of all characters which are meaningless for our use case. Pairs of points are separated by a comma and coordinates in each pair are separated by a single space. The first coordinate is a longitude and latitude comes second.

const strip = new H.geo.Strip();

geometryToPoints(response.geometries[0].geometry).forEach((point) => {
  strip.pushPoint(point);
});

const polygon = new H.map.Polygon(
  strip, { style: { lineWidth: 1, fillColor: "#FF0000", strokeColor: "#000" }}
);

map.addObject(polygon);

We've built an instance of H.geo.Strip from a set of parsed points and we've drawn a fence on a map using H.map.Polygon. Polygons can be styled similarly to SVG elements.

For more information on geofencing visit documentation and HERE blog.

Photo by Ruthie on Unsplash.