Thursday, February 14, 2019

Your Azure Functions Can Be Excel Functions

In this post, I'll show how you can define custom Excel functions that invoke your Azure Functions in the cloud. This is an easy and simple procedure.

The End Result: What We're Building

For our example, we'll imagine an Azure Function named ConvertLength that performs simple length conversions, such as from meters to feet or inches to meters. After we create our function, we'll write some VBA code in Excel to connect to it. The end result will be a custom function in Excel that, when run, executes an Azure Function in the cloud.

=ConvertLength(A1, B1, D1)

Excel function ConvertLength is executing Azure Function 

Creating the Azure Function

The function we're creating, ConvertLength, is an HTTP Trigger-invoked in-portal function written in C#. ConvertLength expects 3 parameters:
  • value : a value to convert, such as 832
  • from : a from unit of measurement, such as ft
  • to: a destination unit of measurement, such as in

The result will be the value in the destination unit of measurement, such as 9984

We create our function app in the Azure portal and then our function named ConvertLength. As we do so, we specify an HTTP Trigger, development in the portal, in .NET Core / C#.

Here is the C# code for our function:
#r "Newtonsoft.Json"

using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;

public static async Task<iactionresult> Run(HttpRequest req, ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");

    double fromValue = Convert.ToDouble(req.Query["value"]);
    string fromUnits = NormalizeUnits(req.Query["from"]);         // m|meter|meters, ft|foot|feet, in|inch|inches
    string toUnits = NormalizeUnits(req.Query["to"]);

    if (fromUnits=="?")
    {
        return (ActionResult)new OkObjectResult($"invalid 'from' parameter - unrecognized unit of measurement");
    }
    if (toUnits=="?")
    {
        return (ActionResult)new OkObjectResult($"invalid 'to' parameter - unrecognized unit of measurement");
    }

    double toValue = 0.00;
    switch(fromUnits)
    {
        case "m":
            switch(toUnits)
            {
                case "m":
                    toValue = fromValue;
                    break;
                case "ft":
                    toValue = fromValue * 3.28084;
                    break;
                case "in":
                    toValue = fromValue * 3.28084 * 12;
                    break;
            }
            break;
        case "ft":
            switch(toUnits)
            {
                case "ft":
                    toValue = fromValue;
                    break;
                case "in":
                    toValue = fromValue * 12;
                    break;
                case "m":
                    toValue = fromValue * 0.3048;
                    break;
            }
            break;
        case "in":
            switch(toUnits)
            {
                case "in":
                    toValue = fromValue;
                    break;
                case "ft":
                    if (fromValue==0)
                        toValue = 0.00;
                    else
                        toValue = fromValue / 12;
                    break;
                case "m":
                    toValue = fromValue * 0.3048;
                    break;
            }
            break;
    }

    return (ActionResult)new OkObjectResult(toValue.ToString());
}

// Normalize units to standard expected values.

private static string NormalizeUnits(String units)
{
    switch(units.ToLower())
    {
        case "m":
        case "meter":
        case "meters":
            return "m";
        case "ft":
        case "foot":
        case "feet":
            return "ft";
        case "in":
        case "inch":
        case "inches":
            return "in";
        default:
            return "?";
    }
    return units;
}
ConvertLength source code

ConvertLength isn't very far along--it only understands feet, inches, and meters as units of measurement--but could become extensive with a little more work. For the purposes of our demo, it's nice to work with a small amount of code.

We can test this in the portal, but also from a browser. Copying the function URL from the portal, here's a test in a browser:

Testing ConvertLength Azure Function in Browser

We can see the function is working. We converted 1 m = 3.28084 ft, 18 ft = 5.4864m, and 5280 ft = 63,360 inches.

Creating The Excel Function

Now that we have a working Azure Function, we need to create an Excel Function to invoke it. To do that, you'll need to get set up for VBA development in Excel. You'll need to do the following in Excel:
  1. Activate the VBA Add-in. Open Excel, select File > Options, select Add-ins. Select Analysis ToolPack - VBA and click OK.
  2. Enable Access to VBA. In Excel, select File > Options, and go to Trust Center. Click Trust Center Settings and search for Macro Settings. Click the Trust access to the VBA project object model checkbox.
  3. Enable Developer Menu. Go to File > Options and select Customize Ribbon. On the list of Main Tabs on the right, check the Developer checkbox and click OK.
  4. Add References for Internet Controls. Select Tools > References from the menu, check the following and click OK: Mirosoft Internet Controls, Microsoft WinHTTP Services, Microsoft Scripting Runtime.
With the above out of the way, you should be able to enter Atl+F11 in Excel and have a Microsoft Visual Basic for Applications environment open up. This is where we'll add our code.

Right-click the VBAProject in the outline at left and select Insert Module. In the module, enter the code for the Excel ConvertLength function.

Excel VBA Code 

Below is a listing of the function code. Let's walk through what it's doing:

  • The function declaration is named ConvertLength and expects three string parameters: value (number we want to convert), from (the source unit of measurement), and to (the destination unit of measurement). Value will actually be a number, but the function treats everything as string for ease of putting the URL together.
  • A URL is assembled: the base URL of our Azure function plus query parameters value, from, and to, whose values are the parameters passed in to the function.
  • Next, MSXML2.XLHTTP is called to make an HTTP GET request using variable request.
  • Lastly, the function result is th request object's ResponseText parameter.
Function ConvertLength(value As String, fromUnits As String, toUnits As String) As String

Dim url As String
Dim request As Object

url = "https://globalization.azurewebsites.net/api/ConvertLength?value=" + value + "&from=" + fromUnits + "&to=" + toUnits

Set request = CreateObject("MSXML2.XMLHTTP")
    With request
        .Open "GET", url, False
        .Send
    End With

ConvertLength = request.ResponseText
End Function
Excel ConvertLength VBA Code

Save the module file by clicking the disk icon at top in the VBA development environment.

Using The Function in Excel

Now it's time to see it all work together in Excel. In your Excel worksheet, enter a number in cell A1, a unit ("m", "ft", or "in") in B1, and another unit in D1.

Excel parameter values

Now, go to cell C1. Up on the formula bar, enter this formula and press ENTER.

=ConvertLength(A1, B1, D1)

Right after you press ENTER, C1 should display the converted value. Pressing ENTER caused the local Excel function code to run, which in turn invoked the Azure Function.


Now that we see how straightforward this is, we can use the function as much as wish in the spreadsheet:


In Conclusion

In this post, we saw how easy it is to connect an Excel function to an Azure Function which can then be used in a natural, familiar way by Excel users. This provides yet another avenue to serverless computing from a familiar application, one with 750 million users: Excel.

Wednesday, February 13, 2019

CIA World Factbook Data on Azure, Part 2: Front-end API & Web Site using Cosmos DB and Azure Functions

In this 2-part post, I'm going to show how you can take CIA World Factbook data and use it for your own purposes on Microsoft Azure. 


Previously in Part 1 we noted that the CIA World Factbook data is public domain. We created the back-end to collect data and store it in Azure Cosmos DB and blob storage, using an Azure Durable Function for the data collection. With that work completed, our Azure-based data repository will now update once a week, all by itself.


Today in Part 2 we will create the front-end, which will include both an API and a web site--powered by Azure Functions and Cosmos DB. With that, we'll finally be able to access and use all that data we collected. You can access the web site at at https://worldfactbook.blob.core.windows.net/site/index.html.

  

What We're Building

Today we'll be creating three things to form our front-end for the World Factbook data we retrieved in Part 1:
  1. Azure Functions. We're going to use Azure Functions to query the Cosmos DB. We'll need functions to look up country data and perform searches.
  2. API. We'll front-end the Azure Functions with a formal REST API using Azure API Management.
  3. Web Site. We'll create a web site that allows browsing and searching of world country data via the API. This site will work on both desktop and mobile devices. 

