A step-by-step guide
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.
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
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!
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.
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!
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.
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
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!
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 >
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: '© <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.