logo Netzschleuder network catalog, repository, and centrifuge

History of viabundus

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] Add the date when each entry was added, and remove icon_hash by Tiago de Paula Peixoto beff7c9
The dates are those of the commits that first added the entries' files
(2020-08-11 for the entries of the initial commit); renamed entries keep
the date of their first name.

Changed: none
diff --git a/entries/viabundus/__init__.py b/entries/viabundus/__init__.py
index e9cf1e4..873416c 100644
--- a/entries/viabundus/__init__.py
+++ b/entries/viabundus/__init__.py
@@ -61,7 +61,7 @@ citation = [('B. Holterman, M. A.B., K. Andersen, M. Dengg, N. Petersen, "Viabun
 # CC BY 4.0
 upstream_license = ("Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) according to the documentation; CC BY 4.0 according to the Zenodo records",
                     "https://creativecommons.org/licenses/by-sa/4.0/")
-icon_hash = None
+date_added = '2026-10-31'
 # Zenodo records of the versions of the dataset
 records = {"v1_0": "5256289", "v1_1": "6459474", "v1_2": "7696886",
            "v1_3": "10828107", "v2": "16611998"}
[entries] Prefix the properties in the descriptions with (Node), (Edge) or (Graph) by Tiago de Paula Peixoto bc3dbbd
The lists of properties at the end of the descriptions marked edge
properties with "Edge" but left node properties unmarked, or grouped
them in "Nodes: `a`, ...; `b`, ..." items, so the kind of a property
was often unclear. Every item now starts with (Node), (Edge) or (Graph),
followed by the property names, a colon and the explanation, one item
per property or group of properties; qualifications (kinds of nodes,
networks) follow the names in parentheses. Properties that were only
named got a short explanation, and a few items that named the wrong
kind of property were corrected.
diff --git a/entries/viabundus/__init__.py b/entries/viabundus/__init__.py
index 9606e75..e9cf1e4 100644
--- a/entries/viabundus/__init__.py
+++ b/entries/viabundus/__init__.py
@@ -30,8 +30,29 @@ There is one network for each version of the dataset on Zenodo: `v1_0` (April 19
 
 The networks have the following properties (the columns of the nodes and edges tables, whose set changes slightly between versions; missing values are empty strings or -1):
 
-- Nodes: `node_id`, the id of the node in Viabundus (-1 for the added nodes); `has_record`, whether the node is in the nodes table; `latitude` and `longitude`; `name`, the modern official name; `alternative_names` (up to `v1_3`), other modern, foreign and historical names, separated by semicolons; `parent_id`, the id of the parent node; `geonames_id` (up to `v1_2`), or `links` (from `v1_3` on), the ids of the place in other gazetteers (e.g. `gn:` GeoNames, `wd:` Wikidata); `zoomlevel`, the lowest zoom level at which the web map shows the node; `ready`, whether the information on the node is considered correct and complete; and, for each of the attributes settlement, town, fair, toll, bridge, staple, ferry, harbour and lock: `is_<attribute>`, whether the node has it, `<attribute>_from` and `<attribute>_to`, the years in which it existed (usually the first and last mentions), and `<attribute>_description`, a description with references. Also `gregorian_calendar` (from `v1_1` on), whether the dates of the fairs are in the Gregorian calendar; `staple_duration_of_stay`, the duration of the obligatory stay at a staple market; and in `v2` `toll_primary_owner`.
-- Edges: `edge_id`, the id of the edge in Viabundus; `type`, `land`, `winter`, `river`, `canal`, `coast` or `ferry` (`land`, `water` or `ferry` before `v2`, with one edge of type `lsnd` as given); `certainty`, the accuracy of the reconstruction (1 very certain, 2 mediocre, 3 uncertain, including the roads digitized from *Hansische Handelsstraßen* that were not yet checked); `zoomlevel`, the zoom level (1 to 4) from which the web map shows the edge; `from_year` and `to_year`, the years between which the route existed; `length`, its length (m); `section` (except in `v1_3`), the code of the region of the edge (e.g. `NDS` for Lower Saxony, `B-W` for the rough digitizations of *Hansische Handelsstraßen*); `comment_id`, the id(s) of the comments on the edge, separated by semicolons; `ready` (`v1_2` and `v1_3`), whether the edge is considered complete; in `v2` `slope_multiplier`, the factor by which the slope of the segment increases the travel time on foot (1 for water routes)."""
+- (Node) `node_id`: the id of the node in Viabundus (-1 for the added nodes).
+- (Node) `has_record`: whether the node is in the nodes table.
+- (Node) `latitude`, `longitude`: the position of the node.
+- (Node) `name`: the modern official name.
+- (Node) `alternative_names` (up to `v1_3`): other modern, foreign and historical names, separated by semicolons.
+- (Node) `parent_id`: the id of the parent node.
+- (Node) `geonames_id` (up to `v1_2`), `links` (from `v1_3` on): the ids of the place in other gazetteers (e.g. `gn:` GeoNames, `wd:` Wikidata).
+- (Node) `zoomlevel`: the lowest zoom level at which the web map shows the node.
+- (Node) `ready`: whether the information on the node is considered correct and complete.
+- (Node) `is_<attribute>`, `<attribute>_from`, `<attribute>_to`, `<attribute>_description`: for each of the attributes settlement, town, fair, toll, bridge, staple, ferry, harbour and lock, whether the node has it, the years in which it existed (usually the first and last mentions), and a description with references.
+- (Node) `gregorian_calendar` (from `v1_1` on): whether the dates of the fairs are in the Gregorian calendar.
+- (Node) `staple_duration_of_stay`: the duration of the obligatory stay at a staple market.
+- (Node) `toll_primary_owner` (`v2` only): the primary owner of the toll.
+- (Edge) `edge_id`: the id of the edge in Viabundus.
+- (Edge) `type`: `land`, `winter`, `river`, `canal`, `coast` or `ferry` (`land`, `water` or `ferry` before `v2`, with one edge of type `lsnd` as given).
+- (Edge) `certainty`: the accuracy of the reconstruction (1 very certain, 2 mediocre, 3 uncertain, including the roads digitized from *Hansische Handelsstraßen* that were not yet checked).
+- (Edge) `zoomlevel`: the zoom level (1 to 4) from which the web map shows the edge.
+- (Edge) `from_year`, `to_year`: the years between which the route existed.
+- (Edge) `length`: its length (m).
+- (Edge) `section` (except in `v1_3`): the code of the region of the edge (e.g. `NDS` for Lower Saxony, `B-W` for the rough digitizations of *Hansische Handelsstraßen*).
+- (Edge) `comment_id`: the id(s) of the comments on the edge, separated by semicolons.
+- (Edge) `ready` (`v1_2` and `v1_3`): whether the edge is considered complete.
+- (Edge) `slope_multiplier` (`v2` only): the factor by which the slope of the segment increases the travel time on foot (1 for water routes)."""
 tags = ['Transportation', 'Roads', 'Spatial', 'Weighted', 'Metadata']
 url = ['https://www.viabundus.eu', 'https://doi.org/10.5281/zenodo.5256288']
 citation = [('B. Holterman, M. A.B., K. Andersen, M. Dengg, N. Petersen, "Viabundus: Map of Premodern European Transport and Mobility." Research Data Journal for the Humanities and Social Sciences 7, 1-13 (2022)', 'https://doi.org/10.1163/24523666-bja10025'),
[entries] New entry: viabundus by Tiago de Paula Peixoto 23251f4
diff --git a/entries/viabundus/__init__.py b/entries/viabundus/__init__.py
new file mode 100644
index 0000000..9606e75
--- /dev/null
+++ b/entries/viabundus/__init__.py
@@ -0,0 +1,171 @@
+#! /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/>.
+
+from .. import *
+import csv
+import html
+import re
+
+title = "Viabundus premodern northern European road and waterway network (1350-1650)"
+description = """The road and waterway network of late medieval and early modern northern Europe (1350-1650) of Viabundus, a historical map and database of premodern transport and mobility, originally conceived as the digitization of the atlas of land roads in the Hanseatic area of F. Bruns and H. Weczerka, *Hansische Handelsstraßen* (1962), and extended with researched road networks of the present-day Netherlands, Denmark, Finland, several German states (Lower Saxony, Schleswig-Holstein, Thuringia, Saxony-Anhalt, Brandenburg, Mecklenburg-Vorpommern, Hesse, North Rhine-Westphalia), parts of Poland and the Baltic states, and with navigable waterways.
+
+Nodes are the places that may require an action of a traveler (change roads, pay a fee, change the means of transport, cross a river, reach a destination): settlements, towns, tolls, staple markets, fairs, ferries, bridges, harbors and locks, with the years in which these attributes existed, and junctions of routes. A cluster of nodes in a town can be subordinated to a parent node (`parent_id`), whose attributes settlement, town, staple and fair apply to them as well. Edges are route segments between two nodes: land roads, winter roads (usable only in winter, e.g. across marshes or frozen lakes), rivers, canals, coastal and lake routes, and ferries, with the certainty of their reconstruction and the years in which they existed (if not given, the whole period). The networks are undirected, and have a few parallel edges (alternative routes between the same nodes; 44 to 83 per network).
+
+There is one network for each version of the dataset on Zenodo: `v1_0` (April 19, 2021), `v1_1` (December 6, 2021), `v1_2` (September 21, 2022), `v1_3` (March 17, 2024) and `v2` (April 25, 2025), each built from the nodes and edges tables of that version. The edges of `v1_0` do not give their end nodes: they are joined to the nodes at their end points (after rounding the coordinates to $10^{-5}$ degrees, about 1 m), and the 3,175 points where edges end without a node (junctions, which are nodes of their own in later versions) are added as nodes. In the later versions, the edges give their end nodes, and the few end nodes that are missing from the nodes table (69 in `v1_3`, 1 in `v2`) are added as nodes, at the end points of the edges. In both cases, these nodes have `has_record` false and no other attributes than their position. The geometries of the edges, the descriptions of the edges, the alternative names of `v2` (a separate table), and the tables of literature, populations, fairs, town outlines and waterways of 1500 are not included.
+
+The networks have the following properties (the columns of the nodes and edges tables, whose set changes slightly between versions; missing values are empty strings or -1):
+
+- Nodes: `node_id`, the id of the node in Viabundus (-1 for the added nodes); `has_record`, whether the node is in the nodes table; `latitude` and `longitude`; `name`, the modern official name; `alternative_names` (up to `v1_3`), other modern, foreign and historical names, separated by semicolons; `parent_id`, the id of the parent node; `geonames_id` (up to `v1_2`), or `links` (from `v1_3` on), the ids of the place in other gazetteers (e.g. `gn:` GeoNames, `wd:` Wikidata); `zoomlevel`, the lowest zoom level at which the web map shows the node; `ready`, whether the information on the node is considered correct and complete; and, for each of the attributes settlement, town, fair, toll, bridge, staple, ferry, harbour and lock: `is_<attribute>`, whether the node has it, `<attribute>_from` and `<attribute>_to`, the years in which it existed (usually the first and last mentions), and `<attribute>_description`, a description with references. Also `gregorian_calendar` (from `v1_1` on), whether the dates of the fairs are in the Gregorian calendar; `staple_duration_of_stay`, the duration of the obligatory stay at a staple market; and in `v2` `toll_primary_owner`.
+- Edges: `edge_id`, the id of the edge in Viabundus; `type`, `land`, `winter`, `river`, `canal`, `coast` or `ferry` (`land`, `water` or `ferry` before `v2`, with one edge of type `lsnd` as given); `certainty`, the accuracy of the reconstruction (1 very certain, 2 mediocre, 3 uncertain, including the roads digitized from *Hansische Handelsstraßen* that were not yet checked); `zoomlevel`, the zoom level (1 to 4) from which the web map shows the edge; `from_year` and `to_year`, the years between which the route existed; `length`, its length (m); `section` (except in `v1_3`), the code of the region of the edge (e.g. `NDS` for Lower Saxony, `B-W` for the rough digitizations of *Hansische Handelsstraßen*); `comment_id`, the id(s) of the comments on the edge, separated by semicolons; `ready` (`v1_2` and `v1_3`), whether the edge is considered complete; in `v2` `slope_multiplier`, the factor by which the slope of the segment increases the travel time on foot (1 for water routes)."""
+tags = ['Transportation', 'Roads', 'Spatial', 'Weighted', 'Metadata']
+url = ['https://www.viabundus.eu', 'https://doi.org/10.5281/zenodo.5256288']
+citation = [('B. Holterman, M. A.B., K. Andersen, M. Dengg, N. Petersen, "Viabundus: Map of Premodern European Transport and Mobility." Research Data Journal for the Humanities and Social Sciences 7, 1-13 (2022)', 'https://doi.org/10.1163/24523666-bja10025'),
+            ('B. Holterman, M. C. Dengg, M. A.B., K. H. Andersen, "Viabundus map of premodern European transport and mobility." Zenodo, dataset, versions 1.0-2 (2021-2025)', 'https://doi.org/10.5281/zenodo.5256288')]
+# The documentation and the web site state CC BY-SA 4.0, the Zenodo records
+# CC BY 4.0
+upstream_license = ("Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) according to the documentation; CC BY 4.0 according to the Zenodo records",
+                    "https://creativecommons.org/licenses/by-sa/4.0/")
+icon_hash = None
+# Zenodo records of the versions of the dataset
+records = {"v1_0": "5256289", "v1_1": "6459474", "v1_2": "7696886",
+           "v1_3": "10828107", "v2": "16611998"}
+upstream_prefix = {alt: f"https://zenodo.org/records/{r}/files"
+                   for alt, r in records.items()}
+files = [((f"Viabundus-{v}-CSV.zip:Nodes.csv", f"Viabundus-{v}-CSV.zip:Edges.csv"),
+          f"v{v.replace('.', '_')}", None) for v in ["1.0", "1.1", "1.2", "1.3"]]
+files += [(("nodes.csv", "edges.csv"), "v2", None)]
+
+def fetch_upstream(force=False):
+    for f in files:
+        fetch_upstream_files(__name__.split(".")[-1], upstream_prefix[f[1]],
+                             [f], force)
+
+# Columns renamed for consistency between the versions
+RENAME = {"parentid": "parent_id", "fromyear": "from_year", "toyear": "to_year",
+          "from": "from_year", "to": "to_year", "descriptionid": "comment_id",
+          "fromnode": "from_node", "tonode": "to_node", "alternative_name":
+          "alternative_names", "slopemultiplier": "slope_multiplier",
+          "town_first_mentioned": "town_from"}
+NULL = {"", "null", "NULL"}
+
+def read_table(name, fname):
+    csv.field_size_limit(1 << 30)
+    with open_upstream_file(name, fname, "r", encoding="utf-8-sig") as f:
+        rows = list(csv.reader(f))
+    cols = [RENAME.get(c.lower(), c.lower()) for c in rows[0]]
+    return [{c: (None if x in NULL else x) for c, x in zip(cols, r)}
+            for r in rows[1:] if r]
+
+def clean_text(x):
+    "HTML-escaped text with markup, as plain text."
+    x = html.unescape(x)
+    x = re.sub(r"<br\s*/?>", "\n", x, flags=re.I)
+    return html.unescape(re.sub(r"<[^>]+>", "", x)).strip()
+
+def set_props(g, pmap, rows, skip, n=None):
+    """Store the columns of `rows` as properties: flags ("y") as bools,
+    integer columns as integers (-1 if missing), the others as strings."""
+    n = len(rows) if n is None else n
+    for c in rows[0]:
+        if c in skip:
+            continue
+        vals = [r[c] for r in rows]
+        present = [x for x in vals if x is not None]
+        if c.startswith("is_") or c in ("ready", "gregorian_calendar"):
+            t, vals = "bool", [x == "y" for x in vals]
+        elif c != "comment_id" and present and all(re.fullmatch(r"-?\d+", x) for x in present):
+            t, vals = "int64_t", [-1 if x is None else int(x) for x in vals]
+        elif c in ("length", "slope_multiplier") and present:
+            t, vals = "double", [numpy.nan if x is None else float(x) for x in vals]
+        else:
+            if c.endswith("description"):
+                vals = [clean_text(x or "") for x in vals]
+            t, vals = "string", [x or "" for x in vals]
+        default = {"bool": False, "int64_t": -1, "double": numpy.nan,
+                   "string": ""}[t]
+        vals = vals + [default] * (n - len(vals))
+        pmap[c] = g.new_vp(t, vals=vals) if pmap is g.vp else g.new_ep(t, vals=vals)
+
+def endpoints(wkt):
+    "First and last point (lon, lat) of a WKT line."
+    pts = wkt[wkt.index("(") + 1:].replace("(", "").replace(")", "").split(",")
+    return [tuple(map(float, p.split())) for p in (pts[0], pts[-1])]
+
+@cache_network()
+@coerce_props()
+@annotate()
+def parse(alts=None):
+    name = __name__.split(".")[-1]
+    for fnames, alt, fmt in files:
+        if alts is not None and alt not in alts:
+            continue
+        nodes = read_table(name, fnames[0])
+        edges = read_table(name, fnames[1])
+        idx = {r["id"]: i for i, r in enumerate(nodes)}
+        lon = [float(r["longitude"]) for r in nodes]
+        lat = [float(r["latitude"]) for r in nodes]
+        geom = "wkt" if "wkt" in edges[0] else "geometry"
+        # nodes added for the edges (junctions or missing nodes), by position
+        extra = {}
+        if "from_node" not in edges[0]:
+            # the first version gives no end nodes: the edges are joined to
+            # the nodes at their end points (rounded to 1e-5 degrees, about
+            # 1 m), and to junctions (not in the nodes table) where several
+            # edges end at the same point
+            pos = {(round(x, 5), round(y, 5)): i
+                   for i, (x, y) in enumerate(zip(lon, lat))}
+        elist = []
+        for r in edges:
+            ends = endpoints(r[geom])
+            e = []
+            for k, (x, y) in zip(["from_node", "to_node"], ends):
+                if "from_node" in r:
+                    if r[k] in idx:
+                        e.append(idx[r[k]])
+                        continue
+                    key = ("id", r[k])
+                else:
+                    key = (round(x, 5), round(y, 5))
+                    if key in pos:
+                        e.append(pos[key])
+                        continue
+                if key not in extra:
+                    extra[key] = len(nodes) + len(extra)
+                    lon.append(x)
+                    lat.append(y)
+                e.append(extra[key])
+            elist.append(e)
+        g = Graph(directed=False)
+        g.add_vertex(len(nodes) + len(extra))
+        g.add_edge_list(elist)
+        g.vp.node_id = g.new_vp("int64_t", vals=[int(r["id"]) for r in nodes] +
+                                [int(k[1]) if k[0] == "id" else -1 for k in extra])
+        g.vp.has_record = g.new_vp("bool", vals=[True] * len(nodes) +
+                                   [False] * len(extra))
+        g.vp.latitude = g.new_vp("double", vals=lat)
+        g.vp.longitude = g.new_vp("double", vals=lon)
+        set_props(g, g.vp, nodes, {"id", "latitude", "longitude"},
+                  g.num_vertices())
+        g.ep.edge_id = g.new_ep("int64_t", vals=[int(r["id"]) for r in edges])
+        set_props(g, g.ep, edges, {"id", geom, "from_node", "to_node"})
+        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)