Relatude Help

32.8 Positions

A column can hold a position. Its type is Position (LogDataType.GeoCoordinate), the same GeoCoordinate the node store uses (§5). The Positions template starts a log with a position column, a text column naming what moved, and one for what happened.

using Relatude.DB.Common;   // GeoCoordinate

db.CustomLogs.Record("deliveries", ("position", new GeoCoordinate(59.9139, 10.7522)), ("vehicle", "van-7"));
db.CustomLogs.Record("deliveries", ("position", "60.3913, 5.3221"));   // text: latitude, then longitude
db.CustomLogs.Record("deliveries", ("position", (63.4305, 10.3951)));  // a (latitude, longitude) tuple

A position converts from a GeoCoordinate, from text such as "59.91, 10.75" (a comma, semicolon or space between the numbers), from a GeoCoordinate's JSON, from a (latitude, longitude) tuple, and from an array of the two. Latitude always comes first. GeoCoordinate.Empty and coordinates out of range are left out. A position reads as text with six decimals, 59.9139, 10.7522, which is what the Entries view shows and a search matches.

Searching by distance. A term naming a column of positions, with a place and a distance, finds the entries within that distance. near: searches every column of positions:

position:59.9139,10.7522~2km        within 2 km of the middle of Oslo
near:60.39,5.32~500m                any column of positions, within 500 m
-position:59.9139,10.7522~50km      everything further out than 50 km

The distance is in m or km, or metres without a unit. Other terms search the position's text: position:59.91* finds the positions whose text starts that way.

Statistics. A column of positions can have Count and Count per value, which counts exact positions and suits a small set of known places such as shops or sensors. It also has six statistics of its own:

Statistic What it answers Measured against
Centre and spread (GeoSpread) The centre of each interval's positions, how far and which way they spread, and their bounding box —
Heatmap (GeoHeatmap) How many positions each grid cell held, drawn as a map A cell size (Level)
Distance from a point (GeoDistance) Count, total, average, nearest and farthest distance from a point, in metres A point (Reference)
Distance bands (GeoDistanceBands) How many fell in each band of distance from a point: under 1 km, 1–5 km, … A point and the band edges in metres (Bands)
Zones (GeoZones) How many fell in each named circle (the first that holds them), or Outside Each zone's name, centre and radius in metres (Zones)
Areas covered (GeoCoverage) How many different grid cells had a position: how much ground was covered, estimated A cell size (Level)

Centre and spread and the heatmap have graphs of their own. The others are drawn like the general statistics: distances like Count, total, average, min, max, bands and zones like Count per value, areas like Unique count, estimated.

When you switch one on, the editor asks what it is measured against: a point, the bands in kilometres, the zones as rows, or a cell size from about 20 m to 20 km. The definition file keeps it beside the statistic:

"position": { "Name": "Position", "DataType": "GeoCoordinate", "Statistics": [
  { "StatisticsType": "GeoSpread", "Resolution": 3 },
  { "StatisticsType": "GeoHeatmap", "Resolution": 3, "Level": 16 },
  { "StatisticsType": "GeoDistance", "Resolution": 3, "Reference": { "latitude": 59.9139, "longitude": 10.7522 } },
  { "StatisticsType": "GeoDistanceBands", "Resolution": 3, "Reference": "59.9139, 10.7522", "Bands": [1000, 5000, 20000] },
  { "StatisticsType": "GeoZones", "Resolution": 3, "Zones": [
    { "Name": "Oslo", "Center": { "latitude": 59.9139, "longitude": 10.7522 }, "RadiusMeters": 3000 } ] } ] }

In code, these are optional arguments of StatisticsInfo: new(StatisticsType.GeoDistance, reference: depot) or new(StatisticsType.GeoZones, zones: [new GeoZone("Oslo", oslo, 3000)]). A statistic missing what it needs is refused when you save. Measured against something else, a statistic counts as a new one: it starts empty, and the confirmation offers to rebuild it from the entries.

How the two with graphs of their own behave:

  • Centre and spread uses the spherical mean, so it is right across the date line and near the poles, and intervals combine exactly. The spread is the standard distance, the root mean square distance from the centre. The standard deviational ellipse shows one standard deviation along the main direction of spread and one across it.
  • Heatmap cells are about 305 m tall at level 16 (the default), 1.2 km at level 14 and 4.9 km at level 12. Once an interval is over, sparse cells are merged to save space, but no count is lost. Cells shrink toward the poles, so compare positions per square kilometre, not per cell.

Analyse reads a column of positions from its entries and shows what the statistics cannot:

The median centre The place with the least total distance to every position. A few far-off positions barely move it.
How far from it Percentiles and a histogram of the distances
Where they crowd Clusters of grid cells holding at least twice the average; click one to list its entries
Tracks With Tracks by set to the column naming what moved: each one's route in time order, with distance, time moving, speeds and stops. Bad fixes are left out of the distance.
A map Every position read, up to 250,000, coloured by another column if you choose one

From code, read the centre and spread with AnalyseGeoSpread (per interval) or AnalyseCombinedGeoSpread (for the range), and the heatmap with AnalyseGeoHeatmap or AnalyseCombinedGeoHeatmap. Read the other four with the general methods, such as AnalyseCombinedGroupCounts, naming the statistic. With from and to as in §32.5:

var store = db.CustomLogs.LogStore;
var spread = store.AnalyseCombinedGeoSpread("deliveries", "position", IntervalType.Day, from, to).Value;
Console.WriteLine($"centre {spread.Center}, spread {spread.StandardDistanceMeters:0} m");
var heat = store.AnalyseCombinedGeoHeatmap("deliveries", "position", IntervalType.Day, from, to, maxCells: 1000).Value;
var densest = heat.Cells.MaxBy(c => c.Count / c.Cell.AreaSquareMeters);
var zones = store.AnalyseCombinedGroupCounts("deliveries", "position", IntervalType.Day, from, to, StatisticsType.GeoZones).Value;

Relatude.DB.Common has the same mathematics for use anywhere:

  • GeoMoments adds positions, merges sets, and turns them into a GeoSpread with Summarize().
  • GeoCell is a position's cell at a level, with its parent, neighbours, bounds and area.
  • GeoCellCounts.Compress fits counts per cell into a budget of cells, keeping every count.

Made-up entries for a log with a position and a column naming what moves (vehicle, device, courier, …) come from a simulated fleet, so every view has something to show before the application records anything.