Interactive map of remote alpine spots that I visited in Switzerland

Author

Marco Notaro

Published

January 27, 2026

Aim

Here I built an interactive map showing remote alpine spots I visited in Switzerland.

To make geolocation more efficient, data are cached in a JSON file.

Markers are color-coded according to the Swiss Alpine Club grading system: red for hikes up to T3, blue for routes up to T6 and dark blue for glacier tours. Magenta and orange markers indicate alpine lakes and cities, respectively.

import pandas as pd

import os 
import json
from pathlib import Path

import requests
from geopy.geocoders import Nominatim
from geopy.extra.rate_limiter import RateLimiter

pd.set_option('display.max_rows', 20)
# pd.reset_option('display.max_rows')

Get spots

Alpine spots to geolocalize

## path level define marker color

swiss_alpine_spots = {
  ## Zermatt
  "Hörnlihütte": ["T3", "Trail Hike"], ## {"path": "T3", "type": "hike"}
  "Schönbielhütte": ["T3", "Trail Hike"],
  "Gandegghütte": ["T3", "Trail Hike"],
  "Berggasthaus Trift": ["T2", "Trail Hike"],
  "Rothornhütte": ["T3", "Trail Hike"],
  
  ## Engelberg
  "Brunnihütte": ["T2", "Trail Hike"],
  
  ## Lauterbrunnen
  "Lobhornhütte": ["T2", "Trail Hike"],

  ## Uri
  "Sidelenhütte": ["T2", "Trail Hike"],
  "Albert-Heim-Hütte": ["T4", "Trail Hike"],
  
  ## Bern
  "Trifthütte": ["T4", "Trail Hike"],
  "Tierberglihütte": ["T4", "Trail Hike"],
  "Blüemlisalphütte": ["T2", "Trail Hike"],
  "Gelmerhütte": ["T3", "Trail Hike"],
  "Lämmerenhütte": ["T2", "Trail Hike"],

  ## Glarus  
  "Planurahütte": ["WS", "Glacier Hike"],
  "Hüfihütte": ["WS", "Glacier Hike"],
  "Muttseehütte": ["T3", "Trail Hike"],
  "Leglerhütte": ["T2", "Trail Hike"],

  ## Glaciers
  "Hüfifirn": ["WS", "Glacier Crossing"],
  "Aletsch Glacier": ["WS", "Glacier Crossing"],
  "Rhône Glacier": ["WS", "Glacier Crossing"],
  "Zinal Glacier": ["WS", "Glacier Cave"],
  "Wildstrubelgletscher": ["T4", "Glacier Cave"],

  ## Lakes
  "Jöriseen": ["T3", "Lake"],
  "Oeschinensee": ["T2", "Lake"],
  "Caumasee": ["T2", "Lake"],
  "Silvaplana": ["-", "Lake"],

  ## main alpine cities
  "Zermatt": ["-", "City"],
  "Davos": ["-", "City"],
  "Saint Moritz": ["-", "City"],
  "Lauterbrunnen": ["-", "City"]
}

Get coordinates

Get elevation and coordinates of alpine spots caching results in a JSON file

# use Open-Elevation to get elevation, since Nominatim doesn't have an API for this

def get_elevation(lat, lon):
  """
  Get elevation from Open-Elevation API
  """
  
  url = f"https://api.open-elevation.com/api/v1/lookup?locations={lat},{lon}"
  
  try:
    response = requests.get(url, timeout=10)
    if response.ok: ## when response fails (404) doesn't raise any exception
      return response.json()["results"][0]["elevation"]
  except:
    pass ## bad http status
  
  return None # Handles both failures
def get_coordinates(spots, cachefile):
  """
  Get coordinates of alpine spots with caching
  """

  cachefile = Path(cachefile)
  cache = {}
  
  # load cache
  if cachefile.exists():
    with open(cachefile, "r", encoding="utf-8") as f:
      cache = json.load(f)
  
  # fetch not cached spots
  uncached_spots = [s for s in spots if s not in cache]
  
  if uncached_spots:
    geolocator = Nominatim(user_agent="alpine_spot_finder")
    geocode = RateLimiter(geolocator.geocode, min_delay_seconds=1)
      
    for spot in uncached_spots:
      # location = geocode(f"{spot}")
      location = None
      for country in ["Switzerland"]: ## "Italy", "France"
        location = geocode(f"{spot}, {country}")
        if location: 
          break

      if location:
        cache[spot] = {
          "Latitude": location.latitude,
          "Longitude": location.longitude,
          "Elevation": round(get_elevation(location.latitude, location.longitude))
        }
      else:
        print(f"Could not find coordinates for: {spot}")
        cache[spot] = {
          "Latitude": None,
          "Longitude": None,
          "Elevation": None
        }
    
    # update cache
    with open(cachefile, "w", encoding="utf-8") as f:
      json.dump(cache, f, indent=2, ensure_ascii=False)
  
  # build dataframe from cache
  data = [{"Name": spot, **cache[spot]} for spot in spots]
  return pd.DataFrame(data)
spot_name = list(swiss_alpine_spots.keys())
cachefile = "data/swiss_alpine_spots.json"

df = get_coordinates(spots = spot_name, cachefile = cachefile)

# map path level
# df['Path'] = df["Name"].map(swiss_alpine_spots)
df['Path'] = df["Name"].map(lambda x: swiss_alpine_spots[x][0])
df['Type'] = df["Name"].map(lambda x: swiss_alpine_spots[x][1])

