Map projection on a two-dimensional terminal with Go
Enhance!
If you want more detail at a higher resolution, you either need to scale down the font used by the terminal and open the window wide enough or switch the terminal to graphics mode. The latter is done by the termui package, which I have used in this column many times.
Listing 4 reads the GPX file just like in Listing 2 and then uses ui.Init()
to set up the terminal UI. The display consists of two widgets, a large Canvas
object at the top and a single-line Paragraph
widget with a border at the bottom that outputs the name of the GPX file plotted at the top of the Canvas
widget for informational purposes.
Listing 4
gpx-tui.go
01 package main 02 import ( 03 "fmt" 04 ui "github.com/gizak/termui/v3" 05 "github.com/gizak/termui/v3/widgets" 06 "log" 07 ) 08 09 func main() { 10 prog, file := cmdLineParse() 11 geo, err := gpxPoints(file) 12 if err != nil { 13 log.Fatalf("Parse error: %v\n", err) 14 } 15 if err := ui.Init(); err != nil { 16 panic(err) 17 } 18 defer ui.Close() 19 20 w, h := ui.TerminalDimensions() 21 txt := widgets.NewParagraph() 22 txt.Text = fmt.Sprintf("%s rendered %s", prog, file) 23 txt.TextStyle.Fg = ui.ColorWhite 24 txt.SetRect(0, h-3, w, h) 25 c := ui.NewCanvas() 26 c.SetRect(0, 0, w, h-3) 27 28 xy := projectSimple(geo, 2*(w-1), 4*(h-4)) 29 for _, pt := range xy { 30 c.SetPoint(pt, ui.ColorWhite) 31 } 32 33 ui.Render(txt, c) 34 35 for e := range ui.PollEvents() { 36 if e.Type == ui.KeyboardEvent { 37 break 38 } 39 } 40 }
The widgets rely on termui's built-in SetRect()
function to help them take up their positions in the terminal window. The function expects the spatial boundary of the widgets as coordinates with row and column values; termui counts the columns from left to right, and the rows from top to bottom.
The call to projectSimple()
in line 28 of Listing 4 returns the projected track points, which are already in the correct coordinate system. This means that, for each track point, line 30 only needs to call the c.SetPoint()
method of the termui Canvas
object to draw a graphical dot at the correct location on the Canvas
widget.
The Render()
function in line 33 throws both widgets up on the screen, and the for
loop starting in line 35 uses ui.PollEvents()
to check for things like keyboard events until line 37 stops the carousel if the user presses any key to halt.
To compile, again you need the three commands mentioned in Listing 3; only this time the build command is
go build gpx-tui.go gpx.go
The resulting gpx-tui
binary expects a GPX file, as you can see in Figure 6, and displays the trail's track points at a relatively high resolution in the termui Canvas
widget. Pressing any key closes the terminal UI and the shell returns to its prompt.
Eye Candy
All told, though, the terminal display still leaves something to be desired. Reference points such as roads are missing, as are the natural boundaries usually offered by maps, such as coastlines, rivers, and mountains. Unfortunately, map data from providers such as Google Maps is not available for free (feel free to boo at this point).
However, there is always OpenStreetMap (yay!). This community project's maps are licensed under the Open Data Commons Open Database License [3], and there is even a tile server from which arbitrary applications can download the maps as tiles free of charge, even without registering. A tile displays a small section of the world map in appropriate detail, depending on the zoom setting.
The process of finding the tile for a given latitude and longitude on Earth at a given zoom setting is well documented [4]. Applications need to put the three values into a geometric formula and then download the tile data as a PNG file from https://tile.openstreetmap.org/Z/X/Y.png
(where Z, X, and Y are the zoom setting, and two calculated values based on the latitute and longitude of the location). However, there's an even easier way. The go-staticmaps Golang library – available on GitHub – elegantly abstracts both the task of fetching tiles for specific latitudes and longitudes and of inserting markers into the displayed maps.
To turn this into production, compile the source code in Listing 5 with
go build gpx-osm.go gpx.go
Listing 5
gpx-osm.go
01 package main 02 import ( 03 sm "github.com/flopp/go-staticmaps" 04 "github.com/fogleman/gg" 05 "github.com/golang/geo/s2" 06 "image/color" 07 "log" 08 "os/exec" 09 ) 10 11 func main() { 12 _, file := cmdLineParse() 13 14 geo, err := gpxPoints(file) 15 if err != nil { 16 log.Fatalf("Parse error: %v\n", err) 17 } 18 edges := []s2.LatLng{} 19 for _, gpxp := range geo { 20 edges = append(edges, s2.LatLngFromDegrees(gpxp.Latitude, gpxp.Longitude)) 21 } 22 23 ctx := sm.NewContext() 24 ctx.SetSize(800, 600) 25 ctx.AddObject(sm.NewPath(edges, color.RGBA{0, 0, 0xff, 0xff}, 10.0)) 26 img, err := ctx.Render() 27 if err != nil { 28 panic(err) 29 } 30 31 const png = "/tmp/osm.png" 32 if err := gg.SavePNG(png, img); err != nil { 33 panic(err) 34 } 35 cmd := exec.Command("eog", png) 36 if err := cmd.Run(); err != nil { 37 log.Fatal("Error: ", err) 38 } 39 }
which creates a Go binary named gpx osm.. When called with a GPX file as an argument, the trail comes up drawn neatly in blue on a map (Figure 7).
How does this work? Line 23 in Listing 5 uses NewContext()
to create a new map object. The previously called for loop iterated through all the track points in the GPX file, appending each one as a LatLng
object to the array slice named edges
. The call to NewPath()
in line 25 turns this into the segments of a path object that AddObject()
inserts into the virtual map. The path's color is set to deep blue with an RGB value 0x0000ff
, and the alpha channel defines full opacity with a value of 0xff
. The weight
of the path is specified by the value of 10.0
, which is quite thick.
The Render()
function on the Context
object molds the whole thing to the right shape in line 26, while SavePNG()
in line 32 writes the PNG data to a file in the /tmp
directory. To immediately show the new artwork on screen, line 35 calls the Eye of Gnome utility with the path of the temporary map file as a parameter – and up comes the map with the trail drawn on it. Amazing!
All done: Without further ado, you now have a handy tool that graphically displays selected trail files, which means you immediately know where you are going when you make a selection. The whole thing now cries out to be integrated into an application, perhaps with a Fyne GUI. It goes without saying that there are virtually no limits to what creative programmers can do here!
Infos
- "Programming Snapshot – Go Filters" by Mike Schilli, Linux Magazine, issue 269, April 2023, pp. 48-52
- Mercator projection: https://en.wikipedia.org/wiki/Mercator_projection
- OpenStreetMap data license: https://opendatacommons.org/licenses/odbl/
- "Slippy map tilenames": https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon./lat._to_tile_numbers
« Previous 1 2
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
-
Gnome 47.1 Released with a Few Fixes
The latest release of the Gnome desktop is all about fixing a few nagging issues and not about bringing new features into the mix.
-
System76 Unveils an Ampere-Powered Thelio Desktop
If you're looking for a new desktop system for developing autonomous driving and software-defined vehicle solutions. System76 has you covered.
-
VirtualBox 7.1.4 Includes Initial Support for Linux kernel 6.12
The latest version of VirtualBox has arrived and it not only adds initial support for kernel 6.12 but another feature that will make using the virtual machine tool much easier.
-
New Slimbook EVO with Raw AMD Ryzen Power
If you're looking for serious power in a 14" ultrabook that is powered by Linux, Slimbook has just the thing for you.
-
The Gnome Foundation Struggling to Stay Afloat
The foundation behind the Gnome desktop environment is having to go through some serious belt-tightening due to continued financial problems.
-
Thousands of Linux Servers Infected with Stealth Malware Since 2021
Perfctl is capable of remaining undetected, which makes it dangerous and hard to mitigate.
-
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.