Interactive trek profiles: From Google Earth to the web using elevation APIs and Chart.js

New profiles for our trekking routes are now on the website. The heights of the trekking routes in Tajikistan are one of the most frequently asked questions. Altitudes and elevation differences are the most important factor determining the difficulty of a trek. For that reason it is important to give clear insight into the altitudes that can be expected. Here an example of how to achieve this with Google elevation data, ChartJS and some basic Python scripting.

Two hikers walking on a trail towards the Kulikalon plateau

Google Earth is a useful tool to plan hiking routes and check out what elevation differences you can expect. Are there a lot of tough climbs and descents? How high is the mountain pass that we are planning to do? After drawing a custom path in Google Earth, you can generate a height profile and directly see the altitude details.

Before, we had been using these height profiles to create static images for display on the website. This might be good enough, but now let's see how we can create a similar interactive height profiles as on Google Earth. Luckily the process to achieve this is pretty simple.

For doing so, we will make us of the KML file to which we can export our Google Earth routes. When you open the KML file in a text editor, you can see that Google Earth only saves the longitude and latitude of the polylines. Elevation data are not saved – paths are simply clamped to the terrain in Google Earth.

We first need a way to add the elevation information to our longitude-latitude coordinate pairs. The Google Elevation API can for instance be used for this purpose. With a simple Python script, we can easily process the XY data from the KML file to XYZ data that we can use to create a height profile.

First, we need to import the Python libraries that we will be using:

import requests
import json
import math
from geopy import distance

The requests module is used to fire the request to the Google Elevation API. The json module is used to process the response in JSON format. The math and geopy modules are used to perform some spatial calculations to get distances between points.

We have saved the coordinates from the KML file in a separate text file (coordinates.txt). We read the file and parse the data into a list of coordinate pairs:

file = open("coordinates.txt", "r")
kml_input = file.read()
file.close()

kml_xy_pairs = kml_input.replace(",0", ",").replace(" ", "").split(",")[:-1]

With the Google Elevation API, you can only request elevation data for 250 coordinate pairs at the time. For this reason, we divide all the coordinate pairs in sets of max. 250 pairs:

total_xy_pairs = int(len(kml_xy_pairs)/2)
coordinate_sets = [""]
xy_pairs_in_request = 0
request_no = 0
for i in range(total_xy_pairs):
   coordinate_sets[request_no] += f"{kml_xy_pairs[i*2+1]},{kml_xy_pairs[i*2]}|"
   xy_pairs_in_request += 1
   if xy_pairs_in_request == 250 and i < total_xy_pairs-1:
      coordinate_sets.append("")
      request_no += 1
      xy_pairs_in_request = 0

For each set of coordinates, we then fire a request to the Google Elevation API to get the elevation of each coordinate pair and save the resulting XYZ coordinates in a new list. You can get an API key through the Google Cloud Console.

result = []
for coordinate_set in coordinate_sets:
   url = f"https://maps.googleapis.com/maps/api/elevation/json?locations={coordinate_set[:-1]}&key=[your_API_key]"

   try:
      response = requests.request("GET", url)
      data = json.loads(response.text)
      for xyz in data["results"]:
         result.append(xyz)
   except requests.exceptions.HTTPError as err:
      raise SystemExit(err)

For the height profile, we now only need to calculate the distance between the points so we know what to use on the x-axis. We can achieve this with the geopy module and some simple maths. We also check the distance between successive points. This allows us to exclude points that are too close together for the level of detail we need and reduce the size of the final output:

# Set start point
first = result[0]
all_data = [[round(first["location"]["lng"], 6), round(first["location"]["lat"], 6), round(first["elevation"], 2), 0]]

cumulative_distance = 0
dist_check = 0
for x in range(len(result)-1):
   previous = result[x]
   this = result[x+1]
   lat_prev = previous["location"]["lat"]
   lng_prev = previous["location"]["lng"]
   lat_this = this["location"]["lat"]
   lng_this = this["location"]["lng"]

   delta_distance_xy = distance.distance((lat_prev, lng_prev), (lat_this, lng_this)).m
   delta_elevation = abs(this["elevation"] - previous["elevation"])
   distance_from_previous = math.sqrt(delta_distance_xy**2 + delta_elevation**2)
   cumulative_distance += distance_from_previous

   dist_check += distance_from_previous
   # Do not include point if it is very close to last point, but always include the last point
   if dist_check > 50 or x == len(result)-2:
      this_data = [round(lng_this, 6), round(lat_this, 6), round(this["elevation"], 2), round(cumulative_distance, 2)]
      all_data.append(this_data)
      dist_check = 0

The result is a list with elevation information along the entire length of the trek. This is all we need to make a chart. There are various JavaScript libraries that can be used to visualize a chart in the browser. In this case, we will use ChartJS. With the data saved in a variable named "chart_data" and a canvas element in our HTML with id "trek-profile", we can use the following JavaScript code to generate a interactive profile with ChartJS:

const profileCanvas = document.getElementById("trek-profile").getContext("2d");	
Chart.defaults.borderColor = "#a89d87";

let trekProfile = new Chart(profileCanvas, {
  type: "scatter",
  data: {
    datasets: [
      {
        data: chart_data,
        borderColor: "rgba(150, 138, 114, 1)",
        borderWidth: 2,
        pointRadius: 0,
        fill: {
          target: "origin", 
          above: function(context){
            const chart = context.chart;
            const {ctx, chartArea} = chart;
            let gradient = profileCanvas.createLinearGradient(0, chartArea.bottom, 0, chartArea.top);
            gradient.addColorStop(0, 'rgba(150, 138, 114, 1)');
            gradient.addColorStop(1, 'rgba(232, 215, 181, 1)');
            return gradient;
          }
        },
        showLine: true,
        lineTension: 0.1
      }
    ]
  },
  
  options: {
    responsive: true,
    plugins: {
      legend: {
        display: false
      },
      tooltip: {
        mode: "nearest",
        intersect: false,
        displayColors: false,
        callbacks: {
          title: function(context) {
            return "Elevation"
          },
          label: function(context) {
            return context.parsed.y + " m";
          }
        }
      }
    },
    scales: {
      x: {
        border: {
          display: true
        },
        max: chart_data[chart_data.length-1].x,
        grid: {
          display: true,
          drawTicks: true,
          drawOnChartArea: false
        },
        ticks: {
          callback: function(value, index, ticks) {
            return value + ' km';
          }
        }
      },
      y: {
        border: {
          display: true
        },
        grid: {
          display: true,
          drawTicks: true,
          drawOnChartArea: true,
          lineWidth: 0.5
        },
        ticks: {
          callback: function(value, index, ticks) {
            return value + ' m';
          }
        }
      }
    }
  }
});


And there we have it. For our trek from Iskanderkul to Alauddin Lake for example, this results in the profile below. Precise elevations can be retrieved from any point along the route when you hover the mouse over the profile.



These profiles give more detailed height information than a static image (and we make our lives easier by automating the visualization of elevation differences of our treks). As a bonus, below is a transect through Tajikistan starting in Bukhara (Uzbekistan) in the west to the Tibetan Plateau in the east, crossing straight through the Fann Mountains and the Pamirs. This just goes to show how mountainous and rough Tajikistan is.