Skip to content
This repository was archived by the owner on Jun 16, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions .github/workflows/update-gpx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,23 @@ jobs:
with:
python-version: "3.12"

- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y libgeos-dev libproj-dev

- name: Install dependencies
run: pip install requests beautifulsoup4
run: pip install requests beautifulsoup4 cartopy matplotlib

- name: Run update script
run: python scripts/update_gpx.py --url "${{ vars.NAMING_URL }}"

- name: Generate map
run: python scripts/generate_map.py

- name: Commit and push if changed
run: |
git diff --quiet tezos.gpx && exit 0
git diff --quiet tezos.gpx && [ -z "$(git ls-files --others --exclude-standard map.png)" ] && git diff --quiet map.png 2>/dev/null && exit 0
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add tezos.gpx
git commit -m "Update tezos.gpx with new protocol cities"
git add tezos.gpx map.png
git commit -m "Update tezos.gpx and map with new protocol cities"
git push
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# tezos protocols map
Mapping Tezos protocol names on the globe

![Tezos Protocol Cities Map](map.png)

## context
* *cheeses* for feature test nets
Expand All @@ -15,4 +16,4 @@ Mapping Tezos protocol names on the globe
- script to extract cities from tezos project
- trigger this project on changes from previous URL (a few times a year)

- generate map (more than just https://www.gpsvisualizer.com/map_input?country=ca&form:dynamic_data=https%3A%2F%2Fraw.githubusercontent.com%2Fcotezos%2Ftezosprotocolmap%2Fmain%2Ftezos.gpx&force_type=r&form=html&google_wpt_labels=1&google_wpt_sym=pin&trk_as_wpt=blank&trk_colorize=n&trk_opacity=100&units=metric&wpt_color=blue&wpt_list=name )
- ~~generate map~~ done — `scripts/generate_map.py` renders `map.png` from `tezos.gpx`
Binary file added map.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
122 changes: 122 additions & 0 deletions scripts/generate_map.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""Render tezos.gpx waypoints onto a world map using cartopy + matplotlib."""

import argparse
import xml.etree.ElementTree as ET

import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt


# Manual label offsets (dx, dy in points) for cities that would overlap.
# Positive dx = right, positive dy = up.
LABEL_OFFSETS = {
"Athens": (6, -8),
"Babylon": (6, 0),
"Carthage": (-6, 6),
"Delphi": (-6, 6),
"Florence": (-6, 6),
"Granada": (-6, -8),
"Ithaca": (-6, -8),
"Paris": (6, 6),
"Oxford": (-6, -8),
"Tallinn": (6, 0),
"Nairobi": (6, 0),
}

DEFAULT_OFFSET = (6, -4)


def parse_gpx(gpx_path):
"""Parse GPX file and return list of (name, lat, lon)."""
tree = ET.parse(gpx_path)
root = tree.getroot()
ns = ""
# Handle default GPX namespace if present
if root.tag.startswith("{"):
ns = root.tag.split("}")[0] + "}"

waypoints = []
for wpt in root.findall(f"{ns}wpt"):
lat = float(wpt.get("lat"))
lon = float(wpt.get("lon"))
name_el = wpt.find(f"{ns}name")
name = name_el.text if name_el is not None else ""
waypoints.append((name, lat, lon))
return waypoints


def short_label(name):
"""Strip country suffix and shorten known long names."""
label = name.split(",")[0].strip()
if label == "Edo (Tokyo)":
label = "Edo"
if label == "Quebec City":
label = "Quebec"
if label == "Rio de Janeiro":
label = "Rio"
return label


def generate_map(waypoints, output_path, dpi):
"""Render waypoints on a Robinson-projection world map."""
fig = plt.figure(figsize=(12, 6))
ax = fig.add_subplot(1, 1, 1, projection=ccrs.Robinson())

ax.set_global()
ax.add_feature(cfeature.OCEAN, facecolor="#ddeeff")
ax.add_feature(cfeature.LAND, facecolor="#f0f0f0", edgecolor="none")
ax.add_feature(cfeature.BORDERS, linewidth=0.3, edgecolor="#cccccc")
ax.add_feature(cfeature.COASTLINE, linewidth=0.4, edgecolor="#aaaaaa")
ax.spines["geo"].set_linewidth(0.5)

for name, lat, lon in waypoints:
ax.plot(
lon,
lat,
marker="o",
color="#d62728",
markersize=5,
transform=ccrs.PlateCarree(),
zorder=5,
)

label = short_label(name)
dx, dy = LABEL_OFFSETS.get(label, DEFAULT_OFFSET)

ax.annotate(
label,
xy=(lon, lat),
xycoords=ccrs.PlateCarree()._as_mpl_transform(ax),
xytext=(dx, dy),
textcoords="offset points",
fontsize=6,
fontweight="bold",
color="#333333",
zorder=6,
)

plt.tight_layout(pad=0.5)
fig.savefig(output_path, dpi=dpi, bbox_inches="tight")
plt.close(fig)
print(f"Map saved to {output_path}")


def main():
parser = argparse.ArgumentParser(description="Generate map from GPX waypoints")
parser.add_argument("--gpx", default="tezos.gpx", help="Path to GPX file")
parser.add_argument("--output", default="map.png", help="Output PNG path")
parser.add_argument("--dpi", type=int, default=150, help="Output DPI")
args = parser.parse_args()

waypoints = parse_gpx(args.gpx)
print(f"Parsed {len(waypoints)} waypoints from {args.gpx}")
generate_map(waypoints, args.output, args.dpi)


if __name__ == "__main__":
main()