GIS & Geospatial Data (QGIS, GeoPandas, PostGIS) Module 7 – GNSS, field data and point clouds
GNSS, NavIC, field data and GPS tracks
How a GNSS receiver fixes its position and where the errors come from, NavIC and India's CORS, field data with its accuracy, and clean GPS tracks.
What you will learn
- Explain how a receiver solves its position and clock from four or more satellites, and how geometry scales the error
- Describe NavIC and Survey of India's CORS network, and who may use the CORS data
- Record field data with the accuracy of every position, in QField or as GPX
- Clean a GPS track by dropping poor fixes and spikes, finding stops and splitting it into trips
- Protect the people a track describes before it is shared
Before you start
What you’ll learn · Before you start
What you will learn
- Explain how a receiver solves its position and clock from four or more satellites, and how geometry scales the error
- Describe NavIC and Survey of India's CORS network, and who may use the CORS data
- Record field data with the accuracy of every position, in QField or as GPX
- Clean a GPS track by dropping poor fixes and spikes, finding stops and splitting it into trips
- Protect the people a track describes before it is shared
Before you start
On this page
A phone logged a field day of 12.53 km as 20.47 km, because standing still for 30 minutes at a field drifted it 774 m and five fixes jumped hundreds of metres. Every GNSS position carries an error, and a track adds them up.
Here you see how a receiver solves its position from satellite ranges and why the same errors give 6 m or 73 m depending on where the satellites are, what NavIC and India’s CORS network add, how to record field data with the accuracy of each point, and how to clean a track into trips, before sharing it without exposing the people in it.
A receiver solves four unknowns from satellite ranges
GNSS (global navigation satellite system): A system of satellites that broadcast their time and orbit so that a receiver can work out its position and clock: GPS, NavIC, Galileo and BeiDou are examples., global navigation satellite systems such as GPS, NavIC, Galileo and BeiDou, work the same way. Each satellite broadcasts the time by its clock and its orbit; a receiver times each signal’s arrival and turns the travel time into a distance. Its own clock is cheap, though, and an error of 1 ms adds 299,792 m to every distance, so each is a pseudorange: A receiver's measured distance to a satellite, the signal's travel time times the speed of light, which includes the error of the receiver's own clock.. The receiver therefore solves four unknowns, east, north, up and its clock error, and needs four satellites at least. Follow one fix:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The signals. Every satellite broadcasts the time by its atomic clock and the orbit it is on, so the receiver knows where each signal came from and when it left.
- Timing. The receiver notes when each signal arrives, by its own clock. Multiplying the travel time by the speed of light gives a distance to each satellite.
- Pseudoranges. A receiver's clock is not an atomic one. If it runs 1 ms fast, every distance comes out 299,792 m too long, the same amount for every satellite, so each is a pseudorange.
- Four unknowns. The position has three unknowns and the clock error a fourth, so four satellites give four equations. From four exact pseudoranges the program finds the position to the millimetre and the clock's 1.000000 ms.
- More satellites. With six or more satellites the receiver solves by least squares, which spreads the range errors over all of them instead of fitting four exactly.
- Geometry. The same 3 m range errors gave 95 % of fixes within 6.2 m with satellites spread over the sky, HDOP 1.2, and within 73.2 m with all six in one part of it, HDOP 12.6.
The program solves fixes from satellites at chosen positions in the sky, so that their errors and their geometry can be set one at a time:
"""A receiver's position from pseudoranges: four unknowns, east, north, up and the receiver's clock error, solved from
satellite ranges; then the same range errors under good and poor satellite geometry."""
import math
import random
C = 299_792_458.0 # the speed of light, m/s
RANGE = 22_000_000.0 # a satellite of a 20,000 km orbit is about 20,000 to 26,000 km away; one value keeps it simple
def satellite(azimuth, elevation):
"""A satellite's position east, north and up of the receiver, from its azimuth and elevation in degrees."""
a, e = math.radians(azimuth), math.radians(elevation)
return (RANGE * math.cos(e) * math.sin(a), RANGE * math.cos(e) * math.cos(a), RANGE * math.sin(e))
def solve(sats, pseudoranges, steps=8):
"""Least squares for (east, north, up, clock error in metres): start at the origin and refine."""
x = [0.0, 0.0, 0.0, 0.0]
for _ in range(steps):
rows, residuals = [], []
for s, p in zip(sats, pseudoranges):
d = math.dist(s, x[:3])
rows.append([(x[k] - s[k]) / d for k in range(3)] + [1.0])
residuals.append(p - (d + x[3]))
n = len(rows[0])
ata = [[sum(r[i] * r[j] for r in rows) for j in range(n)] for i in range(n)]
atb = [sum(r[i] * v for r, v in zip(rows, residuals)) for i in range(n)]
step = gauss(ata, atb)
x = [a + b for a, b in zip(x, step)]
return x, rows
def gauss(a, b):
"""Solve a·x = b for a small square system (Gaussian elimination with pivoting)."""
n = len(b)
m = [row[:] + [v] for row, v in zip(a, b)]
for i in range(n):
p = max(range(i, n), key=lambda r: abs(m[r][i]))
m[i], m[p] = m[p], m[i]
for r in range(i + 1, n):
f = m[r][i] / m[i][i]
m[r] = [x - f * y for x, y in zip(m[r], m[i])]
out = [0.0] * n
for i in reversed(range(n)):
out[i] = (m[i][n] - sum(m[i][k] * out[k] for k in range(i + 1, n))) / m[i][i]
return out
def dop(rows):
"""Position and horizontal dilution of precision from the geometry: the square roots of (AᵀA)⁻¹'s diagonal."""
n = len(rows[0])
ata = [[sum(r[i] * r[j] for r in rows) for j in range(n)] for i in range(n)]
inv = [gauss(ata, [1.0 if k == j else 0.0 for k in range(n)]) for j in range(n)] # columns of the inverse
return math.sqrt(inv[0][0] + inv[1][1] + inv[2][2]), math.sqrt(inv[0][0] + inv[1][1])
clock = 0.001 # the receiver's clock is 1 ms fast
print(f"a clock error of 1 ms is {C * clock:,.0f} m of range on every satellite")
spread = [satellite(az, el) for az, el in ((0, 75), (90, 30), (200, 40), (300, 25), (45, 15), (250, 60))]
exact = [math.dist(s, (0, 0, 0)) + C * clock for s in spread]
x, _ = solve(spread[:4], exact[:4])
print(f"four exact pseudoranges: position {math.hypot(x[0], x[1]):.3f} m from the truth, clock {x[3] / C * 1000:.6f} ms")
rng = random.Random(4)
for name, sats in (("six satellites spread over the sky", spread),
("six satellites in one part of the sky", [satellite(az, el) for az, el in ((100, 20), (115, 35), (130, 25), (145, 40), (125, 55), (110, 45))])):
errors = []
for _ in range(200): # 200 fixes, each with new range errors of 3 m (standard deviation)
p = [math.dist(s, (0, 0, 0)) + C * clock + rng.gauss(0, 3.0) for s in sats]
x, rows = solve(sats, p, steps=4)
errors.append(math.hypot(x[0], x[1]))
pdop, hdop = dop(rows)
errors.sort()
print(f"{name}: PDOP {pdop:.1f}, HDOP {hdop:.1f}; horizontal error, 95 % of fixes within {errors[189]:.1f} m")
urban = [s for s in spread] # the spread sky, with one low satellite's signal reflected off a wall
p = [math.dist(s, (0, 0, 0)) + C * clock for s in urban]
p[4] += 25.0 # multipath: the reflected path is 25 m longer
x, _ = solve(urban, p)
print(f"one reflected signal, 25 m too long: the fix moves {math.hypot(x[0], x[1]):.1f} m sideways and {x[2]:+.1f} m up") Output
a clock error of 1 ms is 299,792 m of range on every satellite four exact pseudoranges: position 0.000 m from the truth, clock 1.000000 ms six satellites spread over the sky: PDOP 2.3, HDOP 1.2; horizontal error, 95 % of fixes within 6.2 m six satellites in one part of the sky: PDOP 15.4, HDOP 12.6; horizontal error, 95 % of fixes within 73.2 m one reflected signal, 25 m too long: the fix moves 8.6 m sideways and +14.1 m up
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fix.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
From four exact pseudoranges it finds the position to the millimetre and the clock’s 1 ms. With random range errors of 3 m (one standard deviation), six satellites spread over the sky put 95 % of fixes within 6.2 m; the same six crowded into one part of the sky put them within 73.2 m. The factor between range errors and position errors has a name, DOP (dilution of precision): A number from the satellites' geometry that multiplies range errors into position errors: low when the satellites are spread over the sky, high when they crowd into one part of it., and the geometry alone sets it: for horizontal positions it is 1.2 for the spread sky and 12.6 for the crowded one (HDOP), and in three dimensions 2.3 and 15.4 (PDOP).
Key idea
A fix is four unknowns solved from four or more ranges, and the satellites’ geometry decides how far the range errors throw it: the same 3 m became 6.2 m or 73.2 m.
Checkpoint
Check: Four unknowns and the geometry of a fix
Errors come from the sky, the surroundings and the receiver
GPS.gov names the causes that degrade a user’s accuracy: “signal blockage, atmospheric conditions, and receiver design features/quality”, with signals “reflected off buildings or walls” among them. The satellites themselves are good: the US government commits to a daily global average user range error of 2.0 m or less, with 95 % probability, and puts GPS phones within about a 4.9 m radius under open sky (GPS.gov). The last line of the program shows one wall at work: a reflected signal 25 m too long moved the fix 8.6 m sideways and 14.1 m up.
Better positions come from removing the errors that a nearby receiver shares. A reference station at a known position measures what its ranges should be, and the difference, sent to a moving receiver in real time, as in RTK (real-time kinematic positioning): Positioning in which a receiver applies corrections from a reference station, or a network of them, as it measures, reaching centimetres while the corrections arrive., or applied afterwards, cancels most of the satellite clock, orbit and atmosphere errors the two share. GPS.gov notes that dual-frequency receivers and augmentation systems “can enable real-time positioning within a few centimeters”. QField’s documentation puts a phone’s own GNSS at about 5 m, an external receiver at about 1.5 m and receivers corrected over NTRIP at centimetres (QField). Choose for your own work:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Method | Accuracy | Known in the field | Equipment | Access in India | Trees and buildings |
|---|---|---|---|---|---|
| A phone's own GNSS Typical accuracy: about 5 m | Poor about 5 m in QField's documentation | Good live, with nothing to set up | Good the phone you have | Good nothing to register | Poor one reflected signal moved the lesson fix 8.6 m |
| An external receiver Typical accuracy: about 1.5 m | Mixed about 1.5 m | Good live, sent to the phone | Mixed a receiver to buy and carry | Good nothing to register | Mixed a better antenna, the same blocked sky |
| Network RTK from CORS Typical accuracy: centimetres | Good centimetres while corrections arrive | Good live, with a data connection | Poor an RTK receiver and a correction link | Mixed registration; CORS data is for Indian Entities | Mixed corrections cannot fix signals that are blocked |
| Post-processed with CORS Typical accuracy: centimetres | Good centimetres once the logs are processed | Poor known only after the survey | Mixed a receiver that logs raw data | Mixed registration; CORS data is for Indian Entities | Mixed long sessions help, blocked sky still hurts |
How the answers decide
- “What accuracy does the work need?” Under a metre: plot corners, poles: rules out A phone's own GNSS and adds a caveat to An external receiver. About 5 m from a phone is far too coarse for a plot corner or a pole, and about 1.5 m from an external receiver without corrections is still over a metre.
- “What accuracy does the work need?” Centimetres: a survey or stake-out: rules out A phone's own GNSS and An external receiver. Centimetres need corrections from reference stations, live or after the survey.
- “What accuracy does the work need?” Metres: to find a place again: adds a caveat to Network RTK from CORS and Post-processed with CORS. Centimetre methods cost far more than finding a place again needs.
- “Must each position be known in the field, while you stand on it?” Yes, to navigate or stake out: rules out Post-processed with CORS. Post-processing gives its centimetres only after the logs are processed, not while you stand on the point.
- “Is the work done by an Indian Entity, as India's 2021 guidelines define one?” No, a foreign entity: rules out Network RTK from CORS and Post-processed with CORS. India's 2021 guidelines make CORS data available with ease of access to Indian Entities only.
In short: a phone to find a place again, an external receiver for metre-level points, and corrections from reference stations, live or afterwards, when the work needs centimetres. No correction brings back a signal that a wall or a canopy blocks; plan points with a clear view of the sky.
Checkpoint
Check: Where position errors come from
NavIC and CORS give India its own positioning
NavIC: India's regional navigation satellite system, also called IRNSS: a constellation designed as seven satellites serving India and the region up to 1,500 km beyond it., India’s regional system, is designed for India and the region round it (ISRO):
| NavIC | As ISRO describes it |
|---|---|
| Satellites | 7: three geostationary and four in inclined geosynchronous orbits |
| Geostationary at | 32.5° E, 83° E and 129.5° E |
| Inclined orbits | two pairs crossing the equator at 55° E and 111.75° E, inclined 29° |
| Coverage | India and up to 1,500 km beyond its boundary |
| Services | Standard Position Service for civilians; Restricted Service |
| Bands | L5 (1176.45 MHz) and S (2498.028 MHz); L1 (1575.42 MHz) on satellites from 2023 |
| Design accuracy | better than 20 m in the primary service area |
Survey of India's CORS, and who may use its data
GAGAN augments GPS for aviation over Indian airspace. Survey of India runs a network of CORS (Continuously Operating Reference Stations): GNSS receivers fixed at precisely known positions that record without a break, so that their data can correct other receivers, in real time or afterwards. with procedures for network RTK surveys, DGNSS surveys, online post-processing and data download, after registration (Survey of India). India’s 2021 guidelines make CORS data available “without any restrictions” but “with the ease of access to Indian Entities only” (DST guidelines).
The 20 m figure is a design target for NavIC on its own. A phone that receives NavIC as well as GPS sees more satellites in more directions, which, as the first section showed, lowers the dilution of precision. For work that must be repeatable, such as the corners of a plot, the CORS network ties every visit to the same reference stations.
Checkpoint
Check: NavIC and India's CORS
Field data keeps the accuracy of every point
A position without its accuracy cannot be judged later. QField, the field app for QGIS projects, can block digitising while its accuracy indicator is red (“Enable accuracy requirement”), average several fixes into one vertex, subtract the antenna pole’s height, and turn ellipsoidal heights into orthometric ones with a vertical grid file (QField). Its expression variables can fill fields automatically as default values (QField variables):
| Field to keep with each point | QField variable | What it records |
|---|---|---|
| Horizontal accuracy | @position_horizontal_accuracy |
metres, as the receiver reports it |
| Vertical accuracy | @position_vertical_accuracy |
metres, as the receiver reports it |
| Satellites used | @position_number_of_used_satellites |
the count behind the fix |
| Quality | @position_quality_description |
such as “Fixed RTK” |
| Averaged fixes | @position_averaged_count |
0 when not averaged |
| Time | @position_timestamp |
the fix’s time in UTC |
Tracks travel as GPX: An XML format for GPS data: waypoints, routes and tracks, a track made of segments of points with latitude and longitude in WGS 84 and an optional time., an XML format whose root gpx element must carry version="1.1" and a
creator; a trk holds segments, trkseg, of points, trkpt, with lat and lon in decimal degrees of WGS 84 and an
optional time (GPX 1.1). GPX has hdop and sat but no field for an accuracy
in metres, so keep accuracies in the layer the points live in, such as a GeoPackage. The program writes the cleaned
trips of this lesson’s field day as GPX and reads the file back:
"""The cleaned trips written as GPX 1.1, one track segment per trip, then read back and checked."""
import xml.etree.ElementTree as ET
from day import clock, logged
from steps import drop_poor, drop_spikes, find_stops, split_trips
from utm import from_utm
kept, _ = drop_spikes(drop_poor(logged()))
trips, _ = split_trips(kept, find_stops(kept))
NS = "http://www.topografix.com/GPX/1/1"
ET.register_namespace("", NS)
gpx = ET.Element(f"{{{NS}}}gpx", version="1.1", creator="MySmartCoPilot lesson")
trk = ET.SubElement(gpx, f"{{{NS}}}trk")
ET.SubElement(trk, f"{{{NS}}}name").text = "Field day, cleaned"
for trip in trips:
seg = ET.SubElement(trk, f"{{{NS}}}trkseg") # GPX: a new segment for each continuous span of the track
for t, x, y, _ in trip:
lon, lat = from_utm(x, y)
pt = ET.SubElement(seg, f"{{{NS}}}trkpt", lat=f"{lat:.7f}", lon=f"{lon:.7f}")
ET.SubElement(pt, f"{{{NS}}}time").text = f"2025-11-14T{clock(t)}Z"
ET.indent(gpx, " ")
ET.ElementTree(gpx).write("field_day.gpx", encoding="utf-8", xml_declaration=True)
root = ET.parse("field_day.gpx").getroot()
q = lambda name: f"{{{NS}}}{name}"
segs = root.findall(f"{q('trk')}/{q('trkseg')}")
points = [p for s in segs for p in s.findall(q("trkpt"))]
lats, lons = [float(p.get("lat")) for p in points], [float(p.get("lon")) for p in points]
times = [[p.find(q("time")).text for p in s.findall(q("trkpt"))] for s in segs]
print("field_day.gpx, read back:")
print(f" version {root.get('version')}, creator '{root.get('creator')}'")
print(f" {len(segs)} segments, {len(points)} points; latitude {min(lats):.4f} to {max(lats):.4f}, longitude {min(lons):.4f} to {max(lons):.4f}")
print(f" times in order within every segment: {all(t == sorted(t) for t in times)}")
with open("field_day.gpx", encoding="utf-8") as f:
print(" " + "\n ".join(f.read().splitlines()[:7])) Output
field_day.gpx, read back:
version 1.1, creator 'MySmartCoPilot lesson'
4 segments, 332 points; latitude 18.4871 to 18.5686, longitude 73.8017 to 73.8154
times in order within every segment: True
<?xml version='1.0' encoding='utf-8'?>
<gpx xmlns="http://www.topografix.com/GPX/1/1" version="1.1" creator="MySmartCoPilot lesson">
<trk>
<name>Field day, cleaned</name>
<trkseg>
<trkpt lat="18.4871059" lon="73.8017422">
<time>2025-11-14T08:00:00Z</time>
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 gpx.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Each trip is a segment of its own, because the GPX documentation asks for “a new Track Segment for each continuous span of track data”, and every segment’s times come back in order.
Checkpoint
Check: Field data with its accuracy, and GPX
Cleaning a track: poor fixes, spikes, stops and trips
A logged track mixes movement with three kinds of noise: fixes the receiver itself marks as poor, spikes that jump hundreds of metres for one fix, and the drift of a receiver standing still, which adds distance where none was covered. Speed is the test for a spike: a fix that would need 54 m/s to reach is not where anyone went. The test must compare each fix with the last fix kept, or one spike takes the next good fix with it. Step through it on six fixes:
Program · spike/spike.py
import math
V_MAX = 30 # m/s
fixes = [ # seconds, east m, north m
(0, 0, 0),
(10, 12, 1),
(20, 25, 3),
(30, 470, -300),
(40, 49, 4),
(50, 61, 5),
]
kept = [fixes[0]]
for t, x, y in fixes[1:]:
t0, x0, y0 = kept[-1]
d = math.dist((x, y), (x0, y0))
speed = d / (t - t0)
if speed > V_MAX:
print(f"{t} s: {speed:.1f} m/s, dropped")
continue
kept.append((t, x, y))
print(len(kept), "of", len(fixes), "kept") Variables
- just taken by the loop
- changed in this step
- passed by the loop
- read next
- where the loop stopped
Printed so far
Nothing printed yet30 s: 53.8 m/s, dropped 5 of 6 kept
Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.
The drawing shows the variables when the program ends, and the program's 38 steps are listed below.
Change the program and run it here. Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Every step as text
- Start. No line has run yet, so there are no variables. Python starts with line 1.
- Line 1.
import mathloadsmath. - Line 3.
V_MAX = 30setsV_MAXto30. - Line 4.
fixes = [ # seconds, east m, north m (0, 0, 0), (10, 12, 1), (20, 25, 3), (30, 470, -300), (40, 49, 4), (50, 61, 5), ]makesfixesa list of 6 items. - Line 12.
kept = [fixes[0]]makeskepta list of 1 item. - Line 13.
for t, x, y in fixes[1:]:takes the item at position 0 offixes[1:]:tis10,xis12andyis1. - Line 14.
t0, x0, y0 = kept[-1]setst0to0, setsx0to0and setsy0to0. - Line 15.
d = math.dist((x, y), (x0, y0))setsdto12.041594578792296. - Line 16.
speed = d / (t - t0)setsspeedto1.2041594578792296. - Line 17.
speed > V_MAXis false (speedis1.2041594578792296andV_MAXis30), so Python skips the block underif speed > V_MAX:. - Line 20.
kept.append((t, x, y))adds(10, 12, 1)to the end ofkept. - Line 13.
for t, x, y in fixes[1:]:takes the item at position 1 offixes[1:]:tis20,xis25andyis3. - Line 14.
t0, x0, y0 = kept[-1]setst0to10(it was0), setsx0to12(it was0) and setsy0to1(it was0). - Line 15.
d = math.dist((x, y), (x0, y0))setsdto13.152946437965905(it was12.041594578792296). - Line 16.
speed = d / (t - t0)setsspeedto1.3152946437965904(it was1.2041594578792296). - Line 17.
speed > V_MAXis false (speedis1.3152946437965904andV_MAXis30), so Python skips the block underif speed > V_MAX:. - Line 20.
kept.append((t, x, y))adds(20, 25, 3)to the end ofkept. - Line 13.
for t, x, y in fixes[1:]:takes the item at position 2 offixes[1:]:tis30,xis470andyis-300. - Line 14.
t0, x0, y0 = kept[-1]setst0to20(it was10), setsx0to25(it was12) and setsy0to3(it was1). - Line 15.
d = math.dist((x, y), (x0, y0))setsdto538.3623315203247(it was13.152946437965905). - Line 16.
speed = d / (t - t0)setsspeedto53.83623315203247(it was1.3152946437965904). - Line 17.
speed > V_MAXis true (speedis53.83623315203247andV_MAXis30), so the block underif speed > V_MAX:runs. Key point The fix at 30 s is 538 m from the last kept fix, 10 s earlier, so reaching it needs 53.8 m/s. Nobody in the field moves that fast, so it is a spike. - Line 18.
print(f"{t} s: {speed:.1f} m/s, dropped")prints30 s: 53.8 m/s, dropped. - Line 19.
continueskips the rest of this pass and goes back to the first line of the loop. - Line 13.
for t, x, y in fixes[1:]:takes the item at position 3 offixes[1:]:tis40,xis49andyis4. - Line 14.
t0, x0, y0 = kept[-1]t0keeps its value,20,x0keeps its value,25andy0keeps its value,3. Key point The fix at 40 s is compared with the last fix kept, from 20 s, not with the spike. 24 m in 20 s is 1.2 m/s, so it stays; measured from the spike it would have looked like 51.9 m/s and been dropped too. - Line 15.
d = math.dist((x, y), (x0, y0))setsdto24.020824298928627(it was538.3623315203247). - Line 16.
speed = d / (t - t0)setsspeedto1.2010412149464313(it was53.83623315203247). - Line 17.
speed > V_MAXis false (speedis1.2010412149464313andV_MAXis30), so Python skips the block underif speed > V_MAX:. - Line 20.
kept.append((t, x, y))adds(40, 49, 4)to the end ofkept. - Line 13.
for t, x, y in fixes[1:]:takes the item at position 4 offixes[1:]:tis50,xis61andyis5. - Line 14.
t0, x0, y0 = kept[-1]setst0to40(it was20), setsx0to49(it was25) and setsy0to4(it was3). - Line 15.
d = math.dist((x, y), (x0, y0))setsdto12.041594578792296(it was24.020824298928627). - Line 16.
speed = d / (t - t0)setsspeedto1.2041594578792296(it was1.2010412149464313). - Line 17.
speed > V_MAXis false (speedis1.2041594578792296andV_MAXis30), so Python skips the block underif speed > V_MAX:. - Line 20.
kept.append((t, x, y))adds(50, 61, 5)to the end ofkept. - Line 13.
for t, x, y in fixes[1:]:finds no item at position 5 offixes[1:](it has 5 items), so the loop ends. - Line 21.
print(len(kept), "of", len(fixes), "kept")prints5 of 6 kept. That was the last line, so the program ends. Five of the six fixes are kept. Comparing each fix with the last one kept is what stops one spike from taking good fixes with it.
The lesson’s field day, 636 fixes 10 s apart, has five planted spikes, two stops, a stretch near tall buildings and an
underpass with no signal. MovingPandas offers the same steps as OutlierCleaner, which cuts spikes above a speed
(MovingPandas), and
TrajectoryStopDetector, which finds stops within a diameter for a duration
(MovingPandas); here they are written
out:
"""A synthetic field day as a phone logs it: one fix every 10 s, in metres of UTM zone 43N, with the receiver's
accuracy estimate, planted spikes, two stops with the drift of a receiver standing still, and a tunnel with no fixes.
Each fix is (seconds since 08:00 UTC, east, north, accuracy in m). The true path is known, so cleaning can be checked.
"""
import math
import random
START = (373_500.0, 2_044_500.0)
LEGS = [ # (what, seconds, speed in m/s, heading in degrees clockwise from north)
("walk to the first field", 1_200, 1.2, 90),
("stop at the field", 1_800, 0.0, 0),
("ride north", 900, 10.0, 0),
("stop at the market", 1_200, 0.0, 0),
("walk back", 1_500, 1.4, 200),
]
TUNNEL = (3_400, 3_640) # seconds with no fix: an underpass on the ride north
SPIKES = {640: (420.0, -310.0), 2_210: (-650.0, 120.0), 3_250: (300.0, 840.0), 4_120: (-280.0, -520.0), 5_400: (510.0, 380.0)}
BUILDINGS = (4_600, 4_900) # seconds near tall buildings: poor accuracy, larger errors
def truth():
"""[(t, east, north, leg)] every 10 s along the true path."""
out, (x, y), t = [], START, 0
for what, seconds, speed, heading in LEGS:
for _ in range(seconds // 10):
out.append((t, x, y, what))
x += 10 * speed * math.sin(math.radians(heading))
y += 10 * speed * math.cos(math.radians(heading))
t += 10
return out
def logged(seed=11):
"""The fixes the phone logs: truth plus noise, spikes and gaps, each with its accuracy estimate."""
rng = random.Random(seed)
out = []
for t, x, y, _ in truth():
if TUNNEL[0] <= t < TUNNEL[1]:
continue
poor = BUILDINGS[0] <= t < BUILDINGS[1]
sigma, accuracy = (9.0, round(rng.uniform(18, 30), 1)) if poor else (2.5, round(rng.uniform(3.5, 6.0), 1))
dx, dy = SPIKES.get(t, (0.0, 0.0))
out.append((t, x + dx + rng.gauss(0, sigma), y + dy + rng.gauss(0, sigma), accuracy))
return out
def clock(t):
"""Seconds since 08:00 as a time of day."""
return f"{8 + t // 3600:02d}:{t // 60 % 60:02d}:{t % 60:02d}" """The four cleaning steps of a GPS track, each a small function over fixes (t, east, north, accuracy)."""
import math
def length(points):
"""The length of a track in metres, fix to fix."""
return sum(math.dist(a[1:3], b[1:3]) for a, b in zip(points, points[1:]))
def drop_poor(fixes, worst=20.0):
"""Keep the fixes whose accuracy estimate is `worst` metres or better."""
return [f for f in fixes if f[3] <= worst]
def drop_spikes(fixes, v_max=30.0):
"""Keep a fix only if reaching it from the last kept fix needs no more than v_max m/s; return (kept, dropped)."""
kept, dropped = [], []
for f in fixes:
if kept and math.dist(f[1:3], kept[-1][1:3]) / (f[0] - kept[-1][0]) > v_max:
dropped.append(f)
else:
kept.append(f)
return kept, dropped
def find_stops(points, diameter=60.0, minimum=300):
"""(first, last) index of every run that stays within `diameter` m of its first fix for `minimum` s or more."""
out, i = [], 0
while i < len(points):
j = i
while j + 1 < len(points) and math.dist(points[j + 1][1:3], points[i][1:3]) <= diameter / 2:
j += 1
if points[j][0] - points[i][0] >= minimum:
out.append((i, j))
i = j + 1
else:
i += 1
return out
def split_trips(points, stops, gap=120):
"""Trips between stops, also split where no fix came for more than `gap` s; returns (trips, gap lengths in m)."""
stopped = {k: (i, j) for i, j in stops for k in range(i, j + 1)}
trips, current, gaps = [], [], []
for k, f in enumerate(points):
if k in stopped: # a trip ends at a stop's first fix, and the next one starts at its last
i, j = stopped[k]
if k == i and current:
trips.append(current + [f])
current = []
if k == j:
current = [f]
continue
if current and f[0] - current[-1][0] > gap:
trips.append(current)
gaps.append(math.dist(current[-1][1:3], f[1:3]))
current = []
current.append(f)
if len(current) > 1:
trips.append(current)
return trips, gaps """UTM zone 43N on WGS 84, both ways, and the size of a degree on the ground: what the lesson's programs reproject with.
The transverse Mercator projection is computed with Krüger's series to the sixth order of the ellipsoid's third
flattening, as Karney (2011) gives it, which is good to well under a millimetre across the zone.
"""
import math
A, F = 6_378_137.0, 1 / 298.257223563 # WGS 84 semi-major axis (m) and flattening
K0, LON0, FALSE_EASTING = 0.9996, math.radians(75.0), 500_000.0 # zone 43N: central meridian 75° E
E = math.sqrt(F * (2 - F)) # eccentricity
N = F / (2 - F) # third flattening
RECT = A / (1 + N) * (1 + N**2 / 4 + N**4 / 64 + N**6 / 256) # the rectifying radius
ALPHA = [
N / 2 - 2 * N**2 / 3 + 5 * N**3 / 16 + 41 * N**4 / 180 - 127 * N**5 / 288 + 7891 * N**6 / 37800,
13 * N**2 / 48 - 3 * N**3 / 5 + 557 * N**4 / 1440 + 281 * N**5 / 630 - 1983433 * N**6 / 1935360,
61 * N**3 / 240 - 103 * N**4 / 140 + 15061 * N**5 / 26880 + 167603 * N**6 / 181440,
49561 * N**4 / 161280 - 179 * N**5 / 168 + 6601661 * N**6 / 7257600,
34729 * N**5 / 80640 - 3418889 * N**6 / 1995840,
212378941 * N**6 / 319334400,
]
BETA = [
N / 2 - 2 * N**2 / 3 + 37 * N**3 / 96 - N**4 / 360 - 81 * N**5 / 512 + 96199 * N**6 / 604800,
N**2 / 48 + N**3 / 15 - 437 * N**4 / 1440 + 46 * N**5 / 105 - 1118711 * N**6 / 3870720,
17 * N**3 / 480 - 37 * N**4 / 840 - 209 * N**5 / 4480 + 5569 * N**6 / 90720,
4397 * N**4 / 161280 - 11 * N**5 / 504 - 830251 * N**6 / 7257600,
4583 * N**5 / 161280 - 108847 * N**6 / 3991680,
20648693 * N**6 / 638668800,
]
def to_utm(lon, lat):
"""Easting and northing (m) in UTM zone 43N of a longitude and latitude in degrees."""
phi, lam = math.radians(lat), math.radians(lon) - LON0
t = math.sinh(math.atanh(math.sin(phi)) - E * math.atanh(E * math.sin(phi)))
xi0, eta0 = math.atan2(t, math.cos(lam)), math.atanh(math.sin(lam) / math.sqrt(1 + t * t))
xi = xi0 + sum(a * math.sin(2 * j * xi0) * math.cosh(2 * j * eta0) for j, a in enumerate(ALPHA, 1))
eta = eta0 + sum(a * math.cos(2 * j * xi0) * math.sinh(2 * j * eta0) for j, a in enumerate(ALPHA, 1))
return FALSE_EASTING + K0 * RECT * eta, K0 * RECT * xi
def from_utm(x, y):
"""Longitude and latitude in degrees of an easting and northing in UTM zone 43N."""
xi, eta = y / (K0 * RECT), (x - FALSE_EASTING) / (K0 * RECT)
xi0 = xi - sum(b * math.sin(2 * j * xi) * math.cosh(2 * j * eta) for j, b in enumerate(BETA, 1))
eta0 = eta - sum(b * math.cos(2 * j * xi) * math.sinh(2 * j * eta) for j, b in enumerate(BETA, 1))
tau0 = math.sin(xi0) / math.sqrt(math.sinh(eta0) ** 2 + math.cos(xi0) ** 2)
tau = tau0
for _ in range(5): # Newton's method for the conformal latitude
s = math.sinh(E * math.atanh(E * tau / math.sqrt(1 + tau * tau)))
guess = tau * math.sqrt(1 + s * s) - s * math.sqrt(1 + tau * tau)
tau += (tau0 - guess) / math.sqrt(1 + guess * guess) * (1 + (1 - E * E) * tau * tau) / ((1 - E * E) * math.sqrt(1 + tau * tau))
return math.degrees(LON0 + math.atan2(math.sinh(eta0), math.cos(xi0))), math.degrees(math.atan(tau))
def metres_per_degree(lat):
"""(east-west, north-south) length on the ground of one degree at a latitude, on the ellipsoid."""
phi = math.radians(lat)
w = 1 - E * E * math.sin(phi) ** 2
across = A * math.cos(phi) / math.sqrt(w) # radius of the parallel
along = A * (1 - E * E) / w**1.5 # radius of curvature of the meridian
return math.radians(1) * across, math.radians(1) * along """Cleaning the field day: drop the fixes the receiver marks as poor, drop spikes by speed, find the stops, and split
the rest into trips at stops and gaps, checked against the true path."""
from day import SPIKES, clock, logged, truth
from steps import drop_poor, drop_spikes, find_stops, length, split_trips
fixes = logged()
print(f"{len(fixes):,} fixes logged, every 10 s, from {clock(fixes[0][0])} to {clock(fixes[-1][0])} UTC")
print(f"true path: {length(truth()) / 1000:.2f} km; the raw fixes add up to {length(fixes) / 1000:.2f} km")
good = drop_poor(fixes)
print(f"accuracy 20 m or better: {len(good):,} fixes kept, {len(fixes) - len(good)} dropped")
kept, spikes = drop_spikes(good)
print(f"faster than 30 m/s from the last kept fix: {len(spikes)} spikes dropped, at "
f"{', '.join(clock(f[0]) for f in spikes)}; {len(SPIKES)} were planted")
stops = find_stops(kept)
for i, j in stops:
print(f"stop: {clock(kept[i][0])} to {clock(kept[j][0])}, {j - i + 1} fixes, drifting {length(kept[i : j + 1]):,.0f} m while standing still")
trips, gaps = split_trips(kept, stops)
for t in trips:
print(f"trip: {clock(t[0][0])} to {clock(t[-1][0])}, {length(t) / 1000:.2f} km, {len(t)} fixes")
moving = sum(map(length, trips))
print(f"trips: {moving / 1000:.2f} km split at the gap; {(moving + sum(gaps)) / 1000:.2f} km with the gap bridged "
f"by a straight line of {sum(gaps):,.0f} m") Output
636 fixes logged, every 10 s, from 08:00:00 to 09:49:50 UTC true path: 12.53 km; the raw fixes add up to 20.47 km accuracy 20 m or better: 608 fixes kept, 28 dropped faster than 30 m/s from the last kept fix: 5 spikes dropped, at 08:10:40, 08:36:50, 08:54:10, 09:08:40, 09:30:00; 5 were planted stop: 08:19:50 to 08:50:00, 181 fixes, drifting 774 m while standing still stop: 09:05:00 to 09:25:20, 94 fixes, drifting 388 m while standing still trip: 08:00:00 to 08:19:50, 1.48 km, 119 fixes trip: 08:50:00 to 08:56:30, 3.90 km, 39 fixes trip: 09:00:40 to 09:05:00, 2.60 km, 27 fixes trip: 09:25:20 to 09:49:50, 2.14 km, 147 fixes trips: 10.12 km split at the gap; 12.63 km with the gap bridged by a straight line of 2,507 m
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 clean.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The accuracy filter drops the 28 fixes near the buildings, the speed filter finds all five spikes, and the stop detector finds the two stops, whose drift of 774 m and 388 m would otherwise count as distance. The four trips add up to 10.12 km. That is short of the true 12.53 km because the underpass split the ride: bridging its four minutes with a straight line of 2,507 m gives 12.63 km. Whether to bridge a gap is a decision about the road, not the data, so record it with the result.
Measuring a track before cleaning it
The raw fixes add up to 20.47 km for a day of 12.53 km. Summing distances fix to fix counts every spike twice, once out and once back, and every metre of drift while standing still. Drop poor fixes and spikes and take the stops out first, then measure the trips.
Checkpoint
Check: Cleaning a track into trips
A track is personal data
A track shows where someone sleeps, works and stops. Europe’s GDPR counts “location data” among the identifiers that make information personal data: Information about an identified or identifiable person. The GDPR names location data among the identifiers through which a person can be identified. (GDPR, Article 4). A study of the movements of 1.5 million mobile phone users over 15 months found that four points of place and time singled out 95 % of them (de Montjoye and others). The field day’s two stops, a field and a market, are exactly such points.
Share tracks only as far as the purpose needs:
| Purpose | Share | Leave out |
|---|---|---|
| A mileage claim | each trip’s length and times | the route and the stops |
| A map of where teams worked | fixes counted per grid cell, small counts suppressed | every route and every stop |
| A person’s own record | the full track, to that person only | nothing |
Cut the start and end of every trip near a home, remove the times when they are not needed, and suppress small counts, as the lesson on web map privacy does for cases. Photos taken in the field carry the same risk in their location tags.
Image Metadata Viewer See which location and time a photo from the field carries before you share it.Checkpoint
Check: Tracks as personal data
Practise: clean a track
Two steps turn this lesson’s cleaning into code you can reuse: drop spikes by speed against the last kept fix, then find the stops in what is left. Each step has sample tests on small tracks:
Code lab · 2 steps
Clean a GPS track
Drop the spikes of a track by speed, then find the stops in what is left.
Exercise · Easy · Python
Drop the spikes of a GPS track by speed
A track is a list of fixes (t, east, north): seconds, and metres in a projected CRS, in time order.
Write drop_spikes(fixes, v_max), which returns (kept, dropped), two lists of fixes. The first fix is always kept. Every later fix is kept if reaching it from the last kept fix needs a speed of v_max metres a second or less, and dropped otherwise.
The sample tests use the lesson's six fixes with one spike, two spikes in a row, and a slower limit for a walker.
Starter code · spikes.py
import math
def drop_spikes(fixes, v_max):
"""(kept, dropped): keep each fix reachable from the last kept fix at v_max m/s or less."""
# Replace this line: compare every fix with the last kept one.
return list(fixes), [] The sample tests · test_spikes.py
from spikes import drop_spikes
LESSON = [(0, 0, 0), (10, 12, 1), (20, 25, 3), (30, 470, -300), (40, 49, 4), (50, 61, 5)]
def test_lesson_spike():
"""drops the lesson's spike and keeps the fix after it"""
kept, dropped = drop_spikes(LESSON, 30)
assert dropped == [(30, 470, -300)]
assert len(kept) == 5 and kept[3] == (40, 49, 4)
def test_two_spikes_in_a_row():
"""two spikes in a row are both dropped, and the walk goes on"""
track = [(0, 0, 0), (10, 10, 0), (20, 900, 0), (30, 905, 2), (40, 30, 0)]
kept, dropped = drop_spikes(track, 30)
assert [f[0] for f in dropped] == [20, 30]
assert [f[0] for f in kept] == [0, 10, 40]
def test_walker_limit():
"""a lower limit for a walker drops a jump that a rider's limit keeps"""
track = [(0, 0, 0), (10, 15, 0), (20, 160, 0)]
assert len(drop_spikes(track, 30)[0]) == 3
assert drop_spikes(track, 3)[1] == [(20, 160, 0)]
def test_first_fix_kept():
"""the first fix is always kept"""
assert drop_spikes([(0, 5_000, 5_000)], 1) == ([(0, 5_000, 5_000)], []) A hint
Keep the last kept fix in kept[-1]. The speed to a new fix is math.dist((x, y), (x0, y0)) / (t - t0); compare it with v_max, then append the fix to kept or to dropped.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Exercise · Medium · Python
Find the stops of a GPS track
A track is a list of fixes (t, east, north) in time order: seconds, and metres in a projected CRS.
Write find_stops(fixes, diameter, minimum), which returns a list of (start, end) times, one for each stop. A stop begins at a fix and lasts while the following fixes stay within diameter / 2 metres of that first fix; it counts only if it lasts minimum seconds or more, from its first fix to its last. After a stop, look for the next one from the fix after it; after a run too short to be a stop, try again from the next fix.
The sample tests use a walk with one stop of 6 minutes with drift and a pause of 2 minutes that is not a stop, a diameter too small for the drift, and a slow drift whose stops must follow one another without overlapping.
Starter code · stops.py
import math
def find_stops(fixes, diameter, minimum):
"""[(start, end)] of every run that stays within diameter / 2 m of its first fix for minimum s or more."""
# Replace this line: grow a run from each fix while the fixes stay close to its first one.
return [] The sample tests · test_stops.py
from stops import find_stops
def walk(t0, x0, seconds, speed):
"""Fixes every 10 s of a walk east."""
return [(t0 + k, x0 + speed * k, 0.0) for k in range(0, seconds, 10)]
def stay(t0, x0, seconds, drift):
"""Fixes every 10 s standing near x0, drifting by up to `drift` m."""
return [(t0 + k, x0 + (drift if k % 20 else -drift), drift / 2) for k in range(0, seconds, 10)]
TRACK = walk(0, 0, 300, 1.4) + stay(300, 420, 360, 8) + walk(660, 420, 300, 1.4) + stay(960, 840, 120, 5) + walk(1080, 840, 200, 1.4)
def test_one_stop():
"""finds the 6-minute stop, with the walk's fixes just before and after it, and not the 2-minute pause"""
assert find_stops(TRACK, 60, 300) == [(290, 670)]
def test_short_minimum():
"""with a minimum of 100 s the pause counts too"""
assert find_stops(TRACK, 60, 100) == [(290, 670), (950, 1090)]
def test_tight_diameter():
"""a diameter smaller than the drift finds no stop"""
assert find_stops(TRACK, 10, 300) == []
def test_empty():
"""an empty track has no stops"""
assert find_stops([], 60, 300) == []
def test_next_stop_starts_after_the_last():
"""after a stop the search goes on from the next fix, so a slow drift gives stops that never overlap"""
drift = [(10 * k, 5.0 * k, 0.0) for k in range(20)]
assert find_stops(drift, 60, 60) == [(0, 60), (70, 130)] A hint
Use two indices: i for the fix a stop would start at, and j moving forward while math.dist(fixes[j + 1][1:], fixes[i][1:]) <= diameter / 2. If fixes[j][0] - fixes[i][0] >= minimum, record the stop and jump to j + 1; if not, move i on by one.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
The self-check below covers all six ideas, from the four unknowns to sharing a track, and starts with the ones your answers have been weakest on:
Practice that adapts to you
Practise this lesson
11 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): Why does a GNSS receiver need at least four satellites for a position?
Each satellite gives one range, but the receiver’s clock is not exact, so every range is off by the same unknown amount: 1 ms of clock error is 299,792 m. The receiver must solve its east, north and up position and that clock error together, four unknowns, which needs four ranges at least. More satellites let it solve by least squares, and the more spread they are over the sky, the less their range errors move the fix: with the same 3 m errors, 95 % of the lesson’s fixes were within 6.2 m with spread satellites and within 73.2 m with crowded ones.
Warm-up (fresher to mid-level): A phone says a point’s accuracy is 5 m. What should you store with the point?
The accuracy itself, in metres, as the receiver reported it, with the time of the fix, the number of satellites used
and how many fixes were averaged. In QField those are the default values @position_horizontal_accuracy,
@position_timestamp, @position_number_of_used_satellites and @position_averaged_count. Without them nobody can
later tell a careful point from a quick one, or decide whether two points 4 m apart are the same place.
Two longer questions start from this lesson, from pillars that move between visits to riders’ distances that come out too long; the bank gives what each tests and a first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
Field staff recorded the same 40 boundary pillars with their phones on two days, and the coordinates differ by 3 to 12 m. Which day is right, and how should the pillars be recorded so that the next visit agrees?
-
Mid to senior
Delivery riders' GPS logs give daily distances about 40 % above their odometers, and some trips show jumps across the city in ten seconds. Explain both, and design the cleaning that makes the distances usable for paying riders.
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Why does a GNSS receiver need at least four satellites for a fix?
Its clock is cheap, and 1 ms of clock error adds 299,792 m to every range, so each range is a pseudorange. The receiver solves east, north, up and its own clock error together: four unknowns need at least four ranges.
-
What is dilution of precision, and how much can satellite geometry matter?
The factor that turns range errors into position errors, set by the satellites' geometry alone. With 3 m range errors, six satellites spread over the sky put 95 % of fixes within 6.2 m (HDOP 1.2); the same six crowded into one part of the sky, within 73.2 m (HDOP 12.6).
-
How do reference stations improve a GNSS position, and what can they not fix?
A station at a known position measures what its ranges should be, and the differences, sent live as in RTK or applied afterwards, cancel most of the satellite clock, orbit and atmosphere errors a nearby receiver shares, down to centimetres. No correction restores a signal a wall or a canopy blocks.
-
What is NavIC?
India's regional navigation system: 7 satellites, three geostationary and four in inclined geosynchronous orbits, covering India and up to 1,500 km beyond, with a design accuracy better than 20 m in its primary service area. A phone that adds NavIC to GPS sees more satellites, which lowers the dilution of precision.
-
What does Survey of India's CORS network offer, and who may use its data?
Continuously Operating Reference Stations for network RTK and DGNSS surveys, online post-processing and data download, after registration. India's 2021 guidelines make CORS data available without restrictions, with the ease of access for Indian Entities only.
-
What should be stored with every field point, and where?
Its horizontal and vertical accuracy, the satellites used, the fix quality, the number of averaged fixes and the time, filled in QField by variables such as @position_horizontal_accuracy and @position_timestamp. GPX has hdop and sat but no accuracy in metres, so keep them in the layer, such as a GeoPackage.
-
Why did a 12.53 km field day log as 20.47 km, and how is the track cleaned?
Summing fix to fix counts each spike twice, out and back, and every metre of drift while standing still. Drop the fixes the receiver marks as poor, drop spikes by testing speed against the last fix kept, take out the stops, then measure the trips and record how gaps were bridged.
-
Why is a GPS track personal data, and how much of it should be shared?
It shows where someone sleeps, works and stops, and in one study four points of place and time singled out 95 % of 1.5 million phone users. Share only what the purpose needs: trip lengths and times for a mileage claim, counts per grid cell with small counts suppressed for a map of where teams worked.
Key takeaways
- A receiver solves four unknowns, its position and its clock error, from four or more pseudoranges, and the satellites’ geometry scales range errors into position errors.
- Errors come from blocked and reflected signals, the atmosphere and the receiver; reference stations remove the errors a nearby receiver shares, down to centimetres.
- NavIC covers India and 1,500 km beyond with a design accuracy better than 20 m, and Survey of India’s CORS data is available to Indian Entities.
- Store the accuracy, time and satellites of every field point with it; GPX carries tracks, one segment per continuous span.
- Clean a track before measuring it: drop poor fixes and spikes against the last kept fix, take out the stops, and record how gaps were treated.
- A track is personal data: share only what the purpose needs, aggregated where you can.
Terms in this lesson
- CORS (Continuously Operating Reference Stations)
- GNSS receivers fixed at precisely known positions that record without a break, so that their data can correct other receivers, in real time or afterwards.
- DOP (dilution of precision)
- A number from the satellites' geometry that multiplies range errors into position errors: low when the satellites are spread over the sky, high when they crowd into one part of it.
- GNSS (global navigation satellite system)
- A system of satellites that broadcast their time and orbit so that a receiver can work out its position and clock: GPS, NavIC, Galileo and BeiDou are examples.
- GPX
- An XML format for GPS data: waypoints, routes and tracks, a track made of segments of points with latitude and longitude in WGS 84 and an optional time.
- NavIC
- India's regional navigation satellite system, also called IRNSS: a constellation designed as seven satellites serving India and the region up to 1,500 km beyond it.
- pseudorange
- A receiver's measured distance to a satellite, the signal's travel time times the speed of light, which includes the error of the receiver's own clock.
- RTK (real-time kinematic positioning)
- Positioning in which a receiver applies corrections from a reference station, or a network of them, as it measures, reaching centimetres while the corrections arrive.
References
- GPS Accuracy (National Coordination Office for Space-Based Positioning, Navigation, and Timing (GPS.gov))
- Satellite Navigation Services (NavIC and GAGAN) (Indian Space Research Organisation)
- IRNSS Programme (Indian Space Research Organisation)
- Continuously Operating Reference Stations (CORS) (Survey of India)
- Guidelines for acquiring and producing geospatial data and geospatial data services including maps (Department of Science and Technology, Government of India)
- All about GPS, GNSS and NTRIP (QField documentation) (OPENGIS.ch (QField))
- Expression variables (QField documentation) (OPENGIS.ch (QField))
- GPX 1.1 Schema Documentation (TopoGrafix)
- TrajectoryCleaner (MovingPandas documentation) (MovingPandas developers)
- TrajectoryStopDetector (MovingPandas documentation) (MovingPandas developers)
- Regulation (EU) 2016/679 (GDPR), Article 4: definitions (Publications Office of the European Union)
- Unique in the Crowd: The privacy bounds of human mobility (Scientific Reports (de Montjoye, Hidalgo, Verleysen and Blondel))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress