Friday, October 10, 2025
HomeGolangVisualizing Map Knowledge with Go and Leaflet JS

Visualizing Map Knowledge with Go and Leaflet JS


Introduction

This 12 months I set a private aim of strolling for a complete of 1,000 kilometers and I’m proud to say I’m near hitting that aim. I’ve been monitoring all of the completely different routes I soak up an app named Strava. One good function of Strava is that the app offers entry to the uncooked information in a format referred to as GPX. Starva can visualize the route of their app, however I needed to try to carry out my very own visualization. That introduced me to this weblog put up about utilizing Leaflet JS to plot certainly one of my walks on a map.

On this put up, we’re going to write down a Go service that accepts a GPX file and returns an interactive map {that a} browser can render exhibiting the factors recorded within the GPX file.

The applying will seem like:

Uncooked Knowledge

Let’s begin by reviewing the information that’s recorded within the GPX file.

Itemizing 1: Morning_Walk.gpx

<?xml model="1.0" encoding="UTF-8"?>
<gpx creator="StravaGPX Android" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" model="1.1" xmlns="http://www.topografix.com/GPX/1/1">
    <metadata>
        <time>2023-01-03T06:50:42Z</time>
    </metadata>
    <t1rk>
        <identify>Morning Stroll</identify>
        <sort>strolling</sort>
        <trkseg>
            <trkpt lat="32.5254730" lon="34.9429370">
                <ele>19.5</ele>
                <time>2023-01-03T06:50:42Z</time>
            </trkpt>
            <trkpt lat="32.5254790" lon="34.9429480">
                 <ele>19.5</ele>
                 <time>2023-01-03T06:50:43Z</time>
            </trkpt>
            ...
        </trkseg>
    </trk>
</gpx>

