Introduction

In this tutorial we'll create an AWS application that uses HERE geocoding APIs to geocode a location and reverse geocode using coordinates.

Creating a Geocoding App with HERE on AWS

Most of the time, when you're trying to find a location on a map, you search for a location or an address. We're accustomed to this, but how does the application turn that into a pin on a map?

Mapping and location applications use precise latitude and longitude coordinates behind the scenes. To get from a store name or street address to coordinates is called geocoding. For example, "Rittenhouse Square, Philadelphia" geocodes into the coordinates (39.94711, -75.16943). In this case one of the coordinates is negative, indicating a latitude west of the Prime Meridian.

Reverse geocoding means transforming coordinates into a human-readable address.

AWS Lambda is Amazon\'s platform for serverless computing. HERE Technologies offers an application on AWS\'s Serverless Application Repository that exposes functions for geocoding and autocompletion of addresses. In this tutorial, I\'m going to show you how you can set up this application and expand it.

We'll also take a quick look at isoline routing, which takes the geocoded location and returns points a specified distance away.

The dependencies used in this tutorial are:

Setting Up the Application

Let's set up a simple geocoding API for an application that uses the HERE Technologies serverless applications available through AWS.

Start by logging into or registering your AWS account, then navigating to the HERE Location Services page in AWS Marketplace. This page gives you an overview of APIs, pricing, usage, and so on.

Click the Continue to Subscribe button to configure your software contract. The Standard tier should be sufficient for completing this tutorial.

After the contract is completed, you\'ll be prompted to log into or create a HERE developer account, then you\'ll see the Platform Activation page.

Under REST, click the Create API key button to create the API key. Make note of these for the rest of the tutorial.

Navigate to HERE's Geocode application on the AWS Serverless Application Repository and press the Deploy button:

You'll get redirected to a page that presents an overview of the application. In the bottom-right corner, you'll find the "Application settings" form:

Fill in the API key that you have generated before.

After filling in all application settings, press the Deploy button. AWS will take some time to generate the application, and you'll find two new functions and one new application on the AWS Lambda Console:

You can click on the application name to view its dashboard. An important section is Application endpoint which shows the URL to reach the application:

Each function has its own endpoint:

/geocode/{query} returns full information about the queried location (address, city, country, coordinates, and so on).

/geocodesuggest/{query} returns suggested locations for the query, with less detail.

For example, /geocode/California returns this JSON:

{
  "Response": {
    "MetaInfo": {
      "Timestamp": "2019-10-26T15:57:53.553+0000"
    },
    "View": [
      {
        "_type": "SearchResultsViewType",
        "ViewId": 0,
        "Result": [
          {
            "Relevance": 1,
            "MatchLevel": "state",
            "MatchQuality": {
              "State": 1
            },
            "Location": {
              "LocationId": "NT_4iga2oHrnVHBrEVd76rmQC",
              "LocationType": "point",
              "DisplayPosition": {
                "Latitude": 38.57944,
                "Longitude": -121.49085
              },
              "NavigationPosition": [
                {
                  "Latitude": 38.57944,
                  "Longitude": -121.49085
                }
              ],
              "MapView": {
                "TopLeft": {
                  "Latitude": 42.00946,
                  "Longitude": -124.40962
                },
                "BottomRight": {
                  "Latitude": 32.53429,
                  "Longitude": -114.13084
                }
              },
              "Address": {
                "Label": "CA, United States",
                "Country": "USA",
                "State": "CA",
                "AdditionalData": [
                  {
                    "value": "United States",
                    "key": "CountryName"
                  },
                  {
                    "value": "California",
                    "key": "StateName"
                  }
                ]
              }
            }
          }
        ]
      }
    ]
  }
}

And /geocodesuggest/California returns this JSON:

{
  "suggestions": [
    {
      "label": "United States, California",
      "language": "en",
      "countryCode": "USA",
      "locationId": "NT_4iga2oHrnVHBrEVd76rmQC",
      "address": {
        "country": "United States",
        "state": "California"
      },
      "matchLevel": "state"
    },
    {
      "label": "Brasil, Nova Iguaçu, Califórnia",
      "language": "pt",
      "countryCode": "BRA",
      "locationId": "NT_fImHvmkMfTo6YVKy6Rnv7D",
      "address": {
        "country": "Brasil",
        "state": "Rio de Janeiro",
        "city": "Nova Iguaçu",
        "district": "Califórnia",
        "postalCode": "26220-098"
      },
      "matchLevel": "district"
    },
    {
      "label": "Brasil, Barueri, Califórnia",
      "language": "pt",
      "countryCode": "BRA",
      "locationId": "NT_oZH9fLbZUl52pa-Gg77kfD",
      "address": {
        "country": "Brasil",
        "state": "São Paulo",
        "city": "Barueri",
        "district": "Califórnia",
        "postalCode": "06402-000"
      },
      "matchLevel": "district"
    },
    {
      "label": "Brasil, Belo Horizonte, Califórnia",
      "language": "pt",
      "countryCode": "BRA",
      "locationId": "NT_KA96faRjF5ZNGnkII6JAwC",
      "address": {
        "country": "Brasil",
        "state": "Minas Gerais",
        "city": "Belo Horizonte",
        "district": "Califórnia",
        "postalCode": "30855-220"
      },
      "matchLevel": "district"
    },
    {
      "label": "United States, MD, California",
      "language": "en",
      "countryCode": "USA",
      "locationId": "NT_f3V3dRtuzxencRR6tXxT6B",
      "address": {
        "country": "United States",
        "state": "MD",
        "county": "St Marys",
        "city": "California",
        "postalCode": "20619"
      },
      "matchLevel": "city"
    }
  ]
}

