Integrating Naurt’s Geocoder with Java

A step-by-step guide

Indigo Curnick
August 19, 2024
Resources

In this blog post, you will learn about integrating Naurt’s geocoder with Java to enhance last mile delivery. We’ll use Maven as a build manager. The guide includes steps to sign up for a free Naurt key, set up the environment, make geocoding requests, structure API responses, and plot geocoder responses on a map using HttpServer and Leaflet. You’ll also explore advanced geocoding options and how to improve the usability of a demo app.

Setting up the Environment

We’re going to be using Java for this project, as well as Maven as our build manager. On ArchLinux I used the following to install the necessary tools

$ sudo pacman -S jdk-openjdk maven

Java downloads for your system can be found here. Maven installation details can be found here.You can check these installations with

java -version
mvn -version

Java Project Using Maven

In order to create a basic Maven project, run the following in the command line:

mvn archetype:generate -DgroupId=com.example -DartifactId=naurt-java -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false

Add the following dependencies into the <dependencies> section of the pom.xml file

1 		<dependency>
2        <groupId>com.fasterxml.jackson.core</groupId>
3        <artifactId>jackson-databind</artifactId>
4        <version>2.12.3</version>
5    </dependency>
6    <dependency>
7        <groupId>com.fasterxml.jackson.core</groupId>
8        <artifactId>jackson-core</artifactId>
9        <version>2.12.3</version>
10    </dependency>
11    <dependency>
12        <groupId>com.fasterxml.jackson.core</groupId>
13        <artifactId>jackson-annotations</artifactId>
14        <version>2.12.3</version>
15    </dependency>
16    <dependency>
17        <groupId>com.sun.net.httpserver</groupId>
18        <artifactId>http</artifactId>
19        <version>20070405</version>
20    </dependency>
21    <dependency>
22      <groupId>com.github.spullara.mustache.java</groupId>
23      <artifactId>compiler</artifactId>
24      <version>0.9.14</version>
25    </dependency>

At this point our project structure should look like this

.
├── pom.xml
└── src
    └── main
        └── java
            └── com
                └── example
                    └── App.java

Run the following command to build the project:

mvn clean install

Run the following command to start the application:

mvn exec:java -Dexec.mainClass="com.example.App"

And we should see a “hello world” on the command line!

Sign Up for your FREE Naurt Key

If you haven’t already, sign up at Naurt’s dashboard for a free key. The free key comes loaded with thousands of API requests per month, so there’s no cost to try it out. We don’t require a credit card for sign up, either!

For this post, I’ll be placing my API key in a file called api.key. Make sure that file is next to pom.xml. In most backend applications, this will probably be injected via an environment variable or a secret.

A Basic Web Server

We need to make some edits to the App.java file to create the web server

1package com.example;
2
3import java.io.IOException;
4import java.net.InetSocketAddress;
5
6import com.sun.net.httpserver.HttpServer;
7
8public class App {
9    public static void main(String[] args) throws IOException {
10        // Create an HTTP server that listens on port 8080
11        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
12
13        // Create an instance of ApiHandler
14        ApiHandler apiHandler = new ApiHandler();
15
16        // Create a context for the root path
17        server.createContext("/", apiHandler);
18
19        // Set a default executor
20        server.setExecutor(null);
21
22        // Start the server
23        server.start();
24        System.out.println("Server is listening on port 8080");
25    }
26}

Let’s create a file next to App.java called ApiHandler.java , and add the following code