Itemizing 1 reveals a truncated model of a GPX file for certainly one of my morning walks. You’ll be able to see its XML formatted information and the situation information is within the `trkpt/trkseq“ factor.

Let’s run a fast question utilizing grep to see what number of trkpt tags there are within the information file:

Itemizing 2: Variety of Factors

$ grep '<trkpt' Morning_Walk.gpx | wc -l
2600

In itemizing 2, you possibly can see there’s 2,600 location factors which is lots to show. Since many of those factors are mainly on high of one another, plotting all of them received’t give us a lot worth. One factor we will do to scale back the variety of factors we show by aggregating the factors by a time frequency. On this case, I’ll combination the factors by a minute.

Notice: Parsing XML with grep is just not the most suitable choice although for my use case it labored tremendous. There are higher instruments corresponding to XMLStarlet

Parsing GPX

To start out, we will parse the GPX information file utilizing the built-in encoing/xml package deal from the Go commonplace library.

Itemizing 3: Parsing GPX

09 // Level is some extent in GPX information.
10 sort Level struct {
11     Lat  float64
12     Lng  float64
13     Time time.Time
14 }
15 
16 // GPX is information in a GPX file.
17 sort GPX struct {
18     Title   string
19     Time   time.Time
20     Factors []Level
21 }
22 
23 // ParseGPX parses a GPX file, returns GPX.
24 func ParseGPX(r io.Reader) (GPX, error) {
25     var xmlData struct {
26         Time time.Time `xml:"metadata>time"`
27         Trk  struct {
28             Title   string `xml:"identify"`
29             Factors []struct {
30                 Lat  float64   `xml:"lat,attr"`
31                 Lon  float64   `xml:"lon,attr"`
32                 Time time.Time `xml:"time"`
33             } `xml:"trkseg>trkpt"`
34         } `xml:"trk"`
35     }
36 
37     dec := xml.NewDecoder(r)
38     if err := dec.Decode(&xmlData); err != nil {
39         return GPX{}, err
40     }
41 
42     gpx := GPX{
43         Title:   xmlData.Trk.Title,
44         Time:   xmlData.Time,
45         Factors: make([]Level, len(xmlData.Trk.Factors)),
46     }
47 
48     for i, pt := vary xmlData.Trk.Factors {
49         gpx.Factors[i].Lat = pt.Lat
50         gpx.Factors[i].Lng = pt.Lon
51         gpx.Factors[i].Time = pt.Time
52     }
53 
54     return gpx, nil
55 }

Itemizing 3 reveals the way to parse a GPX information file utilizing the Go commonplace library. On strains 10 and 17, I outline the Level and GPX structs. They’re the categories returned from parsing. As a common rule, don’t export information buildings that can require particular tags to unmarshal inner information (e.g. the one within the XML). Outline clear information buildings for the API and shield the API from uncontrolled change.

For instance, GPX calls longitude lon whereas leaflet makes use of lng.

One different attention-grabbing design alternative is on line 25 contained in the ParseGPX perform. Right here I outline a literal struct with the suitable tags to unmarshal the GPX information. There is no such thing as a have to mannequin the entire construction of the XML, solely the weather we want. Then on line 48, that information is marshaled into the ultimate struct worth for the API.

Knowledge Aggregation

Since there are too many factors to show on the map, we determined to combination the factors by each minute.

That is just like SQL GROUP BY the place you first group rows to buckets relying on a key (the time rounded to a minute in our case) after which run an aggregation on the values in every bucket (imply in our case). The SQL code (for SQLite3) may be one thing like:

SELECT
    strftime('%H:%M', time),
    AVG(lat),
    AVG(lng)
FROM factors
    GROUP BY strftime('%H:%M', time);

We have to do the identical in Go code, so the next code can carry out that aggregation.

Itemizing 4: Aggregation

57 // roundToMinute rounds time to minute granularity.
58 func roundToMinute(t time.Time) time.Time {
59     12 months, month, day := t.12 months(), t.Month(), t.Day()
60     hour, minute := t.Hour(), t.Minute()
61 
62     return time.Date(12 months, month, day, hour, minute, 0, 0, t.Location())
63 }
64 
65 // meanByMinute aggregates factors by the minute.
66 func meanByMinute(factors []Level) []Level {
67     // Combination columns
68     lats := make(map[time.Time][]float64)
69     lngs := make(map[time.Time][]float64)
70 
71     // Group by minute
72     for _, pt := vary factors {
73         key := roundToMinute(pt.Time)
74         lats[key] = append(lats[key], pt.Lat)
75         lngs[key] = append(lngs[key], pt.Lng)
76     }
77 
78     // Common per minute
79     avgs := make([]Level, len(lngs))
80     i := 0
81     for time, lats := vary lats {
82         avgs[i].Time = time
83         avgs[i].Lat = imply(lats)
84         avgs[i].Lng = imply(lngs[time])
85         i++
86     }
87 
88     return avgs
89 }

Itemizing 4 reveals the code wanted to combination the factors by a minute. The perform to name is meanByMinute which makes use of the roundToMinute perform.

Map HTML Template & JavaScript

We’re going to use the html/template package deal to render the map. A lot of the HTML is static, however we’ll generate the title, information, and the factors dynamically.

Itemizing 5: Map HTML Template

01 <!doctype html>
02 <html>
03   <head>
04     <hyperlink rel="stylesheet" href="https://cdn.jsdelivr.web/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" 
05       integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65"
06       crossorigin="nameless">
07     <meta identify="viewport" content material="width=device-width, initial-scale=1">
08     <hyperlink rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
09       integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
10       crossorigin=""/>
11     <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
12      integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
13      crossorigin=""></script>
14   </head>
15   <physique>
16     <div class="container">
17       <div class="row text-center">
18     <h1 class="alert alert-primary" function="alert">GPX File Viewer</h1>
19     <h3 class="alert alert-secondary" function="alert">{{ .Title }} on {{ .Date }}</h1>
20       </div>
21       <div class="row">
22     <div class="col">
23       <div id="map" type="peak: 600px; border: 1px stable black;"></div>
24     </div>
25       </div>
26     </div>
27     <script>
28       let factors = [
29     {{- range $idx, $pt := .Points }}
30     {{ if $idx }},{{ end -}}
31     { "lat": {{ $pt.Lat }}, "lng": {{ $pt.Lng -}}, "time": {{ $pt.Time }} }
32     {{- end }}
33       ];
34       let middle = [{{ .Center.Lat }}, {{ .Center.Lng }}];
35     </script>
36     <script src="/map.js"></script>
37   </physique>
38 </html>

Itemizing 5 reveals the HTML template file. This code is probably not apparent so I’ll break it down.

On strains 04-13 we import bootstrap for the UI and likewise the leafletjs CSS and JS information. Then on line 19, we set the identify and date of the GPX file. On strains 28-33, we generate a JavaScript array with the factors from the enter and line 34 units the middle variable. Lastly on line 36, we import the map.js JavaScript code which can use factors and middle.

Subsequent we want JavaScript that may carry out the precise rendering of the factors.

Itemizing 6: Map JavaScript

02 perform on_loaded() {
03     // Create map & tiles.
04     var map = L.map('map').setView(middle, 15);
05     L.tileLayer(
06         'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
07         {
08             maxZoom: 19,
09             attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
10         }
11     ).addTo(map);
12 
13     // Add factors with tooltip to map.
14     factors.forEach((pt) => {
15         let circle = L.circle(
16             [pt.lat, pt.lng],
17             {
18                 shade: 'crimson',
19                 radius: 20
20             }).addTo(map);
21         circle.bindPopup(pt.time);
22     });
23 }
24 
25 doc.addEventListener('DOMContentLoaded', on_loaded);

Itemizing 6 reveals the JavaScript required to create the map. This code is probably not apparent so I’ll break it down.

On line 04, we create the map with a middle location and set the zoom degree to fifteen. Then on strains 05-11, we load the tiles from OpenStreetMap after which on strains 14-23, we iterate over the factors, including them to the map as a crimson circle and setting the tooltip to be the hour.

Nothing will work and not using a handler perform that may present the HTML, CSS, and JavaScript for rendering within the browser.

Map HTTP Handler

Itemizing 7: Map HTTP Handler

61 // mapHandler will get GPX file by way of HTML type and return map from mapTemplate.
62 func (a *API) mapHandler(w http.ResponseWriter, r *http.Request) {
63     a.log.Data("map referred to as", "distant", r.RemoteAddr)
64     if r.Methodology != http.MethodPost {
65         a.log.Error("unhealthy methodology", "methodology", r.Methodology)
66         http.Error(w, "unhealthy methodology", http.StatusMethodNotAllowed)
67         return
68     }
69 
70     if err := r.ParseMultipartForm(1 << 20); err != nil {
71         a.log.Error("unhealthy type", "error", err)
72         http.Error(w, "unhealthy type", http.StatusBadRequest)
73         return
74     }
75 
76     file, _, err := r.FormFile("gpx")
77     if err != nil {
78         a.log.Error("lacking gpx file", "error", err)
79         http.Error(w, "lacking gpx file", http.StatusBadRequest)
80         return
81     }
82 
83     gpx, err := ParseGPX(file)
84     if err != nil {
85         a.log.Error("unhealthy gpx", "error", err)
86         http.Error(w, "unhealthy gpx", http.StatusBadRequest)
87         return
88     }
89 
90     a.log.Data("gpx parsed", "identify", gpx.Title, "rely", len(gpx.Factors))
91     meanPts := meanByMinute(gpx.Factors)
92     a.log.Data("minute agg", "rely", len(meanPts))
93 
94     // Knowledge for template
95     factors := make([]map[string]any, len(meanPts))
96     for i, pt := vary meanPts {
97         factors[i] = map[string]any{
98             "Lat":  pt.Lat,
99             "Lng":  pt.Lng,
100             "Time": pt.Time.Format("15:04"), // HH:MM
101         }
102     }
103 
104     clat, clng := middle(gpx.Factors)
105     information := map[string]any{
106         "Title":   gpx.Title,
107         "Date":   gpx.Time.Format(time.DateOnly),
108         "Heart": map[string]float64{"Lat": clat, "Lng": clng},
109         "Factors": factors,
110     }
111 
112     w.Header().Set("content-type", "textual content/html")
113     if err := mapTemplate.Execute(w, information); err != nil {
114         a.log.Error("cannot execute template", "error", err)
115     }
116 }

Itemizing 7 reveals the HTTP handler perform. All this code brings collectively the whole lot we did to supply the browser what it wants to visualise the path.

Lastly we have to write some code to begin the service to deal with a HTTP request to render the map.

Beginning The Server

Itemizing 8: HTTP Handler

12 var (
13     //go:embed index.html map.js
14     staticFS embed.FS
15 
16     //go:embed map.html
17     mapHTML     string
18     mapTemplate *template.Template
19 )
20 
21 sort API struct {
22     log *slog.Logger
23 }

104 func principal() {
105     log := slog.New(slog.NewTextHandler(os.Stdout, nil))
106     tmpl, err := template.New("map").Parse(mapHTML)
107     if err != nil {
108         log.Error("cannot parse map HTML", "error", err)
109         os.Exit(1)
110     }
111     mapTemplate = tmpl
112 
113     api := API{
114         log: log,
115     }
116 
117     mux := http.NewServeMux()
118     mux.Deal with("/", http.FileServer(http.FS(staticFS)))
119     mux.HandleFunc("/map", api.mapHandler)
120 
121     addr := ":8080"
122     srv := http.Server{
123         Addr:              addr,
124         Handler:           mux,
125         ReadHeaderTimeout: time.Second,
126     }
127 
128     log.Data("server beginning", "tackle", addr)
129     if err := srv.ListenAndServe(); err != nil {
130         log.Error("cannot serve", "error", err)
131         os.Exit(1)
132     }
133 }

Itemizing 8 reveals the way to begin and run the HTTP service.

Conclusion

Leaflet JS is a good library for map visualization, it makes use of OpenStreetMap for which has many layers of detailed information. I discover it very cool that it solely took about 260 strains of Go and JavaScript code to generate an interactive map from uncooked GPX information.

Leaflet JS has many extra capabilities, try their website for extra demos.

There are two different takeaways from this weblog. The primary is the method: Resolve on the way you need the visualization to look and examine the uncooked information. After getting the top level and place to begin you can begin coding. The second takeaway is to not use the identical information buildings in any respect ranges of your code. You don’t need to expose the information layer (GPX file format) construction to the enterprise logic (aggregation) or to the API (UI layer). In case you do this, adjustments in a single layer won’t be remoted.

The code is out there at https://github.com/353words/leaflet.

How do you visualize map information? I’d love to listen to from you at miki@ardanlabs.com.



RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments