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
-
Halcyon Creates Anti-Ransomware Protection for Linux
As more Linux systems are targeted by ransomware, Halcyon is stepping up its protection.
-
Valve and Arch Linux Announce Collaboration
Valve and Arch have come together for two projects that will have a serious impact on the Linux distribution.
-
Hacker Successfully Runs Linux on a CPU from the Early ‘70s
From the office of "Look what I can do," Dmitry Grinberg was able to get Linux running on a processor that was created in 1971.
-
OSI and LPI Form Strategic Alliance
With a goal of strengthening Linux and open source communities, this new alliance aims to nurture the growth of more highly skilled professionals.
-
Fedora 41 Beta Available with Some Interesting Additions
If you're a Fedora fan, you'll be excited to hear the beta version of the latest release is now available for testing and includes plenty of updates.
-
AlmaLinux Unveils New Hardware Certification Process
The AlmaLinux Hardware Certification Program run by the Certification Special Interest Group (SIG) aims to ensure seamless compatibility between AlmaLinux and a wide range of hardware configurations.
-
Wind River Introduces eLxr Pro Linux Solution
eLxr Pro offers an end-to-end Linux solution backed by expert commercial support.
-
Juno Tab 3 Launches with Ubuntu 24.04
Anyone looking for a full-blown Linux tablet need look no further. Juno has released the Tab 3.
-
New KDE Slimbook Plasma Available for Preorder
Powered by an AMD Ryzen CPU, the latest KDE Slimbook laptop is powerful enough for local AI tasks.
-
Rhino Linux Announces Latest "Quick Update"
If you prefer your Linux distribution to be of the rolling type, Rhino Linux delivers a beautiful and reliable experience.