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 continue
s 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.
« Previous 1 2 3 Next »
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
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.
News
-
Red Hat Adds New Deployment Option for Enterprise Linux Platforms
Red Hat has re-imagined enterprise Linux for an AI future with Image Mode.
-
OSJH and LPI Release 2024 Open Source Pros Job Survey Results
See what open source professionals look for in a new role.
-
Proton 9.0-1 Released to Improve Gaming with Steam
The latest release of Proton 9 adds several improvements and fixes an issue that has been problematic for Linux users.
-
So Long Neofetch and Thanks for the Info
Today is a day that every Linux user who enjoys bragging about their system(s) will mourn, as Neofetch has come to an end.
-
Ubuntu 24.04 Comes with a “Flaw"
If you're thinking you might want to upgrade from your current Ubuntu release to the latest, there's something you might want to consider before doing so.
-
Canonical Releases Ubuntu 24.04
After a brief pause because of the XZ vulnerability, Ubuntu 24.04 is now available for install.
-
Linux Servers Targeted by Akira Ransomware
A group of bad actors who have already extorted $42 million have their sights set on the Linux platform.
-
TUXEDO Computers Unveils Linux Laptop Featuring AMD Ryzen CPU
This latest release is the first laptop to include the new CPU from Ryzen and Linux preinstalled.
-
XZ Gets the All-Clear
The back door xz vulnerability has been officially reverted for Fedora 40 and versions 38 and 39 were never affected.
-
Canonical Collaborates with Qualcomm on New Venture
This new joint effort is geared toward bringing Ubuntu and Ubuntu Core to Qualcomm-powered devices.