1package com.example;
2
3import java.io.IOException;
4import java.io.OutputStream;
5import java.nio.charset.StandardCharsets;
6
7import com.sun.net.httpserver.HttpExchange;
8import com.sun.net.httpserver.HttpHandler;
9
10public class ApiHandler implements HttpHandler {
11
12    @Override
13    public void handle(HttpExchange exchange) throws IOException {
14        if ("GET".equals(exchange.getRequestMethod())) {
15            this.handleGet(exchange);
16        } else {
17            this.handleBadRequest(exchange, "You must use a GET request");
18        }
19    }
20
21    private void handleGet(HttpExchange exchange) throws IOException {
22        this.handleAcceptedResponse(exchange, "Thanks for calling me!");
23    }
24
25    private void handleAcceptedResponse(HttpExchange exchange, String response) throws IOException {
26        this.handleResponse(exchange, response, 200);
27    }
28
29    private void handleBadRequest(HttpExchange exchange, String response) throws IOException {
30        this.handleResponse(exchange, response, 400);
31    }
32
33    private void handleResponse(HttpExchange exchange, String response, int code) throws IOException {
34        exchange.sendResponseHeaders(code, response.getBytes().length);
35        OutputStream os = exchange.getResponseBody();
36        try {
37            os.write(response.getBytes(StandardCharsets.UTF_8));
38        } catch (Exception e) {
39        }
40        os.close();
41    }
42}

Now, we have a really simple web server which replies to GET requests on / . If you navigate to http://localhost:8080/ you should see the output!

Making a Request to Naurt

Now it’s time to start making a request to Naurt. We’re going to continue to work in ApiHandler.java - we need access to the API key from Naurt.

1import java.nio.file.Files;
2import java.nio.file.Paths;
3
4public class ApiHandler implements HttpHandler {
5
6    private final String apiKey;
7
8    public ApiHandler() {
9        try {
10            this.apiKey = new String(Files.readAllBytes(Paths.get("api.key"))).trim();
11        } catch (IOException e) {
12            throw new RuntimeException("Failed to read API key from file", e);
13        }
14    }
15    
16...
17}

If we add this, then when we first initialize the ApiHandler we will store the API key for use. Let’s actually make a request to Naurt by modifying the handleGet method

1import com.fasterxml.jackson.databind.ObjectMapper;
2
3import java.net.http.HttpClient;
4import java.net.http.HttpRequest;
5import java.net.URI;
6
7public class ApiHandler implements HttpHandler {
8
9    private final ObjectMapper objectMapper = new ObjectMapper();
10    private final String apiUrl = "https://api.naurt.net/final-destination/v1";
11
12		...
13
14    private void handleGet(HttpExchange exchange) throws IOException {
15
16        Map<String, String> requestBody = new HashMap<String, String>();
17        requestBody.put("address_string", "The Grand Hotel, Brighton");
18        String jsonInputString = objectMapper.writeValueAsString(requestBody);
19
20        try {
21            HttpClient client = HttpClient.newHttpClient();
22            HttpRequest request = HttpRequest.newBuilder()
23                    .uri(URI.create(apiUrl))
24                    .header("Content-Type", "application/json")
25                    .header("Authorization", apiKey)
26                    .POST(HttpRequest.BodyPublishers.ofString(jsonInputString))
27                    .build();
28
29            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
30
31            this.handleAcceptedResponse(exchange, response.body());
32        } catch (Exception e) {
33            e.printStackTrace();
34            this.handleError(exchange);
35        }
36    }
37        private void handleError(HttpExchange exchange) throws IOException {
38        this.handleResponse(exchange, "Internal Server Error", 500);
39    }
40    
41    ...
42}

This time when we run the code, we should see the JSON response print out in the web browser.

Handle Search Parameters

Right now, every time we want to send a different request we would need to recompile the program. Obviously, it would be better if we could just use query parameters in the GET request instead. In Java, accessing the query string is really easy, at the top of handleGet we just need to do this

String query = exchange.getRequestURI().getQuery();

However, parsing out the individual pieces is a bit more challenging. In the end, I wrote out a custom parser class just to handle the potential Naurt parameters

