Get hiking suggestions from your recorded tours

Starting Point

For the contents of the next two columns, with index numbers 4 and 5, the R script looks for the latitude and longitude of the tour's starting point. Because the GPX data is available as a dataframe, this is a piece of cake. R just addresses the first row with an index of 1 and uses the column name as the column index. This means that line 16 only has to ask for [1, "Latitude"] in the GPX dataframe to get the latitude of the first waypoint as a numerical value. The process for the geographical longitude is the same; lines 18 and 19 insert new columns, numbered 4 and 5, into the resulting idnames dataframe.

I am still missing the duration of the tour, which is determined by the code section starting in line 20 and then inserted into the resulting dataframe. The duration is calculated from the difference between the last and the first timestamps in the GPX file. Line 21 fetches the first line as index number 1 along with "Time", which is the value in the column with the timestamps. The last entry from the GPX dataframe is determined in line 22 by R's standard tail() function with a parameter of 1 (meaning you only want the last element). The time column extraction method is the analog to determining the start time.

Beginning and End

R's difftime() function computes the difference between two timestamps. For a result in minutes, line 23 calls R's standard as.numeric() function with the units="mins" parameter. The return value is a floating-point number with fractions of minutes, which the standard round() function rounds to the nearest integer with a precision of   (zero decimal places). That takes care of the tour duration, and lines 24 and 25 insert the value in column 6 into the resulting dataframe under the "mins" column header.

Finally, write.csv() writes the whole enchilada in CSV format to standard output, which the user redirects to the tour-data.csv file, as metadata to later enable automatic and fast filtering. The Go hikefind program, which I will be explaining in a minute, grabs the results from the file and applies its user-configured filters. To do this, Listing 4 uses readCSV() to read the metadata into memory by placing the individual entries into an array slice with elements of the Tour type. Defined starting in line 10, the elements of this type store all the important metadata, such as the duration, meters of altitude, and starting point.

Listing 4

csvread.go

01 package main
02 import (
03   "encoding/csv"
04   "fmt"
05   "io"
06   "os"
07   "strconv"
08 )
09 const csvFile = "tour-data.csv"
10 type Tour struct {
11   name string
12   file string
13   gain int
14   lat  float64
15   lng  float64
16   mins int
17 }
18 func readCSV() ([]Tour, error) {
19   _, err := os.Stat(csvFile)
20   f, err := os.Open(csvFile)
21   if err != nil {
22     panic(err)
23   }
24   tours := []Tour{}
25   r := csv.NewReader(f)
26   firstLine := true
27   for {
28     record, err := r.Read()
29     if err == io.EOF {
30       break
31     }
32     if err != nil {
33       fmt.Printf("Error\n")
34       return tours, err
35     }
36     if firstLine {
37       // skip header
38       firstLine = false
39       continue
40     }
41     gain, err := strconv.ParseFloat(record[3], 32)
42     panicOnErr(err)
43     lat, err := strconv.ParseFloat(record[4], 64)
44     panicOnErr(err)
45     lng, err := strconv.ParseFloat(record[5], 64)
46     panicOnErr(err)
47     mins, err := strconv.ParseInt(record[6], 10, 64)
48     panicOnErr(err)
49     tour := Tour{
50       name: record[2],
51       gain: int(gain),
52       lat:  lat,
53       lng:  lng,
54       mins: int(mins)}
55     tours = append(tours, tour)
56   }
57   return tours, nil
58 }
59 func panicOnErr(err error) {
60   if err != nil {
61     panic(err)
62   }
63 }

As you can see and have probably expected, data processing in Go is far less elegant than in R. The encoding/csv package understands the CSV format, but Go's reader type needs to laboriously work its way through the lines of the file, checking for the end of file (line 29) and handling any read errors. Because the first line in the CSV format lists the column names, the logic starting in line 36 works its way past this with the firstLine Boolean variable.

Lines 41 to 48 then extract the numeric column values using ParseFloat() and ParseInt() along with the respective precision (32- or 64-bit) and a base of 10 for integers, followed by line 49 to set the corresponding attributes in the Tour type structure. Line 55 appends a single instance of this structure to the array slice with all the line data from the CSV file, and the action continues with the next round.

Choosy

The main program in Listing 5 understands a number of filter flags: --gain is a qualifying tour's maximum elevation gain in meters and --radius is the maximum distance from my home base, the coordinates of which are defined by home in line 9. Adjust this to your private settings for the best results. The command-line parameter --mins defines the maximum tour duration in minutes. The flags take either floating-point or integer values from the user, which hikefind converts to its internal types. hikefind then uses the values to whittle down qualifying tours from the CSV metafile.

Listing 5

hikefind.go

01 package main
02 import (
03   "flag"
04   "fmt"
05   "github.com/fatih/color"
06   geo "github.com/kellydunn/golang-geo"
07 )
08 func main() {
09   home := geo.NewPoint(37.751051, -122.427288)
10   gain := flag.Int("gain", 0, "elevation gain")
11   radius := flag.Float64("radius", 0, "radius from home")
12   mins := flag.Int("mins", 0, "hiking time in minutes")
13   flag.Parse()
14   flag.Usage = func() {
15     fmt.Print(`hikefind [--gain=max-gain] [--radius=max-dist] [--mins=max-mins]`)
16   }
17   tours, err := readCSV()
18   if err != nil {
19     panic(err)
20   }
21   for _, tour := range tours {
22     if *gain != 0 && tour.gain > *gain {
23       continue
24     }
25     start := geo.NewPoint(tour.lat, tour.lng)
26     dist := home.GreatCircleDistance(start)
27     if *radius != 0 && dist > *radius {
28       continue
29     }
30     if *mins != 0 && tour.mins > *mins {
31       continue
32     }
33     fmt.Printf("%s: [%s:%s:%s]\n",
34       tour.name,
35       color.RedString(fmt.Sprintf("%dm", tour.gain)),
36       color.GreenString(fmt.Sprintf("%.1fkm", dist)),
37       color.BlueString(fmt.Sprintf("%dmins", tour.mins)))
38   }
39 }

The for loop starting at line 21 iterates over all metadata read using readCSV() in line 17 and applies the three implemented filters: gain, radius, and mins. The distance from home is checked by the radius filter using the GitHub kellydunn/golang-geo package. This package uses the GreatCircleDistance() function to determine the distance between the two geo-points in kilometers and then compares the numerical result with the defined filter value.

If one of the three filters is tripped, the for loop continues with the next round without producing any output. But if an entry passes through all the filters unscathed, the print statement from line 33 outputs the tour.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Plan Your Hike

    The hiking and cycling app komoot saves your traveled excursion routes. Mike Schilli shows you how to retrieve the data with Go.

  • GPS Tools

    Almost all manufacturers of GPS devices use proprietary formats to save routes, tracks, and waypoints. Vendors unfortunately rarely offer Linux software for uploading and downloading or processing the data. Four GPS editors keep Linux users on the right track.

  • Tutorials – Apache Spark

    Churn through lots of data with cluster computing on Apache's Spark platform.

  • Knight's Tour

    If you're looking for a head start on solving the classic Knight's Tour chess challenge, try this homegrown Python script.

  • Wanderlust

    For running statistics on his recorded hiking trails, Mike Schilli turns to Go to extract the GPS data while relying on plotters and APIs for a bit of geoanalysis.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News