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

New profiles for our trekking routes are out now on the website. The heights of the trekking routes in Tajikistan are one of the main topics 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. This is easy to achieve with the Google Elevation API, 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 easily see these details. The profiles are interactive and let you check the precise altitude for any point along the path.

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 in which we can save our Google Earth paths. 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 is not saved; the paths are rather clamped to the terrain.

We first need a way to add the elevation information to our longitude-latitude coordinate pairs. The Google Elevation API can 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 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 separate TXT 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, we can only request elevation data for 250 coordinate pairs at the time. For this reason, we need to 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 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 all the points so we know what to use on the x-axis. We can achieve 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 data size:

# 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 the 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 vanilla 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 here we have it. For our trek from Iskanderkul to Alauddin Lake for example, this results in the profile below. Precise elevations are shown for each point along the route when you hover the mouse over the profile.



These profiles give more detailed height information and we make our lives easier by automating the process to visualize 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.