History of roman_roads
The changes to the source code of this entry in the repository, newest first, each with the differences it made to the entry's files. They are also listed on GitLab.
[entries] New entry: roman_roads 94d0846
diff --git a/entries/roman_roads/__init__.py b/entries/roman_roads/__init__.py
new file mode 100644
index 0000000..92db5a8
--- /dev/null
+++ b/entries/roman_roads/__init__.py
@@ -0,0 +1,126 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2026 Tiago de Paula Peixoto <tiago@skewed.de>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+
+from scipy.sparse import coo_matrix
+from scipy.sparse.csgraph import connected_components
+from scipy.spatial import cKDTree
+
+from .. import *
+
+title = "Roads of the Roman Empire (Itiner-e, 2024)"
+description = """The network of the roads of the Roman Empire (ca. 300 BCE to 300 CE) from Itiner-e, a digital atlas of Roman roads compiled by P. de Soto, A. Pažout, T. Brughmans and many collaborators (projects MINERVA, Aarhus University, and Viator-e, Universitat Autònoma de Barcelona). The roads were identified from the scholarly literature (atlases, regional studies, survey and excavation reports, gazetteers and milestones), located on modern and historical topographic maps and aerial and satellite imagery, and digitized as 14,769 road segments, each between two ancient places or road crossings, covering the empire at its largest extent (ca. 150 CE). The data were collected between 2020 and 2024.
+
+A node is an end point of road segments (a place or a road crossing), and an edge is a road segment. The network was constructed here from the geometries of the segments: the end points of segments that lie within 1 mm of each other (in the World Mercator projection of the data) are merged into one node, which joins the segments into 13 connected components (the main landmasses and islands; the authors report 14). The network is undirected; some places are joined by several segments (parallel edges). The courses of the roads (the polylines) are not included, and neither is the field `Route_Type`, which repeats the road type as a number, and the constant fields `MaxSimpTol` and `MinSimpTol`.
+
+The data are the canonical 2024 version (version 1.3 of the Zenodo record, September 2025). The earlier versions of the record contain the same segments with the same lengths and attributes, except for corrections of the text fields (version 1, which also has the geometries in longitude and latitude, with more points) and of the geometries of 19 segments (versions 1.1 and 1.2); they are not included. A growing version of the atlas is maintained at itiner-e.org.
+
+The network has the following properties:
+
+- Nodes: `latitude` and `longitude`, the geographical position (converted from the World Mercator coordinates of the data).
+- Edges: `fid`, the identifier of the segment; `name`, its name, formed by the names of the places at its ends (ancient names where known); `type`, `Main Road` or `Secondary Road`; `certainty`, the certainty of the location of the road's course (`Certain`, `Conjectured` or `Hypothetical`); `lower_date` and `upper_date`, the years of construction and abandonment of the road (negative for BCE), and `lower_date_error` and `upper_date_error`, the possible time spans before and after them (NaN where unknown); `construction_period`, the emperor or magistrate under whom the road was built (if known); `itinerary`, the ancient name of the road or the itinerary that it is a part of (e.g. `Tabula Peutingeriana`); `description`, a brief description; `contributor`, the person who digitized the segment; `bibliography`, the sources used; `length`, the geodesic length of the segment (m); `avg_slope`, the average slope along the segment (degrees); `passability`, a number between 0 and 1 (undocumented upstream)."""
+tags = ['Transportation', 'Roads', 'Spatial', 'Weighted', 'Multigraph', 'Metadata']
+url = ['https://zenodo.org/records/17122148', 'https://itiner-e.org']
+citation = [('P. de Soto, A. Pažout, T. Brughmans, P. B. Vahlstrup, Á. Auir, T. Bongers, J. E. B. Christoffersen, M. Crépy, M. H. Johansen, J. Lewis, L. Manière, M. R. Massa, L. M. H. Møller, B. Redon, G. Renda, H. Şahin, A. Sobotková, A. L. Spatzek, P. Verhagen, B. Weissova, "Itiner-e: A high-resolution dataset of roads of the Roman Empire." Scientific Data 12, 1731 (2025)', 'https://doi.org/10.1038/s41597-025-06140-z'),
+ ('P. de Soto, A. Pažout, T. Brughmans et al., "A High-Resolution Dataset of Roads of the Roman Empire: Itiner-e static version 2024." Zenodo, version 1.3 (2025)', 'https://doi.org/10.5281/zenodo.17122148')]
+upstream_license = ("Creative Commons Attribution 4.0 International (CC BY 4.0)",
+ "https://creativecommons.org/licenses/by/4.0/")
+icon_hash = None
+upstream_prefix = 'https://zenodo.org/records'
+files = [("17122148/files/itinere_roads.geojson", None, None)]
+
+def fetch_upstream(force=False):
+ return fetch_upstream_files(__name__.split(".")[-1], upstream_prefix, files,
+ force)
+
+def mercator_to_lonlat(x, y):
+ """Inverse of the ellipsoidal World Mercator projection (EPSG:3395) on the
+ WGS 84 ellipsoid."""
+ a = 6378137.
+ f = 1 / 298.257223563
+ e = numpy.sqrt(f * (2 - f))
+ lon = numpy.degrees(x / a)
+ t = numpy.exp(-y / a)
+ phi = numpy.pi / 2 - 2 * numpy.arctan(t)
+ for i in range(20):
+ s = e * numpy.sin(phi)
+ phi = numpy.pi / 2 - 2 * numpy.arctan(t * ((1 - s) / (1 + s)) ** (e / 2))
+ return lon, numpy.degrees(phi)
+
+# Text fields: (property name, field)
+text_fields = [("name", "Name"), ("type", "Type"), ("certainty", "Segment_s"),
+ ("construction_period", "Cons_per_e"),
+ ("itinerary", "Itinerary"), ("description", "Descriptio"),
+ ("contributor", "Citation"), ("bibliography", "Bibliograp")]
+# Numeric fields (9999 means unknown in the date fields)
+num_fields = [("lower_date", "Lower_Date"), ("lower_date_error", "Low_Date_E"),
+ ("upper_date", "Upper_Date"), ("upper_date_error", "Up_Date_E"),
+ ("length", "Shape_Leng"), ("avg_slope", "Avg_Slope"),
+ ("passability", "passabilit")]
+
+@cache_network()
+@coerce_props()
+@annotate()
+def parse(alts=None):
+ global files
+ name = __name__.split(".")[-1]
+ for fnames, alt, fmt in files:
+ if alts is not None and alt not in alts:
+ continue
+ with open_upstream_file(name, fnames, "r") as f:
+ features = json.load(f)["features"]
+ ends = []
+ for feat in features:
+ lines = feat["geometry"]["coordinates"]
+ if feat["geometry"]["type"] != "MultiLineString" or len(lines) != 1:
+ raise ValueError("unexpected geometry")
+ ends.append(lines[0][0][:2])
+ ends.append(lines[0][-1][:2])
+ ends = numpy.array(ends)
+ # End points within 1 mm of each other (which differ only by rounding
+ # errors) are merged
+ pairs = cKDTree(ends).query_pairs(1e-3, output_type="ndarray")
+ A = coo_matrix((numpy.ones(len(pairs)), (pairs[:, 0], pairs[:, 1])),
+ shape=(len(ends), len(ends)))
+ N, label = connected_components(A, directed=False)
+ x = numpy.bincount(label, weights=ends[:, 0]) / numpy.bincount(label)
+ y = numpy.bincount(label, weights=ends[:, 1]) / numpy.bincount(label)
+ g = Graph(directed=False)
+ g.add_vertex(N)
+ lon, lat = mercator_to_lonlat(x, y)
+ g.vp.latitude = g.new_vp("double", vals=lat)
+ g.vp.longitude = g.new_vp("double", vals=lon)
+ g.add_edge_list(label.reshape(-1, 2))
+ props = [feat["properties"] for feat in features]
+ g.ep.fid = g.new_ep("int", vals=[p["InLine_FID"] for p in props])
+ for k, col in text_fields:
+ g.ep[k] = g.new_ep("string", vals=[(p[col] or "").strip()
+ for p in props])
+ for k, col in num_fields:
+ vals = [numpy.nan if p[col] is None or (k.endswith(("date", "error"))
+ and p[col] == 9999)
+ else p[col] for p in props]
+ g.ep[k] = g.new_ep("double", vals=vals)
+ yield alt, g
+
+
+def get_pos(g, sbm_fit):
+ # Spatial network: laid out with the default parameters of cfd_layout(),
+ # without the partition of the SBM fit
+ return cfd_layout(g)