Schengen Tracking, Location tracking and geospatial processing

The Problem

I have this dream of taking my family for an extended tour of Europe. One of the issues with this is that these days, as UK passport holders, we can only spend 90 days in any 180 day period in the Schengen area. This is made slightly more complex as it is not actually the EU but a select group of countries some of which are not in the EU.

I wanted a way to track the number of days spent in the Schengen area as automatically as possible. So first off I built an Apple Shortcut that got my current location, worked out the country and then was going to send it to an online DB.

This mostly would work, however time based Shortcuts triggers are not always reliable and then if I had no signal it would break. I then needed to build a cache of some sort to store the data if unable to send to the online DB. All totally possible, but messy and fragile.

I was just getting to the point of building the cache when I remembered I had used a rather nifty tool called OwnTracks before.

This is a brilliant little location tracker that enables you, amongst a few other things, to track your location and send it to a system that you control.

There was one final issue though and that is OwnTracks just sends the raw location longitude and latitude to the waiting location which means it needs translating into something a little more useable for this task I need the Country. Now there are a few services that can handle this however if I am driving down a motorway the last thing I need is to be making API calls to whichever service every 30 seconds just to get the location half a mile down the road from the last time and even worse I don’t want to be sat at home and constantly getting the same location +/- a few metres.

Architecture

Now I will say right now that I am unsure Tape is really the best tool for the job here but it seemed like a bit of fun to try and see if I could build it in Tape and so thats what I did.

  • Owntracks on my phone sends to a Tape Webhook
  • Tape creates a record in a Locations App
  • There is then a Countries reference app
  • Also a Daily reference app
  • And I use Mapbox to get the location from the Latitude and Longitude
  • A Tape dashboard shows how many days are available in the Schengen area.

Distance optimisation

When a new location comes in I get the contents of the last location and compare the longitude and latitude to the incoming position, it also checks speed and accuracy and factors those in to the decision to get the location or not finally it writes all the facts to a little JSON to be used later in other automations.

const ev = `{
  "last_lat": fields[external_id = 'lat'].values.value,
  "last_lon": fields[external_id = 'lon'].values.value
}`;

const lastData = jsonata(ev).evaluate(record_collection_location_log);

const newLat = Number(jsonata('lat').evaluate(webhook_payload_parsed));
const newLon = Number(jsonata('lon').evaluate(webhook_payload_parsed));
const acc = Number(jsonata('acc').evaluate(webhook_payload_parsed));
const vel = Number(jsonata('vel').evaluate(webhook_payload_parsed) ?? 0);

const lastLat = Number(lastData.last_lat);
const lastLon = Number(lastData.last_lon);

const hasLastLocation =
  Number.isFinite(lastLat) &&
  Number.isFinite(lastLon);

let distanceKm = null;
let distanceMetres = null;

if (hasLastLocation) {
  const latDiffKm = Math.abs(lastLat - newLat) * 111;
  const lonDiffKm = Math.abs(lastLon - newLon) * 67;

  distanceKm = Math.sqrt(
    Math.pow(latDiffKm, 2) + Math.pow(lonDiffKm, 2)
  );

  distanceMetres = distanceKm * 1000;
}

const speedKmh = vel * 3.6;

let thresholdMetres;

if (speedKmh >= 80) {
  thresholdMetres = 10000; // motorway
} else if (speedKmh >= 40) {
  thresholdMetres = 5000; // main roads
} else if (speedKmh >= 10) {
  thresholdMetres = 1500; // urban / cycling / slow vehicle
} else {
  thresholdMetres = 500; // walking / stationary / pottering about
}

let getLoc = 'no';
let reason = 'within threshold';

if (!Number.isFinite(newLat) || !Number.isFinite(newLon)) {
  reason = 'invalid coordinates';
} else if (!Number.isFinite(acc) || acc > 100) {
  reason = 'poor accuracy';
} else if (!hasLastLocation) {
  getLoc = 'yes';
  reason = 'no previous geocoded location';
} else if (distanceMetres >= thresholdMetres) {
  getLoc = 'yes';
  reason = `moved ${Math.round(distanceMetres)}m over ${thresholdMetres}m threshold`;
}

