Skip to content

Instantly share code, notes, and snippets.

@dgketchum
Last active May 20, 2025 18:51
Show Gist options
  • Save dgketchum/3c9e3d7bb1db5be9a09a3a6d6d26ebee to your computer and use it in GitHub Desktop.
Save dgketchum/3c9e3d7bb1db5be9a09a3a6d6d26ebee to your computer and use it in GitHub Desktop.
Montana Range High Points Shapefile
import os
import csv
import requests
import time
import geopandas
from shapely.geometry import Point
"""
Montana State Library: Montana's Tallest Peaks by Mountain Range
https://msl.mt.gov/geoinfo/geography/geography_facts/montanaxs_tallest_peaks_by_mountain_range
Pulling most of these by GNIS name lookup and filling in the missing values under MISSING
by finding the peak with the MSL link and getting coordinates from Google Earth.
"""
WATERDATA_API_URL = "https://dashboard.waterdata.usgs.gov/service/geocoder/get/location/1.0"
STATE_ABBREVIATION = "MT"
REQUEST_TIMEOUT = 15
DELAY_BETWEEN_REQUESTS = 2.0
MISSING = {'Bears Paw Mountains': (48.148593, -109.65075),
# Beartooth Mountains GNIS lookup gets first result from Tobacco Root
'Beartooth Mountains': (45.1841, -109.79129),
# exact point of Beaverhead Mtns HP is in Idaho
'Beaverhead Mountains': (44.447227, -112.996476),
# Bighorn's Montana HP is drive-up
# 'Bighorn Mountains': (45.00098, -107.91139),
# Bighorn Overall HP is Cloud Peak
'Bighorn Mountains': (44.382147, -107.173951),
'Big Sheep Mountains': (47.044020, -105.730260),
# not sure why this one finds a bad coordinate pair
'Bitteroot Range': (45.88965, -114.2981),
'Blacktail Mountains': (44.987672, -112.578531),
'Castle Mountains': (46.452622, -110.753254),
# not sure why this one finds a bad coordinate pair
'Chalk Buttes': (45.708559, -104.734215),
'Gravelly Range': (44.90425, -111.85563),
'Henrys Lake Mountains': (44.7633, -111.3906),
'Highland Mountains': (45.742484, -112.461789),
'John Long Mountains': (46.479171, -113.680506),
'Little Snowy Mountains': (46.752222, -109.173333),
'Lewis and Clark Range': (47.11654, -112.738643),
'Long Pines': (45.639974, -104.184526),
'North Moccasin Mountains': (47.31448, -109.46758),
'Ruby Range': (45.312778, -112.228056),
'South Moccasin Mountains': (47.1725, -109.523889),
'Sweet Grass Hills': (48.931379, -111.532198),
'Wolf Mountains': (45.03603, -107.19281)}
def get_coordinates_from_gnis(mountain_name):
params = {
"term": mountain_name,
"include": "gnis",
"states": STATE_ABBREVIATION,
"maxSuggestions": 1
}
headers = {}
response = requests.get(WATERDATA_API_URL, params=params, headers=headers, timeout=REQUEST_TIMEOUT)
response.raise_for_status()
data = response.json()
if data and len(data) > 0:
location = data[0]
if location.get("Source") == "gnis" and "Latitude" in location and "Longitude" in location:
print(f'{mountain_name}: {location["Latitude"]:.2f}, {location["Longitude"]:.2f}')
return location["Latitude"], location["Longitude"]
else:
raise ValueError
def process_mountains_csv(input_csv_filepath, output_csv_filepath, output_shapefile_filepath):
processed_mountains_for_csv = []
mountains_for_geodataframe = []
with open(input_csv_filepath, mode='r', encoding='utf-8-sig') as infile:
reader = csv.DictReader(infile)
for row_number, row_dict in enumerate(reader, 1):
name_from_csv = row_dict.get('Name')
climbed = row_dict.get('done')
range = row_dict.get('range')
elev_ft = row_dict.get('elev_ft')
status_msg = ""
lat, lon = None, None
current_attributes = dict(row_dict)
if name_from_csv and name_from_csv.strip():
stripped_name = name_from_csv.strip()
current_attributes['Name'] = stripped_name
if range in MISSING:
loc = MISSING[range]
print(f'{stripped_name}: {loc[0]:.2f}, {loc[1]:.2f}')
else:
loc = get_coordinates_from_gnis(stripped_name)
lat, lon = loc[0], loc[1]
time.sleep(DELAY_BETWEEN_REQUESTS)
status_msg = "Success"
point_data = current_attributes.copy()
point_data['geometry'] = Point(float(lon), float(lat))
point_data['latitude'] = float(lat)
point_data['longitude'] = float(lon)
point_data['status'] = status_msg
mountains_for_geodataframe.append(point_data)
else:
status_msg = "Skipped - Empty name in CSV"
current_attributes['Name'] = name_from_csv if name_from_csv else ''
csv_entry = {
'name': current_attributes['Name'],
'latitude': lat if lat is not None else 'N/A',
'longitude': lon if lon is not None else 'N/A',
'range': range,
'elev_ft': elev_ft,
'done': climbed
}
processed_mountains_for_csv.append(csv_entry)
if processed_mountains_for_csv:
with open(output_csv_filepath, mode='w', newline='', encoding='utf-8') as outfile:
fieldnames = list(csv_entry.keys())
writer = csv.DictWriter(outfile, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(processed_mountains_for_csv)
if mountains_for_geodataframe:
gdf = geopandas.GeoDataFrame(mountains_for_geodataframe, crs="EPSG:4326")
sanitized_columns = {}
for col_name in gdf.columns:
if col_name == 'geometry':
sanitized_columns[col_name] = 'geometry'
continue
s_name = str(col_name).replace(' ', '_').replace('.', '_').replace('-', '_')
s_name = s_name[:10]
original_s_name = s_name
count = 1
while s_name in sanitized_columns.values():
s_name = original_s_name[:10 - len(str(count))] + str(count)
count += 1
sanitized_columns[col_name] = s_name
gdf.rename(columns=sanitized_columns, inplace=True)
gdf.to_file(output_shapefile_filepath, driver='ESRI Shapefile')
if __name__ == "__main__":
d = os.path.expanduser('~')
input_file = os.path.join(d, 'mt_highpoints_noCoords.csv')
output_csv = os.path.join(d, 'mt_highpoints.csv')
output_shp = os.path.join(d, 'mt_highpoints.shp')
process_mountains_csv(input_file, output_csv, output_shp)
# ========================= EOF ====================================================================
Name elev_ft range done
Granite Peak 12799 Beartooth Mountains 0
Hilgard Peak 11316 Madison Range 0
Mount Cowen 11212 Absaroka Range 0
Crazy Peak 11209 Crazy Mountains 1
Tweedy Mountain 11154 Pioneer Mountains 0
Eighteenmile Peak 11125 Beaverhead Mountains 1
Electric Peak 10969 Gallatin Range 0
West Goat Peak 10793 Anaconda Range 1
Un-named Peak 10606 Henrys Lake Mountains 0
Hollowtop Mountain 10604 Tobacco Root Mountains 1
Sunset Peak 10581 Snowcrest Range 1
Black Butte 10542 Gravelly Range 1
Mount Cleveland 10466 Lewis Range 0
Table Mountain 10223 Highland Mountains 0
Mount Jefferson 10203 Centennial Mountains 1
Mount Powell 10168 Flint Creek Range 1
Trapper Peak 10157 Bitteroot Range 1
Kintla Peak 10101 Livingston Range 0
McDonald Peak 9820 Mission Range 1
Ellis Peak 9699 Tendoy Mountains 0
Sacagawea Peak 9650 Bridger Range 0
Mount Edith 9480 Big Belt Mountains 0
Un-named Peak 9477 Blacktail Mountains 0
Crow Peak 9414 Elkhorn Mountains 0
Red Mountain 9411 Lewis and Clark Range 0
Rocky Mountain 9392 Sawtooth Range 1
Un-named Peak 9391 Ruby Range 0
Holland Peak 9356 Swan Range 1
Un-named Peak 9257 Bighorn Mountains 0
Big Baldy Mountain 9177 Little Belt Mountains 0
Kent Peak 9010 Sapphire Mountains 0
Haystack Mountain 8819 Boulder Mountains 0
Big Pryor Mountain 8786 Pryor Mountains 0
Snowshoe Peak 8738 Cabinet Mountains 1
Great Northern Mountain 8705 Flathead Range 1
Greathouse Peak 8681 Big Snowy Mountains 1
McLeod Peak 8620 Rattlesnake Mountains 1
Elk Peak 8566 Castle Mountains 0
Butte Cabin Ridge 8468 John Long Mountains 0
Nasukoin Mountain 8086 Whitefish Range 1
Ch-paa-qn Peak 7996 Reservation Divide 0
Poorman Mountain 7832 Galton Range 0
Northwest Peak 7705 Purcell Mountains 1
Highwood Baldy 7670 Highwood Mountains 0
Old Baldy Mountain 7511 Garnet Range 0
Cherry Peak 7352 Couer d'Alene Mountains 0
Stark Mountain 7352 Ninemile Divide 1
McGuire Mountain 6991 Salish Mountains 0
West Butte 6983 Sweet Grass Hills 0
Bearpaw Baldy 6916 Bears Paw Mountains 0
Judith Peak 6428 Judith Mountains 0
Un-named Peak 6260 Little Snowy Mountains 0
Un-named Peak 5798 South Moccasin Mountains 0
Antoine Butte 5720 Little Rocky Mountains 0
Un-named Peak 5602 North Moccasin Mountains 0
Un-named Peak 5450 Wolf Mountains 0
Dunn Mountain 4744 Bull Mountains 0
West Chalk Butte 4200 Chalk Buttes 0
Tri-Point Lookout 4120 Long Pines 0
Big Sheep Mountain 3590 Big Sheep Mountains 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment