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')Interactive map of remote alpine spots that I visited in Switzerland
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.
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 failuresdef 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)
mMake 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