const compareObj = {
  last_lat: hasLastLocation ? lastLat : null,
  last_lon: hasLastLocation ? lastLon : null,
  new_lat: newLat,
  new_lon: newLon,
  accuracy_m: acc,
  velocity_mps: vel,
  speed_kmh: speedKmh,
  moved_km: distanceKm,
  moved_m: distanceMetres,
  threshold_m: thresholdMetres,
  get_loc: getLoc,
  reason
};

var_compare = JSON.stringify(compareObj);

Interesting Tape bits

I use JSONata to both extract and create JSON from both other Tape records and the incoming payloads. As an example when you get a location from Mapbox the response is full of information I don’t need (and some duplication of information) so I use JSONata to extract the components that I do need and put them into a ‘simplified’ JSON:

const locData = jsonata(`{'address':features.properties.full_address[],
'country_code_alpha_3': features[0].properties.context.country.country_code_alpha_3,
'country_code': features[0].properties.context.country.country_code,
'country_name': features[0].properties.context.country.name
}`).evaluate(JSON.parse(r));

Which gives me a workable:

{
  "address": [
    "Ffordd Bodnant, Eglwysbach, Colwyn Bay, LL28 5RE, United Kingdom",
    "LL28 5RE, Colwyn Bay, Conwy, Wales, United Kingdom",
    "Eglwysbach, Conwy, Wales, United Kingdom",
    "Colwyn Bay, Conwy, Wales, United Kingdom",
    "Conwy, Wales, United Kingdom",
    "Wales, United Kingdom",
    "United Kingdom"
  ],
  "country_code_alpha_3": "GBR",
  "country_code": "GB",
  "country_name": "United Kingdom"
}

I can then extract the information I need from this as I need it.

As mentioned in the Distance Optimisation section I build my own JSON from a few data sources and then store that in a record field this can be done thanks to the new Multiline plain text field.

I use a separate countries app and search for and then relate the location record to the country record following a location retrieval. I also flag the country record as active and remove that flag from the previous country record (if different).

if (!cRecord) {
  throw new Error(`No country record found for ${searchCode}`);
}
if (currentActiveCountryId !== cRecord) {
  await tape.Record.update(cRecord, {
    fields: {
      active: "Yes",
    }
  });

  if (currentActiveCountryId) {
    await tape.Record.update(currentActiveCountryId, {
      fields: {
        active: "No",
      }
    });
  }
};

Expansion

As I mentioned at the start Tape probably isn’t intended to be a geospatial processing platform and may not be the best tool for the job however yet again Tape proves to be one of the most flexible tools available and can be used for a huge variety of use cases and provides a great platform to build on and expand as needed, the fact that I can use it to do the geospatial processing and then also track the days in the Schengen area and then display that in a dashboard is really useful and means I can have everything in one place and not have to rely on multiple different tools and platforms to achieve the same thing.

Will I keep using Tape for this? Yes I expect so what I may do is build a web front end for it at some point maybe with historical mapping and timelines.

What other extra things could I do:

  1. Country change notifications - One of the advantages of using Tape for this is the easy access to the data. So for example, if you got flagged in the wrong country because of a GPS error, you could get a notification, go into Tape and change or even delete the incorrect record.
  2. Emails and notifications when running short of Schengen days - I can easily set up a notification system to alert me when I am approaching the 90 day limit in the Schengen area and also when I have used up all my days.
  3. Travel timeline - I could build a timeline of my travels showing where I have been.
  4. OwnTracks can have areas configured and then if you are in that area it is included in the location payload I could setup more of these and then use them to set the location on the locations record reducing the Mapbox API calls even further.

Conclusion

Overall what started as a spur of the moment idea to see if I could build the solution in Tape has actually worked out really well and I am genuinely happy with the results.

For those that are interested, it has taken me less than a day to build the whole solution and write this up.

2 Likes

This is amazing! How many hours was this rolling around your noggin from start to finish?