1package com.example;
2
3import java.util.HashMap;
4import java.util.Map;
5
6import com.fasterxml.jackson.core.JsonProcessingException;
7import com.fasterxml.jackson.databind.ObjectMapper;
8
9public class QueryParams {
10    private String addressString;
11    private Boolean additionalMatches;
12    private Float latitude;
13    private Float longtiude;
14
15    private final ObjectMapper objectMapper = new ObjectMapper();
16
17    public QueryParams(String query) {
18        if (query == null || query.isEmpty()) {
19            return;
20        }
21
22        Map<String, String> queryParams = new HashMap<>();
23        String[] pairs = query.split("&");
24        for (String pair : pairs) {
25            String[] keyValue = pair.split("=");
26            if (keyValue.length > 1) {
27                queryParams.put(keyValue[0], keyValue[1]);
28            } else {
29                queryParams.put(keyValue[0], null);
30            }
31        }
32
33        this.addressString = queryParams.get("address_string");
34        this.additionalMatches = this.parseBoolean(queryParams.get("additional_matches"));
35        this.latitude = this.parseFloat(queryParams.get("latitude"));
36        this.longtiude = this.parseFloat(queryParams.get("longitude"));
37    }
38
39    public String convertToNaurtRequestJson() throws JsonProcessingException {
40        Map<String, Object> naurtRequest = new HashMap<>();
41
42        if (this.addressString != null) {
43            naurtRequest.put("address_string", this.addressString);
44        }
45
46        if (this.additionalMatches != null) {
47            naurtRequest.put("additional_matches", this.additionalMatches);
48        }
49
50        if (this.latitude != null) {
51            naurtRequest.put("latitude", this.latitude);
52        }
53
54        if (this.longtiude != null) {
55            naurtRequest.put("longitude", this.longtiude);
56        }
57
58        return objectMapper.writeValueAsString(naurtRequest);
59    }
60
61    private Boolean parseBoolean(String value) {
62        try {
63            return value != null ? Boolean.parseBoolean(value) : null;
64        } catch (Exception e) {
65            return null;
66        }
67    }
68
69    private Float parseFloat(String value) {
70        try {
71            return value != null ? Float.parseFloat(value) : null;
72        } catch (NumberFormatException e) {
73            return null;
74        }
75    }
76
77}
78
79

This class has two responsibilities

  1. Extract the query parameters from the query string. This is handled by the initializer.
  2. Convert the query parameters into a JSON which can be sent to the Naurt endpoint. This is handled by convertToNaurtRequestJson

The query string coming into the initializer would look something like address_string=The Grand Hotel&additional_matches=false . We need to split on the & and then split on the = . We also need to convert into the correct type where necessary (address_string is already a String ).

So if we now edit the handleGet method like so