Preparing the Application Stack for Reverse Geocoding

Let's expand this application with a reverse geocoding function where the input parameters are coordinates and the output consists of suggested addresses.

First, we have to update our application stack. Go to your list of Applications and then to your geocoding application. Go to the Deployments tab and click the CloudFormation stack link:

The application stack is described by an editable template file. Press the Update button, then select Edit template in designer:

Press View in Designer (not the Next button). You'll see a visual designer and a text representation. Select YAML as the template language.

Add the following block to Resources:

  ReverseGeocodeFunctionGETPermissionTest: 

    Type: 'AWS::Lambda::Permission' 

    Properties: 

      Action: 'lambda:invokeFunction' 

      Principal: apigateway.amazonaws.com 

      FunctionName: !Ref ReverseGeocodeFunction 

      SourceArn: !Sub  

        - >- 

         arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/reversegeocode/* 

        - __Stage__: '*' 

          __ApiId__: !Ref ServerlessRestApi 

    ReverseGeocodeFunctionRole: 

    Type: 'AWS::IAM::Role' 

    Properties: 

      ManagedPolicyArns: 

        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' 

      AssumeRolePolicyDocument: 

        Version: 2012-10-17 

        Statement: 

          - Action: 

              - 'sts:AssumeRole' 

            Effect: Allow 

            Principal: 

              Service: 

                - lambda.amazonaws.com 

    ReverseGeocodeFunction: 

    Type: 'AWS::Lambda::Function' 

    Properties: 

      Code: 

        S3Bucket: ... 

        S3Key: ... 

      Description: >- 

        Reverse geocode 

      Tags: 

        - Value: SAM 

          Key: 'lambda:createdBy' 

      Environment: 

        Variables: 

          HERE_API_KEY: !Ref HereApiKey 

      Handler: reversegeocode.reversegeocodeGET 

      Role: !GetAtt  

        - ReverseGeocodeFunctionRole 

        - Arn 

      Runtime: nodejs8.10

In short, you're adding a Lambda permission, an IAM role and a function to the application stack. You still have to fill in the S3Bucket and S3Key values. Copy the ones from the GeocodeFunction section that already existed before you made your changes. This way, we're copying the code from GeocodeFunction into our newly created ReverseGeocodeFunction so we can edit it later.

reversegeocode.reversegeocodeGET as Handler in the ReverseGeocodeFunction block refers to the reversegeocodeGET function exported in reversegeocode.js. Both do not exist at this point, we'll write them later. You're free to rename the function or the file once we get there, but then you'll have to change it in the CloudFormation template as well.

After pasting in that code, press the Create Stack button in the top-left corner (it's the icon of a cloud with an arrow).

You'll get redirected to the Update stack screen. Click Next. Also click Next on the subsequent Specify stack details and Configure stack options screens, because we don't need to make changes here. On the Review screen, tick the box that you acknowledge that CloudFormation may create IAM resources, and press Update stack. You'll end up on the stack's Events tab in CloudFormation. Keep an eye on this page to see whether the update completes successfully.

When the update completes and you go back to the application in AWS Lambda, you should see 14 resources instead of 11:

Writing the Code for Reverse Geocoding

Click on ReverseGeocodeFunction in the table shown above and you'll see an editor for the function's code. Create a reversegeocode.js file:

With these contents:

'use strict';

const axios = require("axios");

const HERE_API_URL = 'https://reverse.geocoder.ls.hereapi.com/6.2/reversegeocode.json';
const HERE_API_KEY = process.env.HERE_API_KEY;
let statusCode = '200';

const getData = async url => {
    try {
        const response = await axios.get(url);
        statusCode = response.status;
        return response.data;
    } catch (error) {
        statusCode = error.response.status;
        console.log('error:'+error.response.data.Details);
        return error;
    }
};

exports.reversegeocodeGET = async (event, context) => {
    const lat = encodeURIComponent(event.pathParameters.lat);
    const long = encodeURIComponent(event.pathParameters.long);
    const radius = encodeURIComponent(event.pathParameters.radius);

    const url = `${HERE_API_URL}?apiKey=${HERE_API_KEY}&prox=${lat},${long},${radius}&mode=retrieveAddresses`;

    const hlsAPIResponse = await getData(url);

    const response = {
        statusCode: statusCode,
        body: (statusCode == '200') ? JSON.stringify(hlsAPIResponse) : JSON.stringify({ 'message' : hlsAPIResponse.response.data.Details })
    };

    context.succeed(response);
};

This code is based on the code in the two existing functions, Geocode and GeocodeSuggest. The only differences are the API URL, the exported function name, and the parameters read from the request. A quick overview of the code:

The constants at the beginning are the URL to the HERE API, your API KEY, as configured within the application. The API-KEY taken from the environment variables HERE_APP_KEY. statusCode is the HTTP status code that will be returned with the response and its value can be changed in case the HERE API returns an error.

The getData function uses axios to perform an HTTP request to the HERE API. It returns the API response (or the API error) and changes statusCode if the request was unsuccessful.

The handler function for web requests is exports.reversegeocodeGET. The event.pathParameters function is provided by AWS Lambda and will contain lat, long, and radius (after you've configured the routing on AWS, as you'll see in the next section).

Press Save to save your changes:

Updating the REST API

This is the last step before your function is ready to be called. From the Resources table of your application, click ServerlessRestApi. You'll get redirected to a list of Resources where you'll see /geocode and /geocodesuggest. Those resources take only one parameter in the path. Your function for reverse geocoding takes three: latitude, longitude, and search radius. The goal is to make the function accessible from the path:

/reversegeocode/{lat}/{long}/{radius}

{radius} will be a child resource of {long}, which will be a child resource of {lat}, which will be a child resource of /reversegeocode.

Start by creating the /reversegeocode resource by clicking Actions and Create Resource:

Fill in the resource path and resource name like this:

Then click Create Resource. Repeat this process for the {lat}, {long}, and {radius} child resources:

Now add a method to the {radius} child resource:

In the tree view at the left, a dropdown will appear:

Select GET and press the checkmark. The setup for the method will appear in the right pane. Check the Use Lambda Proxy Integration box (this makes event.pathParameters available in your code).

You can find the necessary value for Lambda Function in the Resources table of your application. Copy the Physical ID of the function there. (It won't be the same as in my screenshot below.)

Click Save.

If you're prompted about adding the API Gateway permission to invoke your Lambda function, click OK.

To finish, click Actions, and then Deploy API. Select Prod as the deployment stage and click Deploy.

Try it out by opening the following endpoint. The response contains JSON-formatted data with several suggested addresses, the first one being London's Trafalgar Square.

[ApplicationEndpoint]/reversegeocode/51.508039/-0.128069/10

The response JSON looks like:

{
  "Response": {
    "MetaInfo": {
      "Timestamp": "2019-10-26T17:19:58.866+0000",
      "NextPageInformation": "2"
    },
    "View": [
      {
        "_type": "SearchResultsViewType",
        "ViewId": 0,
        "Result": [
          {
            "Relevance": 1,
            "Distance": 0.3,
            "MatchLevel": "street",
            "MatchQuality": {
              "Country": 1,
              "State": 1,
              "County": 1,
              "City": 1,
              "District": 1,
              "Street": [
                1
              ],
              "PostalCode": 1
            },
            "Location": {
              "LocationId": "NT_ay6DhUBRc4geFwx1-ARDAD_l_734593605_L",
              "LocationType": "point",
              "DisplayPosition": {
                "Latitude": 51.5080372,
                "Longitude": -0.128073
              },
              "MapView": {
                "TopLeft": {
                  "Latitude": 51.50827,
                  "Longitude": -0.12818
                },
                "BottomRight": {
                  "Latitude": 51.5079,
                  "Longitude": -0.12801
                }
              },
              "Address": {
                "Label": "Trafalgar Square, London, WC2N 5, United Kingdom",
                "Country": "GBR",
                "State": "England",
                "County": "London",
                "City": "London",
                "District": "Charing Cross",
                "Street": "Trafalgar Square",
                "PostalCode": "WC2N 5",
                "AdditionalData": [
                  {
                    "value": "United Kingdom",
                    "key": "CountryName"
                  },
                  {
                    "value": "England",
                    "key": "StateName"
                  },
                  {
                    "value": "London",
                    "key": "CountyName"
                  }
                ]
              },
              ...

Isoline Routing Functionality

As a second example, let's add a function to our application that calculates an isoline using HERE's Routing API. An isoline is a polygon that answers questions like "What are all positions I can reach by car in 10 minutes starting from X?" or "What are all positions I can reach by walking 4 kilometers starting from Y?"

HERE's Routing API offers a lot of options for isolines. For simplicity, our Lambda function will only calculate isolines starting from a given location in coordinates and showing the points that can be reached by driving X meters with a car. That means we have three parameters: latitude, longitude, and range (in meters). Our function path can then be:

/isoline/{lat}/{long}/{range}

The steps for adding this function are the same as for the reverse geocoding function. Start by adding this to the Resources block of your CloudFormation stack template:

 IsolineFunctionGETPermissionTest:
    Type: 'AWS::Lambda::Permission'
    Properties:
      Action: 'lambda:invokeFunction'
      Principal: apigateway.amazonaws.com
      FunctionName: !Ref IsolineFunction
      SourceArn: !Sub 
        - >-
          arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/isoline/*
        - __Stage__: '*'
          __ApiId__: !Ref ServerlessRestApi
  IsolineFunctionRole:
    Type: 'AWS::IAM::Role'
    Properties:
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action:
              - 'sts:AssumeRole'
            Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
  IsolineFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        S3Bucket: ...
        S3Key: ...
      Description: >-
        Isoline functionality
      Tags:
        - Value: SAM
          Key: 'lambda:createdBy'
      Environment:
        Variables:
          HERE_API_KEY: !Ref HereApiKey
      Handler: isoline.isolineGET
      Role: !GetAtt 
        - IsolineFunctionRole
        - Arn
      Runtime: nodejs8.10

Again, copy S3Bucket and S3Key from the GeocodeFunction block. After updating the stack, you can navigate to the newly created IsolineFunction by clicking the link in the Resources table of the application.

The JavaScript code for this function, which we'll put in a file called isoline.js, looks very similar to our code for the reverse geocoding. The only differences are the API URL, the path parameters, and the name of the exported function.


'use strict';

const axios = require("axios");

const HERE_API_URL = 'https://isoline.route.ls.hereapi.com/routing/7.2/calculateisoline.json';
const HERE_API_KEY = process.env.HERE_API_KEY;
let statusCode = '200';

const getData = async url => {
    try {
        const response = await axios.get(url);
        statusCode = response.status;
        return response.data;
    } catch (error) {
        statusCode = error.response.status;
        console.log('error:'+error.response.data.Details);
        return error;
    }
};

exports.isolineGET = async (event, context) => {
    const lat = encodeURIComponent(event.pathParameters.lat);
    const long = encodeURIComponent(event.pathParameters.long);
    const range = encodeURIComponent(event.pathParameters.range);

    const url = `${HERE_API_URL}?apiKey=${HERE_API_KEY}&mode=shortest;car;traffic:disabled&start=geo!${lat},${long}&range=${range}&rangetype=distance`;

    const hlsAPIResponse = await getData(url);

    const response = {
        statusCode: statusCode,
        body: (statusCode == '200')? JSON.stringify(hlsAPIResponse) : JSON.stringify({ 'message' : hlsAPIResponse.response.data.Details })
    };

    context.succeed(response);
};

Press "Save" after creating this file.

Finally, update the REST API. Add the resource /isoline/{lat}/{long}/{range} with the GET method, like so:

Deploy the API to Prod and your new function is live! Try requesting [ApplicationEndpoint]/isoline/52.51578/13.37749/4000 which represents a travel distance of 4km from the center of Berlin:

{
  "response": {
    "metaInfo": {
      "timestamp": "2019-10-26T17:41:23Z",
      "mapVersion": "8.30.101.157",
      "moduleVersion": "7.2.201942-5589",
      "interfaceVersion": "2.6.74",
      "availableMapVersion": [
        "8.30.101.157"
      ]
    },
    "center": {
      "latitude": 52.51578,
      "longitude": 13.37749
    },
    "isoline": [
      {
        "range": 4000,
        "component": [
          {
            "id": 0,
            "shape": [
              "52.5139618,13.3210087",
              "52.5139618,13.3222961",
              "52.5141335,13.3228111",
              "52.5144768,13.3231544",
              "52.5146484,13.3236694",
              "52.5146484,13.3243561",
              "52.5144768,13.3248711",
              "52.5141335,13.3252144",
              "52.5139618,13.3257294",
              "52.5139618,13.326416",
              "52.5141335,13.326931",
              ...

Conclusion

We have deployed HERE's serverless geocoding application to AWS Lambda and expanded it with functions for reverse geocoding and isoline calculation. If you want to dive deeper, take a look at the related documentation of HERE and AWS:

results matching ""

    No results matching ""