LocationIQ Integration with AIMMS

Summary

This article demonstrates how to replace the legacy GeoFindCoordinates function in AIMMS with a high-performance, asynchronous integration using the LocationIQ REST API and the AIMMS Data Exchange (DEX) library. The article covers how to configure API authentication, construct RESTful requests, map JSON responses to AIMMS identifiers, and implement robust callback procedures to handle both successful data retrieval and potential communication errors.

The legacy AIMMS function GeoFindCoordinates is constrained by its reliance on Nominatim. Nominatim enforces strict rate limits, typically permitting at most one GPS coordinate request per second, which can significantly impede performance for batch geocoding tasks.

To overcome these limitations, AIMMS applications can utilize external REST services. While this article features LocationIQ as the primary example, the implementation logic remains consistent across most modern geocoding providers.

Please use this example to follow along this article:

Geocoding Service Selection

The choice of geocoding provider depends on data coverage requirements, pricing, and terms of service. We utilize LocationIQ for this demonstration due to its ease of setup, robust documentation, and generous free-tier rate limits.

Note

Since AIMMS handles HTTP requests generically via the Data Exchange library, you can adapt this approach for other services:

By using the AIMMS Data Exchange (DEX) Library, these services are accessed asynchronously, ensuring the user interface remains responsive during network operations.

Prerequisites and Configuration

Obtaining an Access Token

To authenticate with the LocationIQ API, you must obtain a unique Access Token. This token identifies your application and monitors usage limits. You can generate one at the LocationIQ Dashboard.

../../_images/LocationIQ-dashboard.png

Specifying the Token in AIMMS

Once obtained, the token must be stored within your AIMMS application. In the provided example project, the token is entered on the configuration page and stored in the scalar parameter sp_accessToken.

In the enclosed AIMMS App, navigate to the page: liq::accesskey.

../../_images/libLocationIQ-ask-accesskey.png

Implementation Steps

The integration follows a three-step process: constructing the request, mapping the JSON response, and handling the result via a callback.

Constructing the API Request

The Geocoding endpoint (/v1/search) converts a human-readable address into geographic coordinates (Latitude and Longitude). This process is known as forward geocoding.

../../_images/ask-address-to-det-GPS-coords.png

The API call is constructed as a standard HTTP GET request. The URL must include the Access Token, the query string (q), and the output format (format=json).

1_sp_url :=
2    formatString("https://%s.locationiq.com/v1/search?" , _sp_reg) +
3    formatString("key=%s&", sp_accessToken) +
4    formatString("q=%s&format=json&", _sp_query);

The request is executed using dex::client::NewRequest. We specify a response file and an asynchronous callback (_ep_callback).

 1dex::client::NewRequest(
 2    theRequest    :  _sp_theRequest,
 3    url           :  _sp_url,
 4    callback      :  _ep_callback,
 5    httpMethod    :  'GET',
 6    requestFile   :  "",
 7    responseFile  :  sp_libfolder + "/data/getLocation.json",
 8    traceFile     :  "",
 9    requestOffset :  0,
10    requestSize   :  0);
11dex::client::AddRequestTag(_sp_theRequest, _sp_theRequest);
12dex::client::PerformRequest(_sp_theRequest);

The JSON Response Structure

The API returns a JSON array. Each object in the array represents a matching location with its latitude and longitude.

 1[
 2    {
 3        "boundingbox": [
 4            "53.0625606",
 5            "53.1235639",
 6            "4.7043896",
 7            "4.8031591"
 8        ],
 9        "class": "boundary",
10        "display_name": "De Koog, Texel, North Holland, Netherlands",
11        "icon": "https://locationiq.org/static/images/mapicons/poi_boundary_administrative.p.20.png",
12        "importance": 0.653932315897569,
13        "lat": "53.0998817",
14        "licence": "https://locationiq.com/attribution",
15        "lon": "4.7626457",
16        "osm_id": "2730421",
17        "osm_type": "relation",
18        "place_id": "157813526",
19        "type": "administrative"
20    },
21    { "...":"..." }
22]

Mapping JSON to AIMMS Identifiers

The AimmsJSONMapping instructs dex::ReadFromFile how to translate the JSON data. The root array maps to the AIMMS index liq::i_result, and specific values are bound to liq::i_placeId, liq::p_Latitude, and liq::p_Longitude.

1<AimmsJSONMapping>
2    <ArrayMapping>
3        <ObjectMapping iterative-binds-to="liq::i_result">
4            <ValueMapping name="place_id" binds-to="liq::i_placeId"/>
5            <ValueMapping name="lat" maps-to="liq::p_Latitude(liq::i_result,liq::i_placeId)"/>
6            <ValueMapping name="lon" maps-to="liq::p_Longitude(liq::i_result,liq::i_placeId)"/>
7        </ObjectMapping>
8    </ArrayMapping>
9</AimmsJSONMapping>

The Callback Procedure

The callback procedure, specified via _ep_callback, is automatically executed once the HTTP request completes. It acts as the “bridge” between the external JSON file and your AIMMS model logic.

The logic follows these steps:

  • Success Handling (Status 200):
    • The dex::ReadFromFile function uses the defined mapping (getLocationJSON) to parse the response file and populate the indexed parameters p_latitude and p_longitude.

    • Since the API can return multiple matches, the code typically isolates the first result (the most relevant match).

    • The coordinates from this first result are then assigned to global scalar parameters (p_globLat, p_globLon) for immediate use in the model or on a map UI.

  • Error Handling:
    • If the statusCode indicates a failure (e.g., 401 for an invalid key or 429 for rate limiting), the execution jumps to an error handling routine to alert the user.

The following callback routine captures and transforms the response:

 1if statusCode = 200 then
 2    dex::ReadFromFile(
 3        dataFile         :  sp_libfolder + "/data/getLocation.json",
 4        mappingName      :  "getLocationJSON",
 5        emptyIdentifiers :  0,
 6        emptySets        :  0,
 7        resetCounters    :  0);
 8
 9    _ep_first_res := first( s_results );
10    _ep_first_pid := first( s_placeIds );
11    p_globLat := p_latitude(  _ep_first_res, _ep_first_pid );
12    p_globLon := p_longitude( _ep_first_res, _ep_first_pid );
13else
14    ! Error handling.

Error Handling

When communicating with external APIs, it is essential to handle potential network issues or invalid queries.

 1if statusCode = 0 then
 2    ! Message from CURL.
 3    dex::client::GetErrorMessage(errorCode,_sp_curl_message);
 4    _sp_error_message := formatString("Error obtaining GPS coordinates from LocationIQ for \"%s\", CURL details: \"%s\"",
 5        sp_req_address, _sp_curl_message);
 6else
 7    ! Message from Server
 8    _ep_statusCode := StringToElement(dex::HTTPStatusCodes, formatString("%i", statusCode),create:0);
 9    if _ep_statusCode then
10        dex::ReadFromFile(
11            dataFile         :  sp_libfolder + "/data/getLocation.json",
12            mappingName      :  "errorJSON",
13            emptyIdentifiers :  0,
14            emptySets        :  0,
15            resetCounters    :  0);
16        _sp_error_message := formatString("Error obtaining GPS coordinates from LocationIQ for \"%s\", status code %e: \"%s\", error code: %i, LocationIQ feedback: \"%s\"",
17            sp_req_address, _ep_statusCode, dex::HTTPStatusCodeDescription(_ep_statusCode), errorCode, sp_locationIQ_error_message );
18    else
19        ! Shouldn't happen, unknown status/error code.
20        _sp_error_message := formatString("Error obtaining GPS coordinates from LocationIQ for \"%s\", unknown status code: %i, error code: %i",
21            sp_req_address, statusCode, errorCode );
22    endif ;
23endif ;
24raise error _sp_error_message ;

Remarks:

  • Lines 3-5: Handles cases where the server cannot be reached (CURL errors).

  • Lines 10-17: Handles server-side errors (e.g., 401 Unauthorized) by reading the error feedback from the JSON response.