1    private void handleGet(HttpExchange exchange) throws IOException {
2
3        try {
4            String query = exchange.getRequestURI().getQuery();
5
6            QueryParams queryParams = new QueryParams(query);
7            String jsonInputString = queryParams.convertToNaurtRequestJson();
8            HttpClient client = HttpClient.newHttpClient();
9            ...
10            // rest of method, unchanged

And we compile and run now, we can go to http://localhost:8080/?additional_matches=true&address_string=The Grand Hotel, Brighton and hopefully see a JSON output!

Visualising the GeoJSON

While a JSON output is great for a backend application, it doesn’t really mean much to a human. Especially since GeoJSON is a very nested format, it would be nice to visualize the GeoJSON on a map. For this one, we’re going to use Leaflet for the map, and Mustache for the template rendering.

Let’s start by creating a resources folder in the src/main folder, where we will store the template. Then make a index.mustache file inside that folder. This is a template, which will render with the Naurt response to actually visualize the GeoJSONs. Here’s the contents of index.mustache

1<!DOCTYPE html>
2<html>
3
4<head>
5    <title>Naurt Geocoder</title>
6    <meta charset="utf-8" />
7    <meta name="viewport" content="width=device-width, initial-scale=1.0">
8    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
9    <style>
10        #map {
11            height: 100vh;
12        }
13    </style>
14</head>
15
16<body>
17    <div id="map"></div>
18    <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
19    <script>
20
21        function getColor(type) {
22            switch (type) {
23                case 'naurt_door': return '#FF0000';
24                case 'naurt_building': return '#9d79f2';
25                case 'naurt_parking': return '#00FF00';
26                default: return '#000000';
27            }
28        }
29
30        function style(feature) {
31            return {
32                fillColor: getColor(feature.properties.naurt_type), weight: 2, opacity: 1, color: 'white', dashArray: '5', fillOpacity: 0.4
33            };
34        }
35
36        function onEachFeatureCurry(address) {
37            return function onEachFeature(feature, layer) {
38                if (feature.properties) {
39                    var popupContent = 'Type: ' + feature.properties.naurt_type;
40                    if (feature.properties.naurt_type == 'naurt_building') {
41                        popupContent += '<br>Address: ' + address;
42                    }
43                    layer.bindPopup(popupContent).openPopup();
44                }
45            }
46        }
47
48
49
50        var map = L.map('map').setView([0, 0], 2);
51        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' }).addTo(map);
52        var response = {{{ response }}};
53
54
55        L.geoJSON(response["best_match"]["geojson"], { style: style, onEachFeature: onEachFeatureCurry(response["best_match"]["address"]) }).addTo(map);
56
57        if ("additional_matches" in response) {
58            for (const additionalMatch of response["additional_matches"]) {
59                L.geoJSON(additionalMatch["geojson"], { style: style, onEachFeature: onEachFeatureCurry(additionalMatch["address"]) }).addTo(map);
60            }
61        }
62
63        var bounds = L.geoJSON(response["best_match"]["geojson"]).getBounds();
64        map.fitBounds(bounds);
65    </script>
66</body>
67
68
69</html>

I won’t walk through every detail of the template, as it’s pretty self-explanatory. I’ll just note that the basic idea here is to plot each aspect of the GeoJSON - naurt_door, naurt_parking, naurt_building - as its own layer. We also check to see if there are additional_matches, and if there are, loop over them and plot them in the same way as the best_match.

Then, we need to adjust ApiHandler slightly to accommodate this

1import com.github.mustachejava.DefaultMustacheFactory;
2import com.github.mustachejava.Mustache;
3import com.github.mustachejava.MustacheFactory;
4
5public class ApiHandler implements HttpHandler {
6
7    private final String apiUrl = "https://api.naurt.net/final-destination/v1";
8    private final String apiKey;
9
10    private final MustacheFactory mustacheFactory;
11
12    public ApiHandler() {
13        try {
14            this.apiKey = new String(Files.readAllBytes(Paths.get("api.key"))).trim();
15        } catch (IOException e) {
16            throw new RuntimeException("Failed to read API key from file", e);
17        }
18
19        this.mustacheFactory = new DefaultMustacheFactory();
20
21    }
22    
23    // ...
24    
25    private void handleGet(HttpExchange exchange) throws IOException {
26
27        try {
28            String query = exchange.getRequestURI().getQuery();
29
30            QueryParams queryParams = new QueryParams(query);
31            String jsonInputString = queryParams.convertToNaurtRequestJson();
32            HttpClient client = HttpClient.newHttpClient();
33            HttpRequest request = HttpRequest.newBuilder()
34                    .uri(URI.create(apiUrl))
35                    .header("Content-Type", "application/json")
36                    .header("Authorization", apiKey)
37                    .POST(HttpRequest.BodyPublishers.ofString(jsonInputString))
38                    .build();
39
40            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
41
42            // Model
43            Map<String, Object> model = new HashMap<>();
44            model.put("response", response.body());
45
46            // Get the template
47            Mustache mustache = mustacheFactory.compile("index.mustache");
48
49            // Render the template with the model
50            exchange.sendResponseHeaders(200, 0); // 0 means we are using chunked transfer encoding
51
52            try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) {
53                mustache.execute(writer, model).flush();
54            } catch (Exception e) {
55                e.printStackTrace();
56                exchange.sendResponseHeaders(500, -1);
57            }
58        } catch (Exception e) {
59            e.printStackTrace();
60            this.handleError(exchange);
61        }
62    }
63    
64    // ...
65    
66}

The basic idea here is to take the response from Naurt, render it into the Mustache template, and then return that to the user!

Try running and going to http://localhost:8080/?additional_matches=true&address_string=The Grand Hotel, Brighton now, you should see the following!

You can view the whole code at our GitHub.

Subscribe To Our Newsletter - Sleek X Webflow Template

Subscribe to our newsletter

Sign up at Naurt for product updates, and stay in the loop!

Thanks for subscribing to our newsletter
Oops! Something went wrong while submitting the form.