Azure Functions

We'll need several Azure Functions. In Part 1, we developed an Azure Durable Function for data collection, which was written in C# / .NET Core, working locally in Visual Studio. Today, we're going to change our development experience: our front-end Azure Function will be written in JavaScript, and we'll enter and test it via the Azure Portal.

Our API and web site only need a few simple functions:

  • Retrieve the data for a selected country
  • Search the data for countries containing a search term

For the first item, country lookup, we could return the entire country record (which you recall from Part 1 is quite large); or we could return sections of the country record such as the Geography section or the People section. We're going to do both: the function country will return an entire country record, and functions named geography, people, and so on will return just those sections of a country record. All of these functions will work the same way: an input parameter will specify a country key (such as "australia"); the result will be complete or partial country record JSON. In addition we will need a search function; it will accept a search term, and return a list of country name/key pairs.

We begin by creating an Azure Function App project in the portal named world-factbook. As we create the Function App and functions, we're selecting the option to develop in the portal, and to do it in JavaScript (Node.js). Our functions will be accessed via HTTP, so we'll use HTTP Triggers.


All of these functions need to access our Cosmos DB database, which I've previously only done in C#. In Part 1, you'll recall we installed a NuGet package and created a DatabaseClient in our C# code, wihch allowed us to perform actions like CreateDocumentQuery, CreateDocumentAsync, and DeleteDocumentAsync. We also had to carefull code a retry pattern around our database access. Clearly I would need to find the equivalent for JavaScript and get to work.

I was expecting to find a very similar library for JavaScript with similar methods and patterns... but I didn't. Instead, I kept running into documentation, guidance, and StackOverflow posts encouraging me to use Azure Cosmos DB Bindings, which are settings you configure for functions in the portal. This didn't sound right at all, so I resisted the idea at first, but eventually realized I would need to give it a try since I wasn't finding much of an alternative....

...and I'm glad I did! Azure Cosmos DB Bindings are fantastic. I'm now a believer, and you will be too if you give them a try.

country 

Let's walk through how I used bindings in the country function, whose job is to look up and return one country record. Conceptually, we need to get an incoming country key (such as "brazil") and use it to execute this query:

SELECT * FROM c WHERE c.key={key}

The result is our country record, which the function will return. That sounds easy enough, but what are the mechanics of getting at the data in an in-portal JavaScript Azure Function?

Bindings to the rescue. In the portal, we go to the Integration section of our function where we can readily see our HTTP Trigger. But there's more than triggers on this page; there are also Input bindings and Output bindings. Following a quickstart, I clicked New Input under Inputs and created a new Cosmos DB binding.

Adding a Cosmos DB Input Binding under Function Integration

Next we need to configure the binding, which means filling out a form with the following:

  • Document parameter name: this is the variable name we will get in our code with the results of a database query, which is an array of documents/records. I used countryList.
  • Collection name: the name of the Cosmos collection. Our collection is named Country.
  • Document ID: A specific document ID; not applicable to what we're doing.
  • SQL Query: The query we want to execute. For this function, it is:
    SELECT * FROM c where c.key = {country}
  • Database name: Name of the Cosmos DB--mine is named Factbook.
  • Cosmos DB account connection: A key name of a configuration record which will be stored in function.json. I kept the generated name of factbook_DOCUMENTDB.
  • Paritition key: Paritition key to use. All of our 260 country records use the same partition key (Factbook).

Configured Cosmos DB Input Binding

Now that we have a binding configured, we can write our function code--and you won't believe how short it is. Here's the code:
// Function: geography - returns the geography section of a Country document
// Inputs: country - country key (example: algeria)
// Outputs: country JSON, or status 400 if not found

module.exports = function (context, req, countryList) {

    if (!countryList || countryList.length===0)
    {
        context.log("Country not found");

        context.res = {
                status: 400
        };
        context.done();
    }
    else
    {
        context.log("Found Country, name=" + countryList[0].name);

        context.res = {
                body: countryList[0]
        };
        context.done();
    }


};
country function JavaScript code

Wait, where's all the code to access the database? You don't need any, because the binding already executed the database query by the time this code executes. The result JSON is passed to us in the variable name we asked for--countryList--and all we have to do is process and return the results.

Hmm. But we want the country key to be passed in as a URL parameter, how does the Cosmos DB Binding know to do that? Well, in our SQL query we specified {country} as a parameter name. The binding is smart enough to realize we are configured with an HTTP Trigger, and therefore automatically looks for a URL parameter named country

If this sounds auto-magical, it really is. But the proof is in the pudding, and we can use the Test pane of the portal to prove the function works. We set our HTTP method to GET and add a query parameter named country and set it to a country key we know is in the database. Then we click Run and we get a complete country record response. If we test again with a non-existing parameter, we instead get HTTP 400, the status our JavaScript was coded to return for no-result. 

    
Testing the country function with valid and invalid input

They're truly a thing of beauty, these database bindings. And I wrote so little code, I'm almost ashamed.

geography

With our first function finished and working, it should be a slam dunk to get the very-similar geography function: the only difference is that it returns a section of a country record instead of the complete record.

There are in fact two ways we could go about writing this function:
1. Use the same binding configuration as we did for the country function, but change the JavaScript code to only return the .geography portion of the result. Or,
2. Change the query from SELECT * FROM c to SELECT c.geography FROM c, so that only the geography section is selected.

Either approach would work, but I went with #1. Accordingly, we have an identical Cosmos DB Input Binding configuration to country, but slighty different code:
// Function: geography - returns the geography section of a Country document
// Inputs: country - country name (example: Algeria)
// Outputs: people JSON

module.exports = function (context, req, countryList) {

    if (!countryList || countryList.length===0)
    {
        context.log("Country item not found");

        context.res = {
                status: 400
        };
        context.done();
    }
    else
    {
        context.log("Found Country item, name=" + countryList[0].name);

        context.res = {
                body: countryList[0].geography
        };
        context.done();
    }

};
geography function JavaScript code

The only difference is line 21, which returns the .geography section of the country record.

people

The people function is exactly like geography, except line 21 returns the .people section. Our functions are getting easier and easier.

        context.res = {
                body: countryList[0].geography
        };

Now, to complete matters I really should create similar functions for the other sections of the country record: Introduction, Government, Economy, Energy, Communications, Transporation, Military and Security, and Transnational Issues. But I'm not going to worry about that right now, because our web site will only be using country. As we've seen, we're now at the point where writing these kind of functions is trivial.

search

On the web site, search will be called after the user has entered a search term and clicked the search button. The results are then displayed, and the user can click on a result to see the full country record. 

Up until now, our functions have been very similar in that they each look up a country by key and return a country record or a portion of a country record. search is a little different: our input parameter is a search term, not a country key; and we'll be returning a collection of results rather than a single record.

We don't need to return complete country records, just their identifiers. Our function will return an array of objects that contain just two properties, name and key. That's enough to display country names in search results, and to have the key to pull up the full country record should we want to.

Once again we set up a Cosmos DB Binding. How do we perform a search in Cosmos DB? There is a SQL CONTAINS(field, value) function that we can use. However, we'll need to have WHERE clauses for each field we want to check. Our SQL query is below. We'll call the incoming argument term this time. 

