Create GPS Test Data In Go
Tags: go, programming, datascience
Published: 2024-04-24 15:43:50

Many of the functions I'm working on consume arrays of points (GPS tracks) `[]float64{lng,lat}` on which to run statistical analysis. Individual tracks can have 50,000+ points, describing a real journey from A to B.

Testing functions that process GPS tracks has been unexpectedly difficult. Test data of the form `[1.0,2.0]`, to test logically is fine. But beyond that, I want to be able to test consistency in things like finding clusters, or coefficient breakpoints.

For testing I'm not concerned with where the locations are on earth, but it's handy to be able to view tracks on a map for visual confirmation. So the coordinates need to, sort of, coordinate.

I've created a function that generates semi-coherent GPS location data tracks. It's trivial to create a 5,000 data input track, designed to test something in particular. Being able to export it as geoJSON and view the shape on map is good for speedy intuition checks.

I can setup the data to be skewed one way or another, or insert a cluster of weirdness. Particularly useful at larger data sizes, where one analysis method may be better than another.

```
func CreateGPSData() {

        line := orb.LineString{orb.Point{-3.188267, 55.953251}}

        // general direction and distance per point
        bearingRange := [2]int{Direction_SSE, Direction_SSW}
        distanceRange := [2]int{10 * 1000, 15 * 1000}

        // generate a test GPS track
        for range 5000 {
                newPoint := generateNewLocation(line[len(line)-1],
                        bearingRange,
                        distanceRange)
                line = append(line, newPoint)
        }

        // add skewness in the data
        bearingRange = [2]int{Direction_W, Direction_WNW}
        distanceRange = [2]int{1000, 1500}
        for range 100 {
                newPoint := generateNewLocation(line[len(line)-1],
                        bearingRange,
                        distanceRange)
                line = append(line, newPoint)
        }

        // do testing
}
```

What doesn't seem to be too common is generating a new point, some distance away in a particular direction. The opposite of `point.DistanceTo(point2)`. That's really all that the following function does.

The breakdown starts with some readability. Using compass rose names really helps with reading the calling function.

```
// the compass rose, naming format for readability
const (
        Direction_N = iota
        Direction_NNE
        Direction_NE
        Direction_ENE
        Direction_E
        Direction_ESE
        Direction_SE
        Direction_SSE
        Direction_S
        Direction_SSW
        Direction_SW
        Direction_WSW
        Direction_W
        Direction_WNW
        Direction_NW
        Direction_NNW
)

const (
        compassRoseDegrees = 22.5
)
```

Rather than simply a series of linear points, I want a GPS track that wiggles a little. So the parameters are a range of direction and distance, giving a small amount of randomness when calculating the next point.

```
// generateNewLocation returns a new point in the range of direction and
// distance. It is meant to build non-repetitive but predictable GPS tracks, to
// help generate test input cases.
//
// It's also meant to be readable code.
func generateNewLocation(start orb.Point, direction [2]int, distance [2]int) orb.Point {
```

Possibly the most common _stupid_ mistakes I make are mixing up longitude and latitude indexes through typos. Named index values solve this.

```
// Mistakes with lon/lat indexing area easy to make, explicit index names
// helps
const (
    Longitude = 0
    Latitude  = 1
)
```

I use the excellent [orb](https://github.com/paulmach/orb) package to hide much of the GPS specific calculations. 

A point to note is that the Go maths package works in radians, so here's a helper function.

```
var (
	latitudeOneDegreeOfDistance = 111000  // metres
	newPoint                    orb.Point // []float64{Long, Lat}

	// convert from degrees to radians
	deg2rad = func(d float64) float64 { return d * math.Pi / 180 }
)
```

A while ago I went back to study maths, and have found lots of use for it. Here using trig to workout the distances on the ground.

```
 // Use trigonometry of a right angled triangle to solve the distances on the ground.
// The hypotenuse is our desired distance to travel,  and one angle
// is our desired bearing.
//
// now work out the vertical (longitude) and horizontal (latitude) sides in
// distance units.
hyp := (rand.Float64() * float64(distance[1]-distance[0])) + float64(distance[0])

// Get the compass bearing in degrees, with a little randomness between the
// general direction. Non-linear tracks are easier to troubleshoot visually.
bearingMin := float64(direction[0]) * 22.5
bearingMax := float64(direction[1]) * 22.5
angle := (rand.Float64() * (bearingMax - bearingMin)) + bearingMin

// Calulate the other side lengths using SOH CAH TOA. The Go math package
// works in radians
adj := math.Cos(deg2rad(angle)) * hyp // adjacent side of angle
opp := math.Sin(deg2rad(angle)) * hyp // opposite side of angle
```

Each degree change in latitude moves a fixed distance on the earth surface. So it is fairly simple to find the degree change we need to move our required distance on the ground.

```
// Each degree change in every latitude equates to ~111 km on the ground. So
// now find the degree change required for the length of adj
latitudeDelta := (1.0 / float64(latitudeOneDegreeOfDistance)) * adj
newPoint[Latitude] = start[Latitude] + latitudeDelta
```

Longitude 'distance moved on the earth surface' on the other hand changes depending on the corresponding latitude. 

First need to find what one longitude degree of travel is, at the current latitude. This is the longitude equivalent to `latitudeOneDegreeOfDistance = 111000`. 

```
// Distance on the ground for each degree of longitude changes depending on
// latitude because the earth is not perfectly spherical. So we need to
// calculate the distance of one degree longitude for our current latitude.
p1 := orb.Point{1.0, start[Latitude]}
p2 := orb.Point{2.0, start[Latitude]}
longitudeOneDegreeOfDistance := geo.Distance(p1, p2) // returns metres
```

After which it's the same calculation to find the longitude degree required to move our required distance on the ground.

```
// Now we can use this value to calculate the longitude degree change
// required to move opp distance (in a horizontal straight line) at this
// latitude.
longitudeDelta := (1.0 / longitudeOneDegreeOfDistance) * opp
```

Now we have the new point, located roughly at the direction and distance required.

```
// The new point is a vertical and horizontal shift to arrive at hyp
// distance from the start point on the required bearing.
newPoint[Longitude] = start[Longitude] + longitudeDelta

return newPoint
```

To print the geoJSON object, which can be viewed using the [Mapbox](https://geojson.io) viewer. The `geojson` package is part of `orb`.

```
fc := geojson.NewFeatureCollection()
f := geojson.NewFeature(line)
fc.Append(f)

rawJSON, _ := fc.MarshalJSON()
fmt.Println(string(rawJSON))
```

The geoJSON output, which can be dropped right into the [Mapbox viewer](https://geojson.io).

```
{"features":[{"type":"Feature","geometry":{"type":"LineString","coordinates":[[-3.188267,55.953251],[-3.135746915373942,55.86790144687853],[-3.1203515713600174,55.77480489552086],[-3.1047549460145367,55.67255911652837],[-3.157869557083529,55.55060126862168],[-3.228498091310353,55.43475670091365],[-3.2496225715462885,55.436812333443044],[-3.2661004391340707,55.439707415244094],[-3.2813563020992547,55.44313063594729],[-3.3021255075874594,55.44562577470332],[-3.319279338837285,55.446998922512265]]},"properties":{"stroke":"#b464d6","stroke-width":3}}],"type":"FeatureCollection"}
```

The full code is in a Gist [here](https://gist.github.com/thisdougb/e8da04a51947e5e5146c3fe5968a55f0).