df
Name Latitude Longitude Elevation Path Type
0 Hörnlihütte 45.982197 7.677011 3274 T3 Trail Hike
1 Schönbielhütte 46.001980 7.628957 2639 T3 Trail Hike
2 Gandegghütte 45.964138 7.725649 2960 T3 Trail Hike
3 Berggasthaus Trift 46.030039 7.721071 2334 T2 Trail Hike
4 Rothornhütte 46.048179 7.697287 3220 T3 Trail Hike
... ... ... ... ... ... ...
26 Silvaplana 46.459933 9.795917 1808 - Lake
27 Zermatt 46.021208 7.749254 1610 - City
28 Davos 46.796198 9.823689 1546 - City
29 Saint Moritz 46.497896 9.839243 1825 - City
30 Lauterbrunnen 46.593904 7.907802 816 - City

31 rows × 6 columns

Build map

Make an interactive and color-coded map to display alpine spots

import folium
import re
from folium.plugins import MarkerCluster, Geocoder, Fullscreen

center_lat = df["Latitude"].mean()
center_lon = df["Longitude"].mean()

# initialize map
m = folium.Map(
  location=[center_lat, center_lon],
  zoom_start=7, 
  tiles="OpenStreetMap"
)

markers = MarkerCluster(
  options={
    "maxClusterRadius": 30, # def: 80, smaller less grouping
    "disableClusteringAtZoom": 10  # no clustering at this zoom and higher
  }
).add_to(m)

def get_marker_color(spot):
  if spot["Type"] == "Lake":
    return "purple"
  
  elif spot["Type"] == "City":
    return "orange"
  
  elif spot["Type"] == "Trail Hike":
    if spot["Path"] in ["T2", "T3"]:
      return "red"
    elif spot["Path"] in ["T4", "WS"]:
      return "blue"
  
  elif re.search(r"Glacier (Crossing|Cave|Hike)", spot["Type"]):
    return "darkblue"
  
  return "gray"  # fallback

# Add markers
for _, spot in df.iterrows():
  folium.Marker(
    location=[spot["Latitude"], spot["Longitude"]],
    popup=spot["Name"],
    tooltip=f"{spot['Name']} <br> Path: {spot['Path']} <br> Elevation: {spot['Elevation']}",
    icon=folium.Icon(color=get_marker_color(spot), icon="home")
  ).add_to(markers)

# Add search bar
Geocoder(
  collapsed=False, 
  position='topright',
  add_marker=False
).add_to(m)

# Add fullscreen button
Fullscreen(position='topleft').add_to(m)

m
Make this Notebook Trusted to load map: File -> Trust Notebook

Annex

Utils to clean the cache file

def clean_cache_file(cache_file = 'data/swiss_alpine_spots.json', spots=None):
  if spots is None:
    if Path(cache_file).exists():
      os.remove(cache_file)
      print(f"cache_file {cache_file} deleted")
  else:
    if isinstance(spots, str):
      spots = [spots]
    with open(cache_file, "r") as f:
      data = json.load(f)
    for hut in spots:
      if hut in data:
        del data[hut]
        print(f"{hut} deleted")
      else:
        print(f"{hut} not found in cache")
    with open(cache_file, "w") as f:
      json.dump(data, f, indent=2, ensure_ascii=False)

# clean_cache_file()
# clean_cache_file(spots=["Davos"])

When data are fetched too quickly, NaN is returned.

dd = df.copy() 

dd["Altitude"] = df.apply(lambda row: get_elevation(row["Latitude"], row["Longitude"]), axis=1)
dd
Name Latitude Longitude Elevation Path Type Altitude
0 Hörnlihütte 45.982197 7.677011 3274 T3 Trail Hike 3274.0
1 Schönbielhütte 46.001980 7.628957 2639 T3 Trail Hike 2639.0
2 Gandegghütte 45.964138 7.725649 2960 T3 Trail Hike 2960.0
3 Berggasthaus Trift 46.030039 7.721071 2334 T2 Trail Hike 2334.0
4 Rothornhütte 46.048179 7.697287 3220 T3 Trail Hike NaN
... ... ... ... ... ... ... ...
26 Silvaplana 46.459933 9.795917 1808 - Lake NaN
27 Zermatt 46.021208 7.749254 1610 - City 1610.0
28 Davos 46.796198 9.823689 1546 - City NaN
29 Saint Moritz 46.497896 9.839243 1825 - City NaN
30 Lauterbrunnen 46.593904 7.907802 816 - City 816.0

31 rows × 7 columns

Add a delay to avoid API rate limit

import time

def get_elevation_with_delay(hut):
  time.sleep(0.2)  # need to avoid rate limit
  return round(get_elevation(hut["Latitude"], hut["Longitude"]))
  
start_time = time.time()
dd["Altitude2"] = dd.apply(get_elevation_with_delay, axis=1)
end_time = time.time()

display(dd)

elapsed_time = end_time - start_time
print(f"elapsed time: {elapsed_time:.2f}")
Name Latitude Longitude Elevation Path Type Altitude Altitude2
0 Hörnlihütte 45.982197 7.677011 3274 T3 Trail Hike 3274.0 3274
1 Schönbielhütte 46.001980 7.628957 2639 T3 Trail Hike 2639.0 2639
2 Gandegghütte 45.964138 7.725649 2960 T3 Trail Hike 2960.0 2960
3 Berggasthaus Trift 46.030039 7.721071 2334 T2 Trail Hike 2334.0 2334
4 Rothornhütte 46.048179 7.697287 3220 T3 Trail Hike NaN 3220
... ... ... ... ... ... ... ... ...
26 Silvaplana 46.459933 9.795917 1808 - Lake NaN 1808
27 Zermatt 46.021208 7.749254 1610 - City 1610.0 1610
28 Davos 46.796198 9.823689 1546 - City NaN 1546
29 Saint Moritz 46.497896 9.839243 1825 - City NaN 1825
30 Lauterbrunnen 46.593904 7.907802 816 - City 816.0 816

31 rows × 8 columns

elapsed time: 8.52