SELECT c.name,c.key FROM c WHERE CONTAINS(c.name,{term})
OR CONTAINS(c.key,{term})
OR CONTAINS(c.introduction.background, {term})
OR CONTAINS(c.geography.map_references, {term})
OR CONTAINS(c.geography.climate, {term})
OR CONTAINS(c.geography.terrain, {term})
OR CONTAINS(c.geography.population_distribution, {term})
OR CONTAINS(c.government.government_type, {term})
OR CONTAINS(c.economy.overview, {term})
OR CONTAINS(c.transnational_issues.disputes[0], {term})
ORDER by c.name

The query is checking the country name/key and primarily the large text sections. Ideally we'd like to do a full-text search on our country records, but I haven't found a way to do that as of yet. It's too bad there isn't a function to search the entire document for a contains match but as far as I can tell there's no such animal. Also, CONTAINS is case-sensitive, so a search for "mountain" won't match "Mountain" in the database. Despite these limitations, search is still a useful and important feature even in its present form; and I'll be looking to improve its reach over time.

Here's our JavaScript function code:
// Function: search - returns names and keys of country records containing search term
// Inputs: term - search word
// Outputs: array of objects with name and key properties. May be 0-length if no matches.

module.exports = function (context, req, countryList) {

    if (!countryList)
    {
        context.log("No matches");
        context.res = {
            body: []
        };
        context.done();
    }
    else
    {
        context.log("Found Country items, count=" + countryList.length.toString());

        context.res = {
            body: countryList
        };
        context.done();
    }

};
search function JavaScript code

Now to test the function. We add a query parameter named term and set it to 'socialist'. The function returns 15 matches, and the results have the expected country names and keys.

Testing Search Function

At this point we are finished with our Azure Functions. 

API

Our functions all use HTTP Triggers, meaning we could now just access our functions  using their HTTP URLs, but instead we're going to use Azure API Management to create an API for getting at them. There are a number of reasons why you might want to front-end your Azure Functions with an API:
  • Shaping a REST API with URL paths you prefer
  • Controlling access to authorized parties
  • Enforcing policies
  • Tracking activity

Since this project is just a demonstration, there's no need to restrict access. Both the API and the Azure Functions were configured for anonymous access. This involved tracking down a number of settings and convincing the API to not require subscriber tokens.

In the Azure Portal under API Managment we create an API named World Factbook and define an operation for each of the four functions we created in the previous section. 

API definition for country operation

Our API's base URL is https://factbook.azure-api.net/world-factbook/ and will support these operations:
  • /country/{country} returns a country record for the specified country.
    Behind the scenes, this calls
    https://world-factbook.azurewebsites.net/api/country?country={country}
  • /country/{country}/geography returns the geography portion of a country record for the specified country.
    Behind the scenes, this calls
    https://world-factbook.azurewebsites.net/api/geography?country={country}
  • /country/{country}/people returns the people portion of a country record for the specified country.
    Behind the scenes, this calls
    https://world-factbook.azurewebsites.net/api/people?country={country}
  • /search/{term} performs a search and returns a collection of matching country names and keys.
    Behind the scenes, this calls
    https://world-factbook.azurewebsites.net/api/search?term={term}
How do we get the API to reformat the back-end function URLs from the paths it is given? We configure a rewrite-uri policy on inbound processing. We also configure a cors policy to allow Cross-Original Resource Sharing, so a web site invoking the API doesn't get CORS errors.
<!--
    IMPORTANT: 
    - Policy elements can appear only within the <inbound>, <outbound>, <backend> section elements.
    - To apply a policy to the incoming request (before it is forwarded to the backend service), place a corresponding policy element within the <inbound> section element.
    - To apply a policy to the outgoing response (before it is sent back to the caller), place a corresponding policy element within the <outbound> section element.
    - To add a policy, place the cursor at the desired insertion point and select a policy from the sidebar.
    - To remove a policy, delete the corresponding policy statement from the policy document.
    - Position the <base> element within a section element to inherit all policies from the corresponding section element in the enclosing scope.
    - Remove the <base> element to prevent inheriting policies from the corresponding section element in the enclosing scope.
    - Policies are applied in the order of their appearance, from the top down.
    - Comments within policy elements are not supported and may disappear. Place your comments between policy elements or at a higher level scope.
-->
<policies>
    <inbound>
        <base />
        <set-backend-service base-url="https://world-factbook.azurewebsites.net/api" />
        <set-query-parameter name="term" exists-action="override">
            <value>{term}</value>
        </set-query-parameter>
        <rewrite-uri template="/search?term={term}" />
        <cors>
            <allowed-origins>
                <origin>*</origin>
            </allowed-origins>
            <allowed-methods>
                <method>GET</method>
                <method>POST</method>
            </allowed-methods>
            <allowed-headers>
                <header>*</header>
            </allowed-headers>
            <expose-headers>
                <header>*</header>
            </expose-headers>
        </cors>
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>
API Inbound Processing Policy

Although we can and did test the API via the Azure Portal, the ultimate test is hitting it from a browser. Here's an example of accessing the /country/{country} API from a browser, and the resulting country record JSON.

Testing API from a Browser

With our API created and working, we now have an official access point for our Azure Functions. All that remains is a user interface.

Web Site

Now it's time to create a web site so that users can browse and search the world country data. We now have all the components in place to do this easily.

Our web site consists of only 3 files, and we're hosting from Azure blob storage.
  1. index.html: web page and JavaScript
  2. site.css: styles
  3. old-world-map.jpg: background image (Creative Commons license)

Layout

On the desktop, the web site looks like this when loaded. We have the following visual components:
  • Banner, identifying site, the major Azure services used, and attribution of the data source
  • Search box and search button, which can be used to search
  • Select list, from which a country can be selected to view
  • Accordion grouped into sections (Introduction, Geography, People, Government...). Once a country is selected, each section is loaded with content.


On a mobile device, the top elements are rearrange slightly but usage is the same.

Usage

To view a country's information, select one of the 260 countries in the drop-down. The country's flag and name are displayed, and the accordion sections load with data.

Introduction Section

Expand or collapse accordion sections to view the different sections of content. I have by no means exposed all of the content, the site is showing only a fraction of the available data.

 
Geography Section

People Section

Selecting another country updates the content, and you can continue to browse.

Another way to get at country data is via searching. Enter a term in the search box (case-sensitive) and click the search button. The content accordion is replaced with a list of search results (country flags and names). Click on a country to view it's data, with the same view just described.

Search Results

API Usage

