If you haven’t yet, make sure to install Go. You can follow the official instructions here, but on ArchLinux you can install with
$ sudo pacman -S go
And then add Go to your PATH
with
export PATH=$PATH:/usr/local/go/bin
In either your .zshrc or .bashrc, whichever you use. Confirm your Go installation with
$ go version
To set up the project itself, let’s create a folder, initialise a module, and create our source files.
$ mkdir naurt_example
$ go mod init naurt_example
$ mkdir src
$ touch src/main.go
We can place the following boilerplate in main.go
1package main
2
3import (
4 "fmt"
5 "net/http"
6)
7
8func main() {
9
10 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
11 fmt.Fprintf(w, "Hello world!")
12 })
13
14 fmt.Println("Starting the server!")
15 if err := http.ListenAndServe(":8080", nil); err != nil {
16 fmt.Println((err))
17 }
18}
19
And then run with
$ go build -C src -o naurt
$ ./src/naurt
If you use Windows, you will need to use the following
$ go build -C src -o naurt.exe
$ src/naurt.exe
You should see the web server starting, and if you go to localhost:8080/ in a web browser see a hello world message
Using Naurt services requires an API Key. If you don’t already have one, you can sign up to the dashboard for free. We don’t require a credit card. You’ll get a free key loaded with thousands of requests. I’ll place my api key in a file called api.key
next to the go.mod
file.
In Go, we’ll read the key in as a constant from a file. You’ll need to import os
, io
and sync
. Start by creating the apiKey
object
1var (
2 apiKey string
3 once sync.Once
4)
5
6func initialiseApiKey() {
7 // Use sync.Once to ensure this is executed only once
8 once.Do(func() {
9 // Read file content from a file
10 file, err := os.Open("api.key")
11 if err != nil {
12 panic("`api.key` not found")
13 }
14
15 defer file.Close()
16
17 content, err := io.ReadAll(file)
18 if err != nil {
19 panic("Could not read API key to string")
20 }
21
22 apiKey = string(content)
23 })
24}
And then we need to call this in the main function like so
1func main() {
2
3 initialiseApiKey()
4
5 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
6 fmt.Fprintf(w, "Hello world!")
7 })
8
9 fmt.Println("Starting the server!")
10 if err := http.ListenAndServe(":8080", nil); err != nil {
11 fmt.Println((err))
12 }
13}
apiKey
is now globally available across threads to be used.
Create a new file called types.go
in the src
folder - we’ll put all of the structs
which handle the Naurt request in here.
$ touch src/types.go
We’ll start by typing the Naurt request, so the file only needs the following
1package main
2
3type NaurtRequest struct {
4 AddressString string `json:"address_string,omitempty"`
5 Latitude *float64 `json:"latitude,omitempty"`
6 Longitude *float64 `json:"longitude,omitempty"`
7 AdditionalMatches bool `json:"additional_matches,omitempty"`
8}
Now that we have this type, we can create a simple function which will make this request for us. For now, our goal is to simply make the request and print out the JSON body we get back. We’ll define a function makeNaurtRequest
which will handle actually making a request to Naurt. For now, it’ll be simple and we’ll just hard code the request in, but we can come back and customise that later on.
1func makeNaurtRequest() (string, error) {
2 url := "https://api.naurt.net/final-destination/v1"
3
4 data := NaurtRequest{
5 AddressString: "The Grand Hotel, Brighton",
6 }
7
8 jsonData, err := json.Marshal(data)
9 if err != nil {
10 return "", err
11 }
12
13 req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
14 if err != nil {
15 return "", err
16 }
17
18 req.Header.Set("Content-Type", "application/json")
19 req.Header.Set("Authorization", apiKey)
20
21 client := &http.Client{}
22 resp, err := client.Do(req)
23 if err != nil {
24 return "", err
25 }
26
27 defer resp.Body.Close()
28
29 body, err := io.ReadAll(resp.Body)
30 if err != nil {
31 return "", err
32 }
33
34 return string(body), err
35}
It will also be easier if we write a dedicated handler function
1func handler(w http.ResponseWriter, r *http.Request) {
2 resp, err := makeNaurtRequest()
3 if err != nil {
4 fmt.Fprintf(w, err.Error())
5 return
6 }
7
8 fmt.Fprintf(w, resp)
9
10}
And then finally we can update the main
function to use this new handler
1func main() {
2
3 initialiseApiKey()
4
5 http.HandleFunc("/", handler)
6
7 fmt.Println("Starting the server!")
8 if err := http.ListenAndServe(":8080", nil); err != nil {
9 fmt.Println((err))
10 }
11}
Go ahead and compile and run the code again - you should see a JSON print out when you go to localhost:8080/
If we want to take this further, we’ll need to convert the JSON response we get from Naurt into data that we can work with more effectively in Go. To do this, we’ll use structs
like the NaurtRequest
to deserialise the response into. I’ll put the following types into types.go
1
2type NaurtResponse struct {
3 BestMatch *DestinationResponse `json:"best_match,omitempty"`
4 AdditionalMatches *[]DestinationResponse `json:"additional_matches,omitempty"`
5 Version string `json:"version,omitempty"`
6}
7
8type DestinationResponse struct {
9 ID string `json:"id"`
10 Address string `json:"address"`
11 Geojson NaurtGeojson `json:"geojson"`
12 Distance float32 `json:"distance,omitempty"`
13}
14
15type NaurtGeojson struct {
16 Features []Feature `json:"features"`
17 TypeVal string `json:"type"`
18}
19
20type Feature struct {
21 Geometry Coordinates `json:"geometry"`
22 TypeVal string `json:"type"`
23 Properties Properties `json:"properties"`
24}
25
26type Coordinates struct {
27 Coordinates CoordinatesWrapper `json:"coordinates"`
28 TypeVal string `json:"type"`
29}
30
31type CoordinatesWrapper struct {
32 Number [][]float32
33 NestedNumber [][][]float32
34}
35
36type Properties struct {
37 NaurtType string `json:"naurt_type"`
38 Contributors []string `json:"contributors"`
39}
This is a fairly straightforward JSON deserialisation task, however there’s a few points worth looking at in more detail. I called all the type
fields from the JSON TypeVal
since type
is already a keyword in Go. Thankfully, Go makes it really easy to specify the name in the JSON.
The other point is CoordinatesWrapper
- notice that it does not have a json
marking. This is because Naurt can respond with two kinds of coordinates
naurt_doors
will be of this type.naurt_parking
and naurt_building
are of this typeHowever, both will not be present at once. Therefore, we need to write a small piece of custom deserlisation code for this
1func (f *CoordinatesWrapper) UnmarshalJSON(data []byte) error {
2
3 var doubleArray [][]float32
4 if err := json.Unmarshal(data, &doubleArray); err == nil {
5 f.Number = doubleArray
6 return nil
7 }
8
9 var tripleArray [][][]float32
10 if err := json.Unmarshal(data, &tripleArray); err == nil {
11 f.NestedNumber = tripleArray
12 return nil
13 }
14
15 return errors.New("`CoordinatesWrapper` did not find valid format")
16}
Note that you will need to import encoding/json
and errors
.
The point of this custom deserialise code is the struct will either have a double nested array or a triple nested array, while the other is nil
and we can check for this at run time.
Now, we’ll edit the makeNaurtRequest
function to return a NaurtRequest
rather than a string
1func makeNaurtRequest() (NaurtResponse, error) {
2 url := "https://api.naurt.net/final-destination/v1"
3
4 data := NaurtRequest{
5 AddressString: "The Grand Hotel, Brighton",
6 }
7
8 jsonData, err := json.Marshal(data)
9 if err != nil {
10 return NaurtResponse{}, err
11 }
12
13 req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
14 if err != nil {
15 return NaurtResponse{}, err
16 }
17
18 req.Header.Set("Content-Type", "application/json")
19 req.Header.Set("Authorization", apiKey)
20
21 client := &http.Client{}
22 resp, err := client.Do(req)
23 if err != nil {
24 return NaurtResponse{}, err
25 }
26
27 defer resp.Body.Close()
28
29 body, err := io.ReadAll(resp.Body)
30 if err != nil {
31 return NaurtResponse{}, err
32 }
33
34 var naurt NaurtResponse
35 if err := json.Unmarshal([]byte(body), &naurt); err != nil {
36 return NaurtResponse{}, err
37 }
38
39 return naurt, err
40}
We’ll also temporarily update the handler function to deal with this new return type. For now, we’ll just convert it back into a JSON and print it out. Although it seems like a lot of work to do nothing, we will very soon do something much more useful with the response
1func handler(w http.ResponseWriter, r *http.Request) {
2 resp, err := makeNaurtRequest()
3 if err != nil {
4 fmt.Fprintf(w, err.Error())
5 return
6 }
7
8 jsonBody, err := json.Marshal(resp)
9 if err != nil {
10 fmt.Fprintf(w, err.Error())
11 return
12 }
13
14 fmt.Fprintf(w, string(jsonBody))
15
16}
You should see the JSON again, but this time it has first been processed as a Go object we can work with.
At the moment, we’ve hardcoded the request into the program. This is obviously much less useful than having the request be dynamic. The design we’ll use is parsing arguments out of the URL. Editing the makeNaurtRequest
function is really straightforward
1func makeNaurtRequest(address string, latitude *float64, longitude *float64) (NaurtResponse, error) {
2 url := "https://api.naurt.net/final-destination/v1"
3
4 data := NaurtRequest{
5 AddressString: address,
6 Latitude: latitude,
7 Longitude: longitude,
8 AdditionalMatches: true,
9 }
10 ...
Make sure you import strcnov
, then we can edit the handler
to look like so
1func handler(w http.ResponseWriter, r *http.Request) {
2
3 query := r.URL.Query()
4
5 address := query.Get("address")
6
7 var latitude *float64
8 if query.Has("latitude") {
9 tmp, err := strconv.ParseFloat(query.Get("latitude"), 64)
10 if err != nil {
11 fmt.Fprintf(w, err.Error())
12 return
13 }
14 latitude = &tmp
15 }
16
17 var longitude *float64
18 if query.Has("longitude") {
19 tmp, err := strconv.ParseFloat(query.Get("longitude"), 64)
20 if err != nil {
21 fmt.Fprintf(w, err.Error())
22 return
23 }
24 longitude = &tmp
25 }
26
27 resp, err := makeNaurtRequest(address, latitude, longitude)
28 if err != nil {
29 fmt.Fprintf(w, err.Error())
30 return
31 }
32
33 jsonBody, err := json.Marshal(resp)
34 if err != nil {
35 fmt.Fprintf(w, err.Error())
36 return
37 }
38
39 fmt.Fprintf(w, string(jsonBody))
40
41}
42
Since address
is meant to be a string
we can just get it from the query - if it isn’t present we will simply get the empty string, which the JSON serialisation will ignore. However, we can now see why the latitude and longitude have been *float64
so far. If these arguments are not provided, we need them to be an empty type so they won’t serialise. If we can’t parse the arguments given in latitude or longitude as floats, we return an error, though.
Now if you compile and run the code, and use a search query, you’ll get different results! For example, try http://localhost:8080/?address=The Grand Hotel, Eastbourne
At this point, if we were using Naurt purely in a backend application we’d be more or less done - this could be used in a route planning or ETA system as is. However, we’ll plot the output on a map just so we can visually see the results.
To do the plotting, we’ll make a new file called plotting.go
$ touch src/plotting.go
For this application we’ll be using Plotly, and there’s actually already a Plotly package in Go. We’ll need to install it though, which we can do with
$ go get github.com/MetalBlueberry/go-plotly
Inside plotting.go
make sure to add package main
to the top.
We’ll start by making some helper functions. Plotly typically expects all the latitudes to be grouped into one slice and all the longitudes grouped into another slice. Naurt follows the geojson standard, so the latitudes and longitudes are grouped into a single slice representing a point. We can create the following very simple functions to convert between the two
1func extractLats(points [][]float32) []float32 {
2 lats := []float32{}
3
4 for _, point := range points {
5 lats = append(lats, point[1])
6 }
7
8 return lats
9}
10
11func extractLons(points [][]float32) []float32 {
12 lons := []float32{}
13
14 for _, point := range points {
15 lons = append(lons, point[0])
16 }
17
18 return lons
19}
All of the useful data to do with plotting is found inside the DestinationResponse
struct, so let’s write a function which can convert the DestinationResponse
into something Plotly can work with.
1func extractNaurtInner(data *DestinationResponse, traces *[]grob.Trace, layout **grob.Layout, bestMatch bool) {
2 for _, feat := range data.Geojson.Features {
3 if feat.Geometry.Coordinates.Number != nil {
4 // naurt_door
5
6 if bestMatch {
7 *layout = &grob.Layout{
8 Mapbox: &grob.LayoutMapbox{
9 Style: "carto-positron",
10 Center: &grob.LayoutMapboxCenter{
11 Lat: float64(feat.Geometry.Coordinates.Number[0][1]),
12 Lon: float64(feat.Geometry.Coordinates.Number[0][0]),
13 },
14 Zoom: 15.0,
15 },
16 Showlegend: grob.False,
17 Autosize: grob.True,
18 }
19 }
20
21 trace := &grob.Scattermapbox{
22 Type: "scattermapbox",
23 Lat: extractLats(feat.Geometry.Coordinates.Number),
24 Lon: extractLons(feat.Geometry.Coordinates.Number),
25 Text: fmt.Sprintf("%s - %s", data.Address, feat.Properties.NaurtType),
26 Marker: &grob.ScattermapboxMarker{Size: 9.0},
27 }
28
29 *traces = append(*traces, trace)
30
31 } else if feat.Geometry.Coordinates.NestedNumber != nil {
32 // naurt_parking or naurt_building
33
34 for _, shape := range feat.Geometry.Coordinates.NestedNumber {
35
36 trace := &grob.Scattermapbox{
37 Type: "scattermapbox",
38 Lat: extractLats(shape),
39 Lon: extractLons(shape),
40 Text: fmt.Sprintf("%s - %s", data.Address, feat.Properties.NaurtType),
41 Mode: grob.ScattermapboxModeLines,
42 Fill: grob.ScattermapboxFillToself,
43 }
44
45 *traces = append(*traces, trace)
46 }
47 }
48 }
49}
50
Make sure you import grob "[github.com/MetalBlueberry/go-plotly/graph_objects](<http://github.com/MetalBlueberry/go-plotly/graph_objects>)"
as well as "fmt"
.
Plotly expects a slice of traces, so in the extractNaurtInner
function, we pass in a reference to a slice of these traces that we’ll make outside the function and then continually append to. We also need a layout for a Plotly map. The layout contains the map centre. We’ll centre on the door of the best match from Naurt, which is why we need to tell this function whether it’s a best match or not. Again, we’ll pass in a pointer to this layout object and set it inside the function.
Speaking of a caller function, we can create the plotNaurt
function now
1func plotNaurt(response NaurtResponse) (string, error) {
2
3 traces := []grob.Trace{}
4
5 var layout *grob.Layout
6
7 if response.BestMatch != nil {
8 extractNaurtInner(response.BestMatch, &traces, &layout, true)
9 }
10
11 if response.AdditionalMatches != nil {
12 for _, data := range *response.AdditionalMatches {
13 extractNaurtInner(&data, &traces, &layout, false)
14 }
15
16 }
17
18 if layout == nil {
19 return "", errors.New("no best match found")
20 }
21
22 fig := &grob.Fig{
23 Data: traces,
24 Layout: layout,
25 }
26
27 jsonFig, err := json.Marshal(fig)
28 if err != nil {
29 return "", err
30 }
31
32 return string(jsonFig), nil
33
34}
Make sure you import "encoding/json"
and "errors"
here.
Essentially, we use the Plotly package to convert Naurt into a map. We’ll actually return a JSON as a string from this function, since in order to serve this to the user we’ll place it inside a template.
We’re going to use templates the serve the response to the user. Let’s make a folder and template file for this purpose
$ mkdir templates
$ touch templates/index.html
Inside index.html
place the following template code
1<!DOCTYPE >
2<html>
3<head>
4 <meta charset="UTF-8">
5 <title>Plotly Map</title>
6 <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
7</head>
8<style>
9.container {
10 width: 100%;
11 height: 100vh;
12}
13</style>
14<body>
15 <div class="container" id="mapDiv"></div>
16 <script>
17 var data = JSON.parse({{.Data}});
18
19 console.log(data)
20
21 Plotly.newPlot("mapDiv", data.data, data.layout);
22 </script>
23</body>
24</html>
The basic idea is we’re going to template in a JSON, which will be parsed into an object and then contain the necessary data for creating the Plotly plot. Unfortunately, the Plotly package in Go doesn’t have some kind of function which converts the plot to HTML in memory (it does have one that converts it to a file, and while I didn’t benchmark I assume this is faster than then reading that file off the server disc and sending it).
Back in main.go
we can add this near the top of the file, outside any function. Make sure to import html/template
(not text/template
which is very similar but won’t work!)
var tmpl *template.Template
tmpl
will store the template which we can use over and over again. We’ll need to initialise it somewhere and the main
function is the most natural
1func main() {
2
3 initialiseApiKey()
4
5 tmpl = template.Must(template.ParseFiles("templates/index.html"))
6
7 http.HandleFunc("/", handler)
8
9 fmt.Println("Starting the server!")
10 if err := http.ListenAndServe(":8080", nil); err != nil {
11 fmt.Println(err)
12 }
13}
We’ll create a small helper type which will pass the data to the template
1type PageData struct {
2 Data string
3}
Now all we have to do is update the handler
function to actually use the template
1func handler(w http.ResponseWriter, r *http.Request) {
2
3 query := r.URL.Query()
4
5 address := query.Get("address")
6
7 var latitude *float64
8 if query.Has("latitude") {
9 tmp, err := strconv.ParseFloat(query.Get("latitude"), 64)
10 if err != nil {
11 fmt.Fprintf(w, err.Error())
12 return
13 }
14 latitude = &tmp
15 }
16
17 var longitude *float64
18 if query.Has("longitude") {
19 tmp, err := strconv.ParseFloat(query.Get("longitude"), 64)
20 if err != nil {
21 fmt.Fprintf(w, err.Error())
22 return
23 }
24 longitude = &tmp
25 }
26
27 resp, err := makeNaurtRequest(address, latitude, longitude)
28 if err != nil {
29 fmt.Fprintf(w, err.Error())
30 return
31 }
32
33 mapJson, err := plotNaurt(resp)
34 if err != nil {
35 fmt.Fprintf(w, err.Error())
36 return
37 }
38
39 data := PageData{Data: mapJson}
40
41 e := tmpl.Execute(w, data)
42 if e != nil {
43 fmt.Fprintf(w, e.Error())
44 }
45}
And we’re done! If we compile and run this code again, then we’ll get a map when we go to for example http://localhost:8080/?address=The Grand Hotel, Brighton