Let's discuss how the site interacts with the API. When a selection is made from the site's country list, a call is made to JavaScript function countrySelectionChanged(). That code generates the country key from the country name (e.g. turns Australia into australia or Korea, North into korea_north) and issues an HTTP GET via Ajax to retrieve the full JSON country record.
    // Retrieve JSON country record

    var url = 'https://factbook.azure-api.net/world-factbook/country/' + countryKey;
    $.ajax({
        type: 'GET',
        url: url,
        accepts: "json",
    }).done(function (result) {

        ...load content into accordion sections...
    }
Once the country record is received, various fields form the record need to be loaded into HTML fragments for the accordion sections. Because we can't guarantee that any particular field will be present in every country record, there's a lot of conditional JavaScript code checking that data.government exists, data.government.government_type exists, and so on. Note that we could have used something like Angular and an HTML template for rendering the content instead of JavaScript code, but way you'd still have to work through the same logic one way or another.
// Load content: Government

var govt = '';
if (data.government) {
    if (data.government.government_type) {
        govt += '<div class="item"><b>Type</b><br/>' + data.government.government_type + '</div>';
    }
    if (data.government.capital && data.government.capital.name) {
        govt += '<div class="item"><b>Capital</b><br/>' + data.government.capital.name + '</div>';
    }
    if (data.government.legal_system) {
        govt += '<div class="item"><b>Legal System</b><br/>' + data.government.legal_system + '</div>';
    }
    if (data.government.national_holidays) {
        govt += '<div class="item"><b>National Holidays</b><br/>'
        for (var i = 0; i < data.government.national_holidays.length; i++) {
            var elem = data.government.national_holidays[i].name + ' (' + data.government.national_holidays[i].day + ')';
            if (i == 0)
                govt += elem;
            else
                govt += ", " + elem;
        }
        govt += '</div>';
    }
}
$('#content-government').html(govt);
Loading Content from Country Record

Here's the complete index.html.
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>World Country Data</title>
    <link rel="icon" href="https://worldfactbook.blob.core.windows.net/site/favicon.ico">
    <link rel="stylesheet" href="site.css" />
    <link rel="https://fonts.googleapis.com/css?family=Nunito:200,400,700" type="text/css" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
</head>

<body background="https://worldfactbook.blob.core.windows.net/site/old-world-map.jpg">

    <div id="preamble" class="preamble">
        <table>
            <tr>
                <td><img class="logo" src="https://worldfactbook.blob.core.windows.net/site/LogoAzureFactbook.png" /></td>
                <td>    </td>
                <td>
                    <h1>World Country Data</h1>
                    <div class="subtitle">Powered by Azure Functions & Cosmos DB<br />Data courtesy of <a href="https://www.cia.gov/library/publications/the-world-factbook/" target="_blank">CIA WorldFactbook</a></div>
                </td>
            </tr>
        </table>
    </div>

    <div style="display: block">
        <div class="searchbox" style="vertical-align: middle">
            <input id="search-text" type="text" autocapitalize="none" placeholder="search text" /><button id="btn-search" style="vertical-align: middle" onclick="search();"><i class="fa fa-search" style="height: 32px;"></i></button>
        </div>

        <div class="selector">
            <select id="country" onchange="countrySelectionChanged()">
                <option value="">---- Select a Country ----</option>
                <option value="Afghanistan">Afghanistan</option>
                <option value="Akrotiri">Akrotiri</option>
                <option value="Albania">Albania</option>
                <option value="Algeria">Algeria</option>
                <option value="American Samoa">American Samoa</option>
                <option value="Andorra">Andorra</option>
                <option value="Angola">Angola</option>
                <option value="Anguilla">Anguilla</option>
                <option value="Antarctica">Antarctica</option>
                <option value="Antigua And Barbuda">Antigua And Barbuda</option>
                <option value="Arctic Ocean">Arctic Ocean</option>
                <option value="Argentina">Argentina</option>
                <option value="Armenia">Armenia</option>
                <option value="Aruba">Aruba</option>
                <option value="Ashmore And Cartier Islands">Ashmore And Cartier Islands</option>
                <option value="Atlantic Ocean">Atlantic Ocean</option>
                <option value="Australia">Australia</option>
                <option value="Austria">Austria</option>
                <option value="Azerbaijan">Azerbaijan</option>
                <option value="Bahamas, The">Bahamas, The</option>
                <option value="Bahrain">Bahrain</option>
                <option value="Bangladesh">Bangladesh</option>
                <option value="Barbados">Barbados</option>
                <option value="Belarus">Belarus</option>
                <option value="Belgium">Belgium</option>
                <option value="Belize">Belize</option>
                <option value="Benin">Benin</option>
                <option value="Bermuda">Bermuda</option>
                <option value="Bhutan">Bhutan</option>
                <option value="Bolivia">Bolivia</option>
                <option value="Bosnia And Herzegovina">Bosnia And Herzegovina</option>
                <option value="Botswana">Botswana</option>
                <option value="Bouvet Island">Bouvet Island</option>
                <option value="Brazil">Brazil</option>
                <option value="British Indian Ocean Territory">British Indian Ocean Territory</option>
                <option value="British Virgin Islands">British Virgin Islands</option>
                <option value="Brunei">Brunei</option>
                <option value="Bulgaria">Bulgaria</option>
                <option value="Burkina Faso">Burkina Faso</option>
                <option value="Burma">Burma</option>
                <option value="Burundi">Burundi</option>
                <option value="Cabo Verde">Cabo Verde</option>
                <option value="Cambodia">Cambodia</option>
                <option value="Cameroon">Cameroon</option>
                <option value="Canada">Canada</option>
                <option value="Cayman Islands">Cayman Islands</option>
                <option value="Central African Republic">Central African Republic</option>
                <option value="Chad">Chad</option>
                <option value="Chile">Chile</option>
                <option value="China">China</option>
                <option value="Christmas Island">Christmas Island</option>
                <option value="Clipperton Island">Clipperton Island</option>
                <option value="Cocos (Keeling) Islands">Cocos (Keeling) Islands</option>
                <option value="Colombia">Colombia</option>
                <option value="Comoros">Comoros</option>
                <option value="Congo, Democratic Republic Of The">Congo, Democratic Republic Of The</option>
                <option value="Congo, Republic Of The">Congo, Republic Of The</option>
                <option value="Cook Islands">Cook Islands</option>
                <option value="Coral Sea Islands">Coral Sea Islands</option>
                <option value="Costa Rica">Costa Rica</option>
                <option value="Cote D'Ivoire">Cote D'Ivoire</option>
                <option value="Croatia">Croatia</option>
                <option value="Cuba">Cuba</option>
                <option value="Curacao">Curacao</option>
                <option value="Cyprus">Cyprus</option>
                <option value="Czechia">Czechia</option>
                <option value="Denmark">Denmark</option>
                <option value="Dhekelia">Dhekelia</option>
                <option value="Djibouti">Djibouti</option>
                <option value="Dominica">Dominica</option>
                <option value="Dominican Republic">Dominican Republic</option>
                <option value="Ecuador">Ecuador</option>
                <option value="Egypt">Egypt</option>
                <option value="El Salvador">El Salvador</option>
                <option value="Equatorial Guinea">Equatorial Guinea</option>
                <option value="Eritrea">Eritrea</option>
                <option value="Estonia">Estonia</option>
                <option value="Eswatini">Eswatini</option>
                <option value="Ethiopia">Ethiopia</option>
                <option value="European Union">European Union</option>
                <option value="Falkland Islands (Islas Malvinas)">Falkland Islands (Islas Malvinas)</option>
                <option value="Faroe Islands">Faroe Islands</option>
                <option value="Fiji">Fiji</option>
                <option value="Finland">Finland</option>
                <option value="France">France</option>
                <option value="French Polynesia">French Polynesia</option>
                <option value="Gabon">Gabon</option>
                <option value="Gambia, The">Gambia, The</option>
                <option value="Gaza Strip">Gaza Strip</option>
                <option value="Georgia">Georgia</option>
                <option value="Germany">Germany</option>
                <option value="Ghana">Ghana</option>
                <option value="Gibraltar">Gibraltar</option>
                <option value="Greece">Greece</option>
                <option value="Greenland">Greenland</option>
                <option value="Grenada">Grenada</option>
                <option value="Guam">Guam</option>
                <option value="Guatemala">Guatemala</option>
                <option value="Guernsey">Guernsey</option>
                <option value="Guinea">Guinea</option>
                <option value="Guinea-Bissau">Guinea-Bissau</option>
                <option value="Guyana">Guyana</option>
                <option value="Haiti">Haiti</option>
                <option value="Heard Island And Mcdonald Islands">Heard Island And Mcdonald Islands</option>
                <option value="Holy See (Vatican City)">Holy See (Vatican City)</option>
                <option value="Honduras">Honduras</option>
                <option value="Hong Kong">Hong Kong</option>
                <option value="Hungary">Hungary</option>
                <option value="Iceland">Iceland</option>
                <option value="India">India</option>
                <option value="Indian Ocean">Indian Ocean</option>
                <option value="Indonesia">Indonesia</option>
                <option value="Iran">Iran</option>
                <option value="Iraq">Iraq</option>
                <option value="Ireland">Ireland</option>
                <option value="Isle Of Man">Isle Of Man</option>
                <option value="Israel">Israel</option>
                <option value="Italy">Italy</option>
                <option value="Jamaica">Jamaica</option>
                <option value="Jan Mayen">Jan Mayen</option>
                <option value="Japan">Japan</option>
                <option value="Jersey">Jersey</option>
                <option value="Jordan">Jordan</option>
                <option value="Kazakhstan">Kazakhstan</option>
                <option value="Kenya">Kenya</option>
                <option value="Kiribati">Kiribati</option>
                <option value="Korea, North">Korea, North</option>
                <option value="Korea, South">Korea, South</option>
                <option value="Kosovo">Kosovo</option>
                <option value="Kuwait">Kuwait</option>
                <option value="Kyrgyzstan">Kyrgyzstan</option>
                <option value="Laos">Laos</option>
                <option value="Latvia">Latvia</option>
                <option value="Lebanon">Lebanon</option>
                <option value="Lesotho">Lesotho</option>
                <option value="Liberia">Liberia</option>
                <option value="Libya">Libya</option>
                <option value="Liechtenstein">Liechtenstein</option>
                <option value="Lithuania">Lithuania</option>
                <option value="Luxembourg">Luxembourg</option>
                <option value="Macau">Macau</option>
                <option value="Macedonia">Macedonia</option>
                <option value="Madagascar">Madagascar</option>
                <option value="Malawi">Malawi</option>
                <option value="Malaysia">Malaysia</option>
                <option value="Maldives">Maldives</option>
                <option value="Mali">Mali</option>
                <option value="Malta">Malta</option>
                <option value="Marshall Islands">Marshall Islands</option>
                <option value="Mauritania">Mauritania</option>
                <option value="Mauritius">Mauritius</option>
                <option value="Mexico">Mexico</option>
                <option value="Micronesia, Federated States Of">Micronesia, Federated States Of</option>
                <option value="Moldova">Moldova</option>
                <option value="Monaco">Monaco</option>
                <option value="Mongolia">Mongolia</option>
                <option value="Montenegro">Montenegro</option>
                <option value="Montserrat">Montserrat</option>
                <option value="Morocco">Morocco</option>
                <option value="Mozambique">Mozambique</option>
                <option value="Namibia">Namibia</option>
                <option value="Nauru">Nauru</option>
                <option value="Navassa Island">Navassa Island</option>
                <option value="Nepal">Nepal</option>
                <option value="Netherlands">Netherlands</option>
                <option value="New Caledonia">New Caledonia</option>
                <option value="New Zealand">New Zealand</option>
                <option value="Nicaragua">Nicaragua</option>
                <option value="Niger">Niger</option>
                <option value="Nigeria">Nigeria</option>
                <option value="Niue">Niue</option>
                <option value="Norfolk Island">Norfolk Island</option>
                <option value="Northern Mariana Islands">Northern Mariana Islands</option>
                <option value="Norway">Norway</option>
                <option value="Oman">Oman</option>
                <option value="Pacific Ocean">Pacific Ocean</option>
                <option value="Pakistan">Pakistan</option>
                <option value="Palau">Palau</option>
                <option value="Palmyra Atoll">Palmyra Atoll</option>
                <option value="Panama">Panama</option>
                <option value="Papua New Guinea">Papua New Guinea</option>
                <option value="Paracel Islands">Paracel Islands</option>
                <option value="Paraguay">Paraguay</option>
                <option value="Peru">Peru</option>
                <option value="Philippines">Philippines</option>
                <option value="Pitcairn Islands">Pitcairn Islands</option>
                <option value="Poland">Poland</option>
                <option value="Portugal">Portugal</option>
                <option value="Puerto Rico">Puerto Rico</option>
                <option value="Qatar">Qatar</option>
                <option value="Romania">Romania</option>
                <option value="Russia">Russia</option>
                <option value="Rwanda">Rwanda</option>
                <option value="Saint Barthelemy">Saint Barthelemy</option>
                <option value="Saint Helena, Ascension, And Tristan Da Cunha">Saint Helena, Ascension, And Tristan Da Cunha</option>
                <option value="Saint Kitts And Nevis">Saint Kitts And Nevis</option>
                <option value="Saint Lucia">Saint Lucia</option>
                <option value="Saint Martin">Saint Martin</option>
                <option value="Saint Pierre And Miquelon">Saint Pierre And Miquelon</option>
                <option value="Saint Vincent And The Grenadines">Saint Vincent And The Grenadines</option>
                <option value="Samoa">Samoa</option>
                <option value="San Marino">San Marino</option>
                <option value="Sao Tome And Principe">Sao Tome And Principe</option>
                <option value="Saudi Arabia">Saudi Arabia</option>
                <option value="Senegal">Senegal</option>
                <option value="Serbia">Serbia</option>
                <option value="Seychelles">Seychelles</option>
                <option value="Sierra Leone">Sierra Leone</option>
                <option value="Singapore">Singapore</option>
                <option value="Sint Maarten">Sint Maarten</option>
                <option value="Slovakia">Slovakia</option>
                <option value="Slovenia">Slovenia</option>
                <option value="Solomon Islands">Solomon Islands</option>
                <option value="Somalia">Somalia</option>
                <option value="South Africa">South Africa</option>
                <option value="South Georgia And South Sandwich Islands">South Georgia And South Sandwich Islands</option>
                <option value="South Sudan">South Sudan</option>
                <option value="Southern Ocean">Southern Ocean</option>
                <option value="Spain">Spain</option>
                <option value="Spratly Islands">Spratly Islands</option>
                <option value="Sri Lanka">Sri Lanka</option>
                <option value="Sudan">Sudan</option>
                <option value="Suriname">Suriname</option>
                <option value="Svalbard">Svalbard</option>
                <option value="Sweden">Sweden</option>
                <option value="Switzerland">Switzerland</option>
                <option value="Syria">Syria</option>
                <option value="Taiwan">Taiwan</option>
                <option value="Tajikistan">Tajikistan</option>
                <option value="Tanzania">Tanzania</option>
                <option value="Thailand">Thailand</option>
                <option value="Timor-Leste">Timor-Leste</option>
                <option value="Togo">Togo</option>
                <option value="Tokelau">Tokelau</option>
                <option value="Tonga">Tonga</option>
                <option value="Trinidad And Tobago">Trinidad And Tobago</option>
                <option value="Tunisia">Tunisia</option>
                <option value="Turkey">Turkey</option>
                <option value="Turkmenistan">Turkmenistan</option>
                <option value="Turks And Caicos Islands">Turks And Caicos Islands</option>
                <option value="Tuvalu">Tuvalu</option>
                <option value="Uganda">Uganda</option>
                <option value="Ukraine">Ukraine</option>
                <option value="United Arab Emirates">United Arab Emirates</option>
                <option value="United Kingdom">United Kingdom</option>
                <option value="United States">United States</option>
                <option value="Uruguay">Uruguay</option>
                <option value="Uzbekistan">Uzbekistan</option>
                <option value="Vanuatu">Vanuatu</option>
                <option value="Venezuela">Venezuela</option>
                <option value="Vietnam">Vietnam</option>
                <option value="Virgin Islands">Virgin Islands</option>
                <option value="Wake Island">Wake Island</option>
                <option value="Wallis And Futuna">Wallis And Futuna</option>
                <option value="West Bank">West Bank</option>
                <option value="Western Sahara">Western Sahara</option>
                <option value="World">World</option>
                <option value="Yemen">Yemen</option>
                <option value="Zambia">Zambia</option>
                <option value="Zimbabwe">Zimbabwe</option>
            </select>
        </div>
    </div>

    <div id="country-view" >

        <h2 class="optional"><img id="country-flag" class="content-image-thumbnail" style="visibility: collapse" src="" /> <span id="country-name"></span></h2>

        <div class="accordion">
            <div id="content-header-introduction" class="accordion-header">Introduction</div>
            <div id="content-introduction" class="accordion-content"></div>

            <div class="accordion-header">Geography</div>
            <div id="content-geography" class="accordion-content"></div>

            <div class="accordion-header">People</div>
            <div id="content-people" class="accordion-content"></div>

            <div class="accordion-header">Government</div>
            <div id="content-government" class="accordion-content"></div>

            <div class="accordion-header">Economy</div>
            <div id="content-economy" class="accordion-content"></div>

            <div class="accordion-header">Energy</div>
            <div id="content-energy" class="accordion-content"></div>

            <div class="accordion-header">Communications</div>
            <div id="content-communications" class="accordion-content"></div>

            <div class="accordion-header">Transportation</div>
            <div id="content-transportation" class="accordion-content"></div>

            <div class="accordion-header">Military and Security</div>
            <div id="content-military" class="accordion-content"></div>

            <div class="accordion-header">Transnational Issues</div>
            <div id="content-transnational" class="accordion-content"></div>
        </div>

    </div>

    <div id="results-view" style="visibility: collapse">
        <h2>Search Results</h2>
        <div id="results-list" style="color: white; font-size: 14px"></div>
    </div>

    <script>
        var inCountryView = false;
        var data = null;

        $(document).ready(function () {
            $("#search-text").on("keyup", function (e) {
                if (e.keyCode == 13) {
                    search();
                }
            });

            $(".accordion").on("click", ".accordion-header", function () {
                $(this).toggleClass("active").next().slideToggle();
            });
        });

        // Country selection changed - load and display country data

        function countrySelectionChanged() {
            $("body").css("cursor", "progress");

            setCountryView();

            var countryName = $('#country').val();

            if (countryName === '') {
                setAllContent('No content');
                $('#preamble').removeClass('optional');
                $('h2').addClass('optional');
                $('#country-flag').css('visibility', 'collapse');
                $('#country-name').text('');
                $('#country-flag').attr('src', null);
                data = null;
                $("body").css("cursor", "default");
            }
            else {
                $('h2').removeClass('optional');
                var countryKey = CountryKey(countryName);

                setAllContent('Loading...');

                if (haveFlag(countryName)) {
                    $('#country-flag').attr('src', 'https://worldfactbook.blob.core.windows.net/data/' + countryKey + '.gif');
                    $('#country-flag').css('visibility', 'visible');
                }
                else {
                    $('#country-flag').css('visibility', 'collapse');
                    $('#country-flag').attr('src', null);
                }
                $('#country-name').text(countryName);

                // Retrieve JSON country record

                var url = 'https://factbook.azure-api.net/world-factbook/country/' + countryKey;

                $.ajax({
                    type: 'GET',
                    url: url,
                    accepts: "json",
                }).done(function (result) {
                    data = $.parseJSON(result);

                    if (data) {

                        // If nothing is open, open first accordion (Introduction)

                        if ($('.active').length === 0) {
                            $('#content-header-introduction').toggleClass("active").next().slideToggle();
                        }

                        // Load content: Introduction

                        var intro = '';
                        if (haveFlag(countryName)) {
                            intro = intro + '<div class="item"><b>Flag</b><br/><img class="content-image" src="https://worldfactbook.blob.core.windows.net/data/' + countryKey + '.gif"></div>';
                        }
                        if (data.introduction && data.introduction.background) {
                            intro += '<div class="item"><b>Background</b><br/>' + data.introduction.background + '</div>';
                        }
                        $('#content-introduction').html(intro);

                        // Load content: Geography

                        var geo = '';
                        geo = geo + '<div class="item"><b>Map</b><br/><a href="https://worldfactbook.blob.core.windows.net/data/' + countryKey + '-map.gif" target="_blank"><img class="content-image-large" src="https://worldfactbook.blob.core.windows.net/data/' + countryKey + '-map.gif"></a></div>';
                        if (data.geography) {
                            var geography = data.geography;
                            if (geography.location) {
                                geo += '<div class="item"><b>Location</b><br/>' + geography.location + '</div>';
                            }

                            if (geography.geographic_coordinates && geography.geographic_coordinates.latitude) {
                                var lat = geography.geographic_coordinates.latitude.degrees + '° ' + geography.geographic_coordinates.latitude.hemisphere;
                                var long = geography.geographic_coordinates.longitude.degrees + '° ' + geography.geographic_coordinates.longitude.hemisphere;
                                geo += '<div class="item"><b>Coordinates</b><br/>' + lat + ', ' + long + '</div>';
                            }
                        }
                        $('#content-geography').html(geo);

                        // Load content: People

                        var people = '';
                        if (data.people) {
                            if (data.people.population && data.people.population.total) {
                                people += '<div class="item"><b>Population</b><br/>' + numberWithCommas(data.people.population.total) + '</div>';
                            }
                            if (data.people.populatione && data.people.population.rank) {
                                people += '<div class="item"><b>Global Rank</b><br/>' + data.people.population.global_rank + '</div>';
                            }
                            if (data.people.nationality && data.people.nationality.adjective) {
                                people += '<div class="item"><b>Nationality</b><br/>' + data.people.nationality.adjective + '</div>';
                            }
                            if (data.people.ethnic_groups && data.people.ethnic_groups.ethnicity) {
                                var ethnic_groups = data.people.ethnic_groups;
                                people += '<div class="item"><b>Ethnic Groups</b><br/>'
                                for (var i = 0; i < ethnic_groups.ethnicity.length; i++) {
                                    var pct = ethnic_groups.ethnicity[i].percent;
                                    var elem = ethnic_groups.ethnicity[i].name;
                                    var note = ethnic_groups.ethnicity[i].note;
                                    if (pct)
                                        elem += " (" + pct + '%)';
                                    if (note)
                                        elem += " note: " + note;
                                    if (i == 0)
                                        people += elem;
                                    else
                                        people += ", " + elem;
                                }
                                people += '</div>';
                            }
                            if (data.people.languages && data.people.languages.language) {
                                var languages = data.people.languages;
                                people += '<div class="item"><b>Languages</b><br/>'
                                for (var i = 0; i < languages.language.length; i++) {
                                    var pct = languages.language[i].percent;
                                    var elem = languages.language[i].name;
                                    if (pct)
                                        elem += " (" + pct + '%)';
                                    if (i == 0)
                                        people += elem;
                                    else
                                        people += ", " + elem;
                                }
                                people += '</div>';
                            }
                            if (data.people.religions && data.people.religions.religion) {
                                var religions = data.people.religions;
                                people += '<div class="item"><b>Religions</b><br/>'
                                for (var i = 0; i < religions.religion.length; i++) {
                                    var pct = religions.religion[i].percent;
                                    var elem = religions.religion[i].name;
                                    if (pct)
                                        elem += " (" + pct + '%)';
                                    if (i == 0)
                                        people += elem;
                                    else
                                        people += ", " + elem;
                                }
                                people += '</div>';
                            }
                            if (data.people.life_expectancy_at_birth && data.people.life_expectancy_at_birth.total_population && data.people.life_expectancy_at_birth.total_population.value && data.people.life_expectancy_at_birth.total_population.units) {
                                people += '<div class="item"><b>Life Expectancy at Birth</b><br/>' + data.people.life_expectancy_at_birth.total_population.value + ' ' + data.people.life_expectancy_at_birth.total_population.units + '</div>';
                            }
                        }
                        $('#content-people').html(people);

                        // Load content: Government

                        var govt = '';
                        if (data.government) {
                            if (data.government.government_type) {
                                govt += '<div class="item"><b>Type</b><br/>' + data.government.government_type + '</div>';
                            }
                            if (data.government.capital && data.government.capital.name) {
                                govt += '<div class="item"><b>Capital</b><br/>' + data.government.capital.name + '</div>';
                            }
                            if (data.government.legal_system) {
                                govt += '<div class="item"><b>Legal System</b><br/>' + data.government.legal_system + '</div>';
                            }
                            if (data.government.national_holidays) {
                                govt += '<div class="item"><b>National Holidays</b><br/>'
                                for (var i = 0; i < data.government.national_holidays.length; i++) {
                                    var elem = data.government.national_holidays[i].name + ' (' + data.government.national_holidays[i].day + ')';
                                    if (i == 0)
                                        govt += elem;
                                    else
                                        govt += ", " + elem;
                                }
                                govt += '</div>';
                            }
                        }
                        $('#content-government').html(govt);

                        // Load content: Economy

                        var econ = '';
                        if (data.economy) {
                            var economy = data.economy;
                            if (economy.gdp && economy.gdp.official_exchange_rate && economy.gdp.official_exchange_rate.date && economy.gdp.official_exchange_rate.USD) {
                                econ += '<div class="item"><b>Gross Domestic Product</b><br/>' + economy.gdp.official_exchange_rate.date + ': ' + numberWithCommas(economy.gdp.official_exchange_rate.USD) + ' USD</div>';
                            }
                            if (economy.agriculture_products && economy.agriculture_products.products) {
                                econ += '<div class="item"><b>Agricultural Products</b><br/>'
                                for (var i = 0; i < economy.agriculture_products.products.length; i++) {
                                    var elem = economy.agriculture_products.products[i];
                                    if (i == 0)
                                        econ += elem;
                                    else
                                        econ += ", " + elem;
                                }
                                econ += '</div>';
                            }
                            if (economy.industries && economy.industries.industries) {
                                econ += '<div class="item"><b>Industries</b><br/>'
                                for (var i = 0; i < economy.industries.industries.length; i++) {
                                    var elem = economy.industries.industries[i];
                                    if (i == 0)
                                        econ += elem;
                                    else
                                        econ += ", " + elem;
                                }
                                econ += '</div>';
                            }
                            if (economy.overview) {
                                econ += '<div class="item"><b>Overview</b><br/>' + economy.overview + '</div>';
                            }
                        }
                        $('#content-economy').html(econ);

                        // Load content: Energy

                        var energy = '';
                        if (data.energy) {
                            if (data.energy.electricity && data.energy.electricity.access && data.energy.electricity.access.total_electrification) {
                                energy += '<div class="item"><b>Total Electrification</b><br/>' + data.energy.electricity.access.total_electrification.value + '%</div>';
                            }
                            if (data.energy.crude_oil && data.energy.crude_oil.production && data.energy.crude_oil.production.bbl_per_day) {
                                energy += '<div class="item"><b>Crude Oil Production</b><br/>' + numberWithCommas(parseInt(data.energy.crude_oil.production.bbl_per_day)) + ' bbl/day</div>';
                            }
                            if (data.energy.refined_petroleum_products && data.energy.refined_petroleum_products.production && data.energy.refined_petroleum_products.production.bbl_per_day) {
                                energy += '<div class="item"><b>Refined Petroleum Production</b><br/>' + numberWithCommas(parseInt(data.energy.refined_petroleum_products.production.bbl_per_day)) + ' bbl/day</div>';
                            }
                            if (data.energy.natural_gas && data.energy.natural_gas.production.cubic_metres) {
                                energy += '<div class="item"><b>Natural Gas Production</b><br/>' + numberWithCommas(parseInt(data.energy.natural_gas.production.cubic_metres)) + ' cubic metres</div>';
                            }
                        }
                        $('#content-energy').html(energy);

                        // Load content: Communications

                        var comm = '';
                        if (data.communications) {
                            if (data.communications.telephones) {
                                var telephones = data.communications.telephones;
                                if (telephones && telephones.fixed_lines && telephones.fixed_lines.total_subscriptions) {
                                    comm += '<div class="item"><b>Telephone Fixed-line Subcriptions</b><br/>' + numberWithCommas(telephones.fixed_lines.total_subscriptions) + '</div>';
                                }
                                if (telephones.mobile_cellular && telephones.mobile_cellular.total_subscriptions) {
                                    comm += '<div class="item"><b>Mobile-Cellular Subcriptions</b><br/>' + numberWithCommas(telephones.mobile_cellular.total_subscriptions) + '</div>';
                                }
                            }
                            if (data.communications.broadcast_media) {
                                comm += '<div class="item"><b>Broadcast Media</b><br/>' + data.communications.broadcast_media + '</div>';
                            }
                            if (data.communications.internet) {
                                var internet = data.communications.internet;
                                if (internet.country_code) {
                                    comm += '<div class="item"><b>Internet Country Code</b><br/>' + internet.country_code + '</div>';
                                }
                                if (internet.users && internet.users.total && internet.users.percent_of_population) {
                                    comm += '<div class="item"><b>Internet Users</b><br/>' + numberWithCommas(internet.users.total) + ' (' + internet.users.percent_of_population + '%)</div>';
                                }
                            }
                        }
                        $('#content-communications').html(comm);

                        // Load content: Transportation

                        var trans = '';
                        if (data.transportation) {
                            if (data.transportation.air_transport && data.transportation.air_transport.national_system && data.transportation.air_transport.national_system.number_of_registered_air_carriers) {
                                trans += '<div class="item"><b>Registered Air Carriers</b><br/>' + numberWithCommas(data.transportation.air_transport.national_system.number_of_registered_air_carriers) + '</div>';
                            }
                            if (data.transportation.air_transport && data.transportation.air_transport.national_system && data.transportation.air_transport.national_system.inventory_of_registered_aircraft_operated_by_air_carriers) {
                                trans += '<div class="item"><b>Registered Air Carrier Aircraft</b><br/>' + numberWithCommas(data.transportation.air_transport.national_system.inventory_of_registered_aircraft_operated_by_air_carriers) + '</div>';
                            }
                            if (data.transportation.air_transport && data.transportation.air_transport.airports && data.transportation.air_transport.airports.total && data.transportation.air_transport.airports.total.airports) {
                                trans += '<div class="item"><b>Airports</b><br/>' + numberWithCommas(data.transportation.air_transport.airports.total.airports) + '</div>';
                            }
                            if (data.transportation.railways && data.transportation.railways.total && data.transportation.railways.total.length && data.transportation.railways.total.units) {
                                trans += '<div class="item"><b>Railways</b><br/>' + numberWithCommas(data.transportation.railways.total.length) + ' ' + data.transportation.railways.total.units + '</div>';
                            }
                            if (data.transportation.roadways && data.transportation.roadways.total && data.transportation.roadways.total.value && data.transportation.roadways.total.units) {
                                trans += '<div class="item"><b>Roadways</b><br/>' + numberWithCommas(data.transportation.roadways.total.value) + ' ' + data.transportation.roadways.total.units + '</div>';
                            }
                        }
                        $('#content-transportation').html(trans);

                        // Load content: Military and Security

                        var mil = '';
                        if (data.military_and_security) {
                            if (data.military_and_security.branches && data.military_and_security.branches.by_name) {
                                mil += '<div class="item"><b>Military Branches</b><br/>'
                                for (var i = 0; i < data.military_and_security.branches.by_name.length; i++) {
                                    var elem = data.military_and_security.branches.by_name[i];
                                    if (i == 0)
                                        mil += elem;
                                    else
                                        mil += "; " + elem;
                                }
                                mil += '</div>';
                            }
                            if (data.military_and_security.expenditures && data.military_and_security.expenditures.annual_values) {
                                var expenditures = data.military_and_security.expenditures;
                                mil += '<div class="item"><b>Annual Expenditures</b><br/>'
                                for (var i = 0; i < expenditures.annual_values.length; i++) {
                                    var value = numberWithCommas(expenditures.annual_values[i].value);
                                    var unit = expenditures.annual_values[i].units;
                                    if (unit === 'percent_of_gdp') {
                                        unit = '% GDP';
                                    }
                                    var elem = expenditures.annual_values[i].date + ': ' + value + ' ' + unit;
                                    if (i == 0)
                                        mil += elem;
                                    else
                                        mil += "<br/>" + elem;
                                }
                                mil += '</div>';
                            }
                        }
                        $('#content-military').html(mil);

                        // Load content: Transnational Issues

                        var transnat = '';
                        if (data.transnational_issues) {
                            if (data.transnational_issues.disputes) {
                                transnat += '<div class="item"><b>Disputes</b><br/>' + data.transnational_issues.disputes + '</div>';
                            }
                            if (data.transnational_issues.refugees_and_iternally_displaced_persons && data.transnational_issues.refugees_and_iternally_displaced_persons.refugees && data.transnational_issues.refugees_and_iternally_displaced_persons.refugees.by_country) {
                                transnat += '<div class="item"><b>Refugees by Country</b><br/>'
                                for (var i = 0; i < data.transnational_issues.refugees_and_iternally_displaced_persons.refugees.by_country.length; i++) {
                                    var elem = data.transnational_issues.refugees_and_iternally_displaced_persons.refugees.by_country[i].country_of_origin + ' : ' + numberWithCommas(data.transnational_issues.refugees_and_iternally_displaced_persons.refugees.by_country[i].people);
                                    transnat += elem + '<br/>';
                                }
                                if (data.transnational_issues.refugees_and_iternally_displaced_persons.refugees.by_country.length > 0) transnat += '<br/>';
                            }
                            if (data.transnational_issues.illicit_drugs && data.transnational_issues.illicit_drugs.note) {
                                transnat += '<div class="item"><b>Illicit Drugs</b><br/>' + data.transnational_issues.illicit_drugs.note + '</div>';
                            }
                        }
                        $('#content-transnational').html(transnat);

                        $("body").css("cursor", "default");
                    }
                    else {
                        setAllContent('No content');
                    }
                });
            }
        }

        // Perform a search.

        function search() {

            var term = $('#search-text').val();
            if (!term) return;

            $("body").css("cursor", "progress");

            $('#country').val('');

            $('h2').removeClass('optional');

            $('#country-view').css('visibility', 'collapse');
            inCountryView = false;

            //console.log('search 01 term:[' + term + '], url:[' + url + ']');

            var url = 'https://factbook.azure-api.net/world-factbook/search/' + term;

            $.ajax({
                type: 'GET',
                url: url,
                accepts: "json",
            }).done(function (response) {

                var results = $.parseJSON(response);

                var html = '<table id="results-table" style="color: white; font-size: 20px">';
                var count = 0;
                var countryKey = null;
                if (results) {
                    for (var i = 0; i < results.length; i++) {
                        countryKey = CountryKey(results[i].name);
                        html += '<tr style="cursor: pointer; height: 24px; border-bottom: solid 1px white" onclick="selectCountry(' + "'" + results[i].name + "'" + ');">';
                        if (haveFlag(results[i].name)) {
                            html += '<td style="text-align: right"><img class="content-image-thumbnail" src="https://worldfactbook.blob.core.windows.net/data/' + countryKey + '.gif"></td>';
                        }
                        else {
                            html += '<td> </td>';
                        }
                        html += '<td>  </td><td style="vertical-align: middle">' + results[i].name + '</td></tr>';
                        count++;
                    }
                }
                if (count == 0) {
                    html += '<tr><td>No matches</td></tr>';
                }
                html += '</table>';

                $('#results-list').html(html);
                $('#country-flag').css('visibility', 'collapse');
                $('#results-view').css('visibility', 'visible');

                $("body").css("cursor", "default");
            });
        }

        // Return to country view and auto-select a country

        function selectCountry(name) {
            console.log('selectCountry:' + name);
            $('#country').val(name).trigger('change');
            inCountryView = true;
        }

        function setCountryView() {
            if (!inCountryView) {
                $('#results-view').css('visibility', 'collapse');
                $('#country-view').css('visibility', 'visible');
                inCountryView = true;
            }
        }

        function setAllContent(content) {
            $('#country-flag').attr('src', content);
            $('#content-introduction').html(content);
            $('#content-geography').html(content);
            $('#content-people').html(content);
            $('#content-government').html(content);
            $('#content-economy').html(content);
            $('#content-energy').html(content);
            $('#content-communications').html(content);
            $('#content-transportation').html(content);
            $('#content-military').html(content);
            $('#content-transnational').html(content);
        }

        function numberWithCommas(x) {
            return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
        }

        function replace(value, oldChar, newChar) {
            if (!value) return null;
            return value.split(oldChar).join(newChar);
        }

        function CountryKey(countryName) {
            var countryKey = countryName.toLowerCase();
            countryKey = replace(countryKey, ' ', '_');
            countryKey = replace(countryKey, '-', '_');
            countryKey = replace(countryKey, '(', '');
            countryKey = replace(countryKey, ')', '');
            countryKey = replace(countryKey, ',', '');
            countryKey = replace(countryKey, "'", '');
            return countryKey;
        }

        function haveFlag(name) {
            switch (name) {
                case 'World':
                case 'Antarctica':
                case 'Arctic Ocean':
                case 'Atlantic Ocean':
                case 'Indian Ocean':
                case 'Pacific Ocean':
                case 'Southern Ocean':
                    return false;
                default:
                    return true;
            }
        }
    </script>
</body>
</html>
index.html

What's the final result like? Try it for yourself! I'll leave all this up and running for as long as it's not overly-expensive. A few performance notes:
  • You will wait a bit the very first time you access the site as it downloads the large old world map background image; but after that it wil be cached. 
  • Also, if you call up a country or peform a search and the Azure Functions aren't already warmed up, you may wait a few extra seconds. 
Aside from those [expected] areas, I've found the site to be pretty zippy. Even though Cosmos DB is set to the slowest (cheapest) throughput rate, it's quite fast.

In Conclusion

In this series I showed how to retrieve world country data from CIA World Factbook and store it in Azure, along with an API and web site for accessing the data. Cosmos DB was our primary repository along with blob storage, and it's performed well. Azure Functions were integral to both the back end (C# Durable Functions in Part 1) and front end (JavaScript Functions in Part 2). Cosmos DB Bindings made accessing the database from JavaScript a breeze.

I gave myself one week for this project and that time is now up. I used a number of cloud services and features for the first time during this project, but despite the learning curve progress was nevertheless good. This is a testimony to the ease of use provided by Azure Functions, Cosmos DB, API Management and other cloud services. Serverless really is amazing for developer productivity, ont to mention the fun and satisfaction of going from concept to reality so quickly.

Where do we go from here? There's more we could do on the web site:
  • We're currently only showing some of the World Factbook country data
  • Adding reports or charts would be useful to show things like rankings or country comparisons

So, perhaps we'll do a Part 3 at some point. Stay tuned!