Skip to content

velorail API Documentation

gpxviewer

Created on 2025-01-17

@author: wf

GPXViewer

display a given gpx file

Source code in velorail/gpxviewer.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
class GPXViewer:
    """
    display a given gpx file
    """

    samples = {
        "Mountain bike loop at Middlesex Fells reservation.": "https://www.topografix.com/fells_loop.gpx",
        "Via Verde de Arditurri": "https://www.bahntrassenradwege.de/images/Spanien/Baskenland/V-v-de-arditurri/Arditurri2.gpx",
        "Vía Verde del Fc Vasco Navarro": "https://ebike.bitplan.com/images/ebike/e/ec/VV-del-FC-Vasco-Navarro_fdmi1644.gpx",
    }
    default_center = [51.4934, 0.0098]  # greenwich
    default_zoom = 11

    @classmethod
    def from_url(cls, gpx_url) -> "GPXViewer":
        viewer = cls()
        viewer.load_gpx(gpx_url)
        return viewer

    def __init__(self, args: argparse.Namespace = None):
        """
        constructor

        Args:
            args(argparse.Namespace): command line arguments
        """
        self.args = args
        self.tour = None
        self.leg_styles = LegStyles.default()
        self.set_center()
        if args:
            self.debug = args.debug
            self.token = args.token
            self.center = args.center
            self.zoom = args.zoom
            if self.args.gpx:
                self.load_gpx(self.args.gpx)
        else:
            self.zoom = GPXViewer.default_zoom
            self.center = GPXViewer.default_center

    def load_gpx(self, gpx_url: str):
        """
        load the given gpx file
        """
        response = requests.get(gpx_url)
        self.gpx = gpxpy.parse(response.text)
        self.get_points(self.gpx)
        return self.gpx

    def set_center(self):
        """
        Calculate and set the center and bounding box based on tour legs
        """

        if self.tour and self.tour.legs:
            points = []
            for leg in self.tour.legs:
                points.append(leg.start.coordinates)
                points.append(leg.end.coordinates)
            # Wrong order: lats, lons need to be extracted after zipping
            lats = [p[0] for p in points]
            lons = [p[1] for p in points]
            self.bounding_box = (min(lats), max(lats), min(lons), max(lons))
            self.center = ((min(lats) + max(lats)) / 2, (min(lons) + max(lons)) / 2)
        else:
            self.center = self.default_center
            self.bounding_box = None
        return self.center

    def add_leg(
        self,
        start_point,
        end_point,
        leg_type: str,
        add_end_point: bool = False,
        url: Optional[str] = None,
    ):
        """
        Add a leg to the tour

        Args:
            start_point: GPX point for start of leg
            end_point: GPX point for end of leg
            leg_type: Type of leg (e.g., "bike", "train", "car")
            add_end_point: Whether to add the end point (True for last leg)
            url: Optional URL associated with the leg
        """
        if self.tour is None:
            self.tour = Tour(name="GPX Tour")

        # Create locations
        start_loc = Loc(
            id=str(len(self.tour.legs)),
            name=start_point.name if hasattr(start_point, "name") else None,
            coordinates=(start_point.latitude, start_point.longitude),
        )
        end_loc = Loc(
            id=str(len(self.tour.legs) + 1),
            name=end_point.name if hasattr(end_point, "name") else None,
            coordinates=(end_point.latitude, end_point.longitude),
        )

        # Create and add leg
        leg = Leg(leg_type=leg_type, start=start_loc, end=end_loc, url=url)
        self.tour.legs.append(leg)

    def get_points(self, gpx, way_points_fallback: bool = False):
        """
        Extract waypoints and legs from the GPX object and create a tour
        """
        self.tour = Tour(name="GPX Tour")

        # Process routes
        for route in gpx.routes:
            for i in range(len(route.points) - 1):
                url = route.link.href if route.link else None
                is_last = i == len(route.points) - 2
                self.add_leg(
                    route.points[i],
                    route.points[i + 1],
                    "bike",  # Default to bike for routes
                    add_end_point=is_last,
                    url=url,
                )

        # Process tracks
        for track in gpx.tracks:
            for segment in track.segments:
                for i in range(len(segment.points) - 1):
                    is_last = i == len(segment.points) - 2
                    self.add_leg(
                        segment.points[i],
                        segment.points[i + 1],
                        "bike",  # Default to bike for tracks
                        add_end_point=is_last,
                    )

        prev_loc = None
        # Handle waypoints if no legs were created and fallback is active
        if not self.tour.legs and gpx.waypoints and way_points_fallback:
            for i, waypoint in enumerate(gpx.waypoints):
                loc = Loc(
                    id=str(i),
                    name=waypoint.name,
                    coordinates=(waypoint.latitude, waypoint.longitude),
                    notes=waypoint.description,
                )
                if i > 0:
                    leg = Leg(
                        leg_type="bike",
                        start=prev_loc,
                        end=loc,
                        url=waypoint.link.href if waypoint.link else None,
                    )
                    self.tour.legs.append(leg)
                prev_loc = loc

    def parse_lines(self, lines: str):
        """
        Parse the 'lines' parameter into route segments.

        Args:
            lines (str): The input string containing routes in the format:
                "By bike: 51.243931° N, 6.520022° E, 51.269222° N, 6.625467° E:"

        Returns:
            Tour: Created tour from the parsed coordinates
        """
        coordinate_pattern = r"(\d+\.\d+)° ([NS]), (\d+\.\d+)° ([EW])"
        route_segments = lines.split(":")
        self.tour = Tour(name="Parsed Tour")

        for segment in route_segments:
            segment = segment.strip()

            # Extract leg type (e.g., "bike", "train")
            leg_type_match = re.match(r"By (\w+)→", segment)
            if leg_type_match:
                leg_type = leg_type_match.group(1).lower()
                pass
            else:
                leg_type = "bike"

            points = re.findall(coordinate_pattern, segment)
            if points:
                prev_loc = None
                for i, (lat, ns, lon, ew) in enumerate(points):
                    lat_val = float(lat) * (-1 if ns == "S" else 1)
                    lon_val = float(lon) * (-1 if ew == "W" else 1)

                    loc = Loc(
                        id=str(len(self.tour.legs) + i),
                        name=None,
                        coordinates=(lat_val, lon_val),
                    )

                    if prev_loc:
                        leg = Leg(leg_type=leg_type, start=prev_loc, end=loc)
                        self.tour.legs.append(leg)
                    prev_loc = loc

        if not self.tour.legs:
            raise ValueError("No valid routes found in the input lines.")

        return self.tour

    def parse_lines_and_show(self, lines: str, zoom: int = None):
        """
        Parse lines and display them on the map
        """
        self.parse_lines(lines)
        self.show(zoom=zoom)

    def show(self, zoom: int = None, center=None):
        """
        Show tour with styled paths
        """
        if zoom is None:
            zoom = self.zoom
        if center is None:
            center = self.set_center()

        self.map = LeafletMap(center=center, zoom=zoom)
        if self.tour:
            self.map.draw_tour(self.tour, self.leg_styles)

__init__(args=None)

constructor

Parameters:

Name Type Description Default
args(argparse.Namespace)

command line arguments

required
Source code in velorail/gpxviewer.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def __init__(self, args: argparse.Namespace = None):
    """
    constructor

    Args:
        args(argparse.Namespace): command line arguments
    """
    self.args = args
    self.tour = None
    self.leg_styles = LegStyles.default()
    self.set_center()
    if args:
        self.debug = args.debug
        self.token = args.token
        self.center = args.center
        self.zoom = args.zoom
        if self.args.gpx:
            self.load_gpx(self.args.gpx)
    else:
        self.zoom = GPXViewer.default_zoom
        self.center = GPXViewer.default_center

add_leg(start_point, end_point, leg_type, add_end_point=False, url=None)

Add a leg to the tour

Parameters:

Name Type Description Default
start_point

GPX point for start of leg

required
end_point

GPX point for end of leg

required
leg_type str

Type of leg (e.g., "bike", "train", "car")

required
add_end_point bool

Whether to add the end point (True for last leg)

False
url Optional[str]

Optional URL associated with the leg

None
Source code in velorail/gpxviewer.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def add_leg(
    self,
    start_point,
    end_point,
    leg_type: str,
    add_end_point: bool = False,
    url: Optional[str] = None,
):
    """
    Add a leg to the tour

    Args:
        start_point: GPX point for start of leg
        end_point: GPX point for end of leg
        leg_type: Type of leg (e.g., "bike", "train", "car")
        add_end_point: Whether to add the end point (True for last leg)
        url: Optional URL associated with the leg
    """
    if self.tour is None:
        self.tour = Tour(name="GPX Tour")

    # Create locations
    start_loc = Loc(
        id=str(len(self.tour.legs)),
        name=start_point.name if hasattr(start_point, "name") else None,
        coordinates=(start_point.latitude, start_point.longitude),
    )
    end_loc = Loc(
        id=str(len(self.tour.legs) + 1),
        name=end_point.name if hasattr(end_point, "name") else None,
        coordinates=(end_point.latitude, end_point.longitude),
    )

    # Create and add leg
    leg = Leg(leg_type=leg_type, start=start_loc, end=end_loc, url=url)
    self.tour.legs.append(leg)

get_points(gpx, way_points_fallback=False)

Extract waypoints and legs from the GPX object and create a tour

Source code in velorail/gpxviewer.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def get_points(self, gpx, way_points_fallback: bool = False):
    """
    Extract waypoints and legs from the GPX object and create a tour
    """
    self.tour = Tour(name="GPX Tour")

    # Process routes
    for route in gpx.routes:
        for i in range(len(route.points) - 1):
            url = route.link.href if route.link else None
            is_last = i == len(route.points) - 2
            self.add_leg(
                route.points[i],
                route.points[i + 1],
                "bike",  # Default to bike for routes
                add_end_point=is_last,
                url=url,
            )

    # Process tracks
    for track in gpx.tracks:
        for segment in track.segments:
            for i in range(len(segment.points) - 1):
                is_last = i == len(segment.points) - 2
                self.add_leg(
                    segment.points[i],
                    segment.points[i + 1],
                    "bike",  # Default to bike for tracks
                    add_end_point=is_last,
                )

    prev_loc = None
    # Handle waypoints if no legs were created and fallback is active
    if not self.tour.legs and gpx.waypoints and way_points_fallback:
        for i, waypoint in enumerate(gpx.waypoints):
            loc = Loc(
                id=str(i),
                name=waypoint.name,
                coordinates=(waypoint.latitude, waypoint.longitude),
                notes=waypoint.description,
            )
            if i > 0:
                leg = Leg(
                    leg_type="bike",
                    start=prev_loc,
                    end=loc,
                    url=waypoint.link.href if waypoint.link else None,
                )
                self.tour.legs.append(leg)
            prev_loc = loc

load_gpx(gpx_url)

load the given gpx file

Source code in velorail/gpxviewer.py
59
60
61
62
63
64
65
66
def load_gpx(self, gpx_url: str):
    """
    load the given gpx file
    """
    response = requests.get(gpx_url)
    self.gpx = gpxpy.parse(response.text)
    self.get_points(self.gpx)
    return self.gpx

parse_lines(lines)

Parse the 'lines' parameter into route segments.

Parameters:

Name Type Description Default
lines str

The input string containing routes in the format: "By bike: 51.243931° N, 6.520022° E, 51.269222° N, 6.625467° E:"

required

Returns:

Name Type Description
Tour

Created tour from the parsed coordinates

Source code in velorail/gpxviewer.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
def parse_lines(self, lines: str):
    """
    Parse the 'lines' parameter into route segments.

    Args:
        lines (str): The input string containing routes in the format:
            "By bike: 51.243931° N, 6.520022° E, 51.269222° N, 6.625467° E:"

    Returns:
        Tour: Created tour from the parsed coordinates
    """
    coordinate_pattern = r"(\d+\.\d+)° ([NS]), (\d+\.\d+)° ([EW])"
    route_segments = lines.split(":")
    self.tour = Tour(name="Parsed Tour")

    for segment in route_segments:
        segment = segment.strip()

        # Extract leg type (e.g., "bike", "train")
        leg_type_match = re.match(r"By (\w+)→", segment)
        if leg_type_match:
            leg_type = leg_type_match.group(1).lower()
            pass
        else:
            leg_type = "bike"

        points = re.findall(coordinate_pattern, segment)
        if points:
            prev_loc = None
            for i, (lat, ns, lon, ew) in enumerate(points):
                lat_val = float(lat) * (-1 if ns == "S" else 1)
                lon_val = float(lon) * (-1 if ew == "W" else 1)

                loc = Loc(
                    id=str(len(self.tour.legs) + i),
                    name=None,
                    coordinates=(lat_val, lon_val),
                )

                if prev_loc:
                    leg = Leg(leg_type=leg_type, start=prev_loc, end=loc)
                    self.tour.legs.append(leg)
                prev_loc = loc

    if not self.tour.legs:
        raise ValueError("No valid routes found in the input lines.")

    return self.tour

parse_lines_and_show(lines, zoom=None)

Parse lines and display them on the map

Source code in velorail/gpxviewer.py
225
226
227
228
229
230
def parse_lines_and_show(self, lines: str, zoom: int = None):
    """
    Parse lines and display them on the map
    """
    self.parse_lines(lines)
    self.show(zoom=zoom)

set_center()

Calculate and set the center and bounding box based on tour legs

Source code in velorail/gpxviewer.py
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def set_center(self):
    """
    Calculate and set the center and bounding box based on tour legs
    """

    if self.tour and self.tour.legs:
        points = []
        for leg in self.tour.legs:
            points.append(leg.start.coordinates)
            points.append(leg.end.coordinates)
        # Wrong order: lats, lons need to be extracted after zipping
        lats = [p[0] for p in points]
        lons = [p[1] for p in points]
        self.bounding_box = (min(lats), max(lats), min(lons), max(lons))
        self.center = ((min(lats) + max(lats)) / 2, (min(lons) + max(lons)) / 2)
    else:
        self.center = self.default_center
        self.bounding_box = None
    return self.center

show(zoom=None, center=None)

Show tour with styled paths

Source code in velorail/gpxviewer.py
232
233
234
235
236
237
238
239
240
241
242
243
def show(self, zoom: int = None, center=None):
    """
    Show tour with styled paths
    """
    if zoom is None:
        zoom = self.zoom
    if center is None:
        center = self.set_center()

    self.map = LeafletMap(center=center, zoom=zoom)
    if self.tour:
        self.map.draw_tour(self.tour, self.leg_styles)

gpxviewer_server

Created on 2025-01-18

@author: wf

clean_smw_artifacts(input_str)

Remove SMW artifacts ([[SMW::on]] and [[SMW::off]]) from the input string.

Parameters:

Name Type Description Default
input_str str

Input string containing SMW artifacts.

required

Returns:

Name Type Description
str str

Cleaned string without SMW markers.

Source code in velorail/gpxviewer_server.py
27
28
29
30
31
32
33
34
35
36
37
38
def clean_smw_artifacts(input_str: str) -> str:
    """
    Remove SMW artifacts ([[SMW::on]] and [[SMW::off]]) from the input string.

    Args:
        input_str (str): Input string containing SMW artifacts.

    Returns:
        str: Cleaned string without SMW markers.
    """
    # Regex to match and remove SMW markers
    return re.sub(r"\[\[SMW::(on|off)\]\]", "", input_str)

gpx(gpx=None, auth_token=None, zoom=GPXViewer.default_zoom)

GPX viewer page with optional gpx_url and auth_token.

Source code in velorail/gpxviewer_server.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@ui.page("/")
def gpx(gpx: str = None, auth_token: str = None, zoom: int = GPXViewer.default_zoom):
    """
    GPX viewer page with optional gpx_url and auth_token.
    """
    global viewer
    if not viewer:
        ui.label("Error: Viewer not initialized")
        return

    if viewer.args.token and auth_token != viewer.args.token:
        ui.label("Error: Invalid authentication token")
        return

    gpx_to_use = gpx if gpx else viewer.args.gpx
    if gpx_to_use:
        viewer.load_gpx(gpx_to_use)
        viewer.show(zoom=zoom)
    else:
        ui.label(
            "Please provide a GPX file via 'gpx' query parameter or the command line."
        )

initialize_viewer()

Initialize the GPXViewer with parsed arguments.

Source code in velorail/gpxviewer_server.py
17
18
19
20
21
22
23
24
def initialize_viewer():
    """
    Initialize the GPXViewer with parsed arguments.
    """
    global viewer
    parser = GPXViewer.get_parser()
    args = parser.parse_args()
    viewer = GPXViewer(args=args)

lines_page(lines=None, auth_token=None, zoom=GPXViewer.default_zoom)

Endpoint to display routes based on 'lines' parameter.

Source code in velorail/gpxviewer_server.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@ui.page("/lines")
def lines_page(
    lines: str = None, auth_token: str = None, zoom: int = GPXViewer.default_zoom
):
    """
    Endpoint to display routes based on 'lines' parameter.
    """
    global viewer
    if not viewer:
        ui.label("Error: Viewer not initialized")
        return

    if viewer.args.token and auth_token != viewer.args.token:
        ui.label("Error: Invalid authentication token")
        return

    if not lines:
        ui.label("Error: No 'lines' parameter provided")
        return

    # Clean the lines parameter to remove SMW artifacts
    cleaned_lines = clean_smw_artifacts(lines)

    # Delegate logic to GPXViewer
    try:
        viewer.parse_lines_and_show(cleaned_lines, zoom=zoom)
    except ValueError as e:
        ui.label(f"Error processing lines: {e}")

main()

Entry point for gpxviewer.

Source code in velorail/gpxviewer_server.py
 95
 96
 97
 98
 99
100
def main():
    """
    Entry point for gpxviewer.
    """
    initialize_viewer()
    ui.run(port=viewer.args.port, title="GPX Viewer")

locfind

Created on 2025-02-01

@author: th

LocFinder

Set of methods to lookup different location types

Source code in velorail/locfind.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
class LocFinder:
    """
    Set of methods to lookup different location types
    """

    def __init__(self):
        """
        constructor
        """
        endpoint_path = Path(__file__).parent / "resources" / "endpoints.yaml"
        query_path = Path(__file__).parent / "resources" / "queries" / "locations.yaml"
        if not query_path.is_file():
            raise FileNotFoundError(f"LocFinder queries file not found: {query_path}")
        self.query_manager = QueryManager(
            lang="sparql", queriesPath=query_path.as_posix()
        )
        self.endpoint_manager = EndpointManager.getEndpoints(endpoint_path.as_posix())

    def query(self,query_name:str,param_dict:dict={},endpoint:str="wikidata-qlever"):
        """
        get the result of the given query
        """
        query: Query = self.query_manager.queriesByName.get(query_name)
        sparql_endpoint = self.endpoint_manager[endpoint]
        endpoint = SPARQL(sparql_endpoint.endpoint)
        qres = endpoint.queryAsListOfDicts(query.query,param_dict=param_dict)
        return qres

    def get_wikidata_geo(self, qid: str) -> WikidataGeoItem:
        """
        Get geographical coordinates and metadata for a Wikidata item

        Args:
            qid: Wikidata QID of the item

        Returns:
            WikidataGeoItem with location data and metadata
        """
        lod = self.query(query_name="WikidataGeo", param_dict={"qid": qid})
        if len(lod) >= 1:
            record = lod[0]
            record["qid"] = qid  # Add qid to record for WikidataGeoItem creation
            return WikidataGeoItem.from_record(record)
        return None

    def get_all_train_stations(self):
        lod = self.query(query_name="AllTrainStations")
        return lod


    def get_train_stations_by_coordinates(
        self, latitude: float, longitude: float, radius: float
    ):
        """
        Get all train stations within the given radius around the given latitude and longitude
        """
        lod = self.get_all_train_stations()
        df = pd.DataFrame.from_records(lod)
        # Haversine formula components
        lat1, lon1 = np.radians(latitude), np.radians(longitude)
        lat2, lon2 = np.radians(df["lat"]), np.radians(df["long"])

        # Differences in coordinates
        dlat = lat2 - lat1
        dlon = lon2 - lon1

        # Haversine formula
        a = np.sin(dlat / 2) ** 2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2) ** 2
        c = 2 * np.arcsin(np.sqrt(a))
        r = 6371  # Radius of Earth in kilometers

        # Calculate distances
        distances = c * r

        # Add distances to dataframe
        df_with_distances = df.copy()
        df_with_distances["distance_km"] = distances

        # Filter points within radius
        points_within_radius = df_with_distances[
            df_with_distances["distance_km"] <= radius
        ].copy()

        # Sort by distance
        points_within_radius = points_within_radius.sort_values("distance_km")

        return points_within_radius

__init__()

constructor

Source code in velorail/locfind.py
105
106
107
108
109
110
111
112
113
114
115
116
def __init__(self):
    """
    constructor
    """
    endpoint_path = Path(__file__).parent / "resources" / "endpoints.yaml"
    query_path = Path(__file__).parent / "resources" / "queries" / "locations.yaml"
    if not query_path.is_file():
        raise FileNotFoundError(f"LocFinder queries file not found: {query_path}")
    self.query_manager = QueryManager(
        lang="sparql", queriesPath=query_path.as_posix()
    )
    self.endpoint_manager = EndpointManager.getEndpoints(endpoint_path.as_posix())

get_train_stations_by_coordinates(latitude, longitude, radius)

Get all train stations within the given radius around the given latitude and longitude

Source code in velorail/locfind.py
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def get_train_stations_by_coordinates(
    self, latitude: float, longitude: float, radius: float
):
    """
    Get all train stations within the given radius around the given latitude and longitude
    """
    lod = self.get_all_train_stations()
    df = pd.DataFrame.from_records(lod)
    # Haversine formula components
    lat1, lon1 = np.radians(latitude), np.radians(longitude)
    lat2, lon2 = np.radians(df["lat"]), np.radians(df["long"])

    # Differences in coordinates
    dlat = lat2 - lat1
    dlon = lon2 - lon1

    # Haversine formula
    a = np.sin(dlat / 2) ** 2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2) ** 2
    c = 2 * np.arcsin(np.sqrt(a))
    r = 6371  # Radius of Earth in kilometers

    # Calculate distances
    distances = c * r

    # Add distances to dataframe
    df_with_distances = df.copy()
    df_with_distances["distance_km"] = distances

    # Filter points within radius
    points_within_radius = df_with_distances[
        df_with_distances["distance_km"] <= radius
    ].copy()

    # Sort by distance
    points_within_radius = points_within_radius.sort_values("distance_km")

    return points_within_radius

get_wikidata_geo(qid)

Get geographical coordinates and metadata for a Wikidata item

Parameters:

Name Type Description Default
qid str

Wikidata QID of the item

required

Returns:

Type Description
WikidataGeoItem

WikidataGeoItem with location data and metadata

Source code in velorail/locfind.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def get_wikidata_geo(self, qid: str) -> WikidataGeoItem:
    """
    Get geographical coordinates and metadata for a Wikidata item

    Args:
        qid: Wikidata QID of the item

    Returns:
        WikidataGeoItem with location data and metadata
    """
    lod = self.query(query_name="WikidataGeo", param_dict={"qid": qid})
    if len(lod) >= 1:
        record = lod[0]
        record["qid"] = qid  # Add qid to record for WikidataGeoItem creation
        return WikidataGeoItem.from_record(record)
    return None

query(query_name, param_dict={}, endpoint='wikidata-qlever')

get the result of the given query

Source code in velorail/locfind.py
118
119
120
121
122
123
124
125
126
def query(self,query_name:str,param_dict:dict={},endpoint:str="wikidata-qlever"):
    """
    get the result of the given query
    """
    query: Query = self.query_manager.queriesByName.get(query_name)
    sparql_endpoint = self.endpoint_manager[endpoint]
    endpoint = SPARQL(sparql_endpoint.endpoint)
    qres = endpoint.queryAsListOfDicts(query.query,param_dict=param_dict)
    return qres

WikidataGeoItem

Dataclass for storing Wikidata geographical location data with labels

Source code in velorail/locfind.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
@lod_storable
class WikidataGeoItem:
    """
    Dataclass for storing Wikidata geographical location data with labels
    """
    qid: str
    lat: float
    lon: float
    label: Optional[str]=None
    description: Optional[str]=None

    def as_wd_link(self)->Link:
        text = f"""{self.label}({self.qid})☞{self.description}"""
        wd_link = Link.create(f"https://www.wikidata.org/wiki/{self.qid}", text)
        return wd_link

    def get_map_links(self, leg_styles:Optional[LegStyles]=None, zoom:int=14) -> str:
        """
        Get HTML markup with icons grouped by map type
        """
        if leg_styles is None:
            leg_styles = LegStyles.default()

        map_links = {
            "openstreetmap.org": ["car", "bus", "plane"],
            "opencyclemap.org": ["bike"],
            "openrailwaymap.org": ["train"],
            "map.openseamap.org": ["ferry"],
            "hiking.waymarkedtrails.org": ["foot"]
        }

        markup = ""
        delim=""
        for map_url, leg_types in map_links.items():
            icons=""
            for leg_type in leg_types:
                leg_style=leg_styles.get_style(leg_type)
                icons+=leg_style.utf8_icon
            tooltip = f"{','.join(leg_types)} map"
            if "car" in leg_types:
                url = f"https://{map_url}/#map={zoom}/{self.lat}/{self.lon}"
            elif "foot" in leg_types:
                url = f"https://{map_url}/#?map={zoom}/{self.lat}/{self.lon}"
            else:
                url = f"https://{map_url}/?zoom={zoom}&lat={self.lat}&lon={self.lon}"
            link=Link.create(url, text=icons, tooltip=tooltip, target="_blank")
            markup+=link+delim
            delim="\n"
        return markup

    @property
    def osm_url(self, map_type:str= "street", zoom: int = 15) -> str:
        """
        Get OpenStreetMap URL for this location

        Args:
            zoom: Zoom level (default=15)

        Returns:
            OpenStreetMap URL for the location
        """
        osm_url=f"https://www.open{map_type}map.org/?mlat={self.lat}&mlon={self.lon}&zoom={zoom}"
        return osm_url

    @classmethod
    def from_record(cls, record: dict) -> 'WikidataGeoItem':
        """
        Create WikidataGeoItem from a dictionary record

        Args:
            record: Dictionary containing lat, lon, label and description

        Returns:
            WikidataGeoRecord instance
        """
        return cls(
            qid=record["qid"],
            lat=float(record["lat"]),
            lon=float(record["lon"]),
            label=record["label"],
            description=record["description"]
        )

osm_url: str property

Get OpenStreetMap URL for this location

Parameters:

Name Type Description Default
zoom

Zoom level (default=15)

required

Returns:

Type Description
str

OpenStreetMap URL for the location

from_record(record) classmethod

Create WikidataGeoItem from a dictionary record

Parameters:

Name Type Description Default
record dict

Dictionary containing lat, lon, label and description

required

Returns:

Type Description
WikidataGeoItem

WikidataGeoRecord instance

Source code in velorail/locfind.py
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
@classmethod
def from_record(cls, record: dict) -> 'WikidataGeoItem':
    """
    Create WikidataGeoItem from a dictionary record

    Args:
        record: Dictionary containing lat, lon, label and description

    Returns:
        WikidataGeoRecord instance
    """
    return cls(
        qid=record["qid"],
        lat=float(record["lat"]),
        lon=float(record["lon"]),
        label=record["label"],
        description=record["description"]
    )

Get HTML markup with icons grouped by map type

Source code in velorail/locfind.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def get_map_links(self, leg_styles:Optional[LegStyles]=None, zoom:int=14) -> str:
    """
    Get HTML markup with icons grouped by map type
    """
    if leg_styles is None:
        leg_styles = LegStyles.default()

    map_links = {
        "openstreetmap.org": ["car", "bus", "plane"],
        "opencyclemap.org": ["bike"],
        "openrailwaymap.org": ["train"],
        "map.openseamap.org": ["ferry"],
        "hiking.waymarkedtrails.org": ["foot"]
    }

    markup = ""
    delim=""
    for map_url, leg_types in map_links.items():
        icons=""
        for leg_type in leg_types:
            leg_style=leg_styles.get_style(leg_type)
            icons+=leg_style.utf8_icon
        tooltip = f"{','.join(leg_types)} map"
        if "car" in leg_types:
            url = f"https://{map_url}/#map={zoom}/{self.lat}/{self.lon}"
        elif "foot" in leg_types:
            url = f"https://{map_url}/#?map={zoom}/{self.lat}/{self.lon}"
        else:
            url = f"https://{map_url}/?zoom={zoom}&lat={self.lat}&lon={self.lon}"
        link=Link.create(url, text=icons, tooltip=tooltip, target="_blank")
        markup+=link+delim
        delim="\n"
    return markup

tour

Created on 2025-01-19

@author: wf

Tour definition as Legs connecting Locs Styling of Legs

Leg

A segment of a trip between two locations

Source code in velorail/tour.py
31
32
33
34
35
36
37
38
39
40
@lod_storable
class Leg:
    """
    A segment of a trip between two locations
    """

    leg_type: str  # e.g., "bike", "train", "car"
    start: Loc
    end: Loc
    url: Optional[str] = None

LegStyle

Style configuration for a transport leg

Source code in velorail/tour.py
81
82
83
84
85
86
87
88
89
90
91
92
93
@lod_storable
class LegStyle:
    """
    Style configuration for a transport leg
    """

    leg_type: str  # e.g. "bike", "train", "car", "ferry", "bus", "plane"
    point_type: str
    color: str
    utf8_icon: str
    weight: int
    dashArray: Optional[str]
    opacity: float

LegStyles

Collection of predefined styles for different leg types

Source code in velorail/tour.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
@lod_storable
class LegStyles:
    """
    Collection of predefined styles for different leg types
    """

    styles: Dict[str, LegStyle] = field(default_factory=dict)

    def get_style(self, leg_type: str) -> LegStyle:
        """
        Get style for given leg type
        """
        leg_style = self.styles.get(leg_type)
        return leg_style

    @classmethod
    def default(cls) -> "LegStyles":
        """
        Get default leg styles
        """
        default_styles = {
            "bike": LegStyle(
                leg_type="bike",
                point_type="knooppunt",
                color="#FF0000",  # bike - red (avoid green due to map background)
                utf8_icon="🚲",
                weight=3,
                dashArray=None,
                opacity=1.0,
            ),
            "train": LegStyle(
                leg_type="train",
                point_type="train_station",
                color="#555555",  # train - dark gray (improves contrast over black)
                utf8_icon="🚂",
                weight=3,
                dashArray="10,10",
                opacity=1.0,
            ),
            "car": LegStyle(
                leg_type="car",
                point_type="parking",
                color="#404040",  # car - medium gray
                utf8_icon="🚗",
                weight=3,
                dashArray=None,
                opacity=1.0,
            ),
            "ferry": LegStyle(
                leg_type="ferry",
                point_type="ferry_terminal",
                color="#1E90FF",  # ferry - dodger blue for visibility
                utf8_icon="⛴️",
                weight=3,
                dashArray="15,10",
                opacity=0.8,
            ),
            "foot": LegStyle(
               leg_type="foot",
               point_type="waypoint",
               color="#FFD700", #  yellow
               utf8_icon="👣",
               weight=2,
               dashArray="5,5",
               opacity=0.9,
            ),
            "bus": LegStyle(
                leg_type="bus",
                point_type="bus_stop",
                color="#FF4500",  # bus - orange-red for distinctiveness
                utf8_icon="🚌",
                weight=3,
                dashArray=None,
                opacity=1.0,
            ),
            "plane": LegStyle(
                leg_type="plane",
                point_type="airport",
                color="#4B0082",  # plane - indigo for uniqueness
                utf8_icon="✈️",
                weight=3,
                dashArray="20,10,5,10",
                opacity=0.7,
            ),
        }
        return cls(styles=default_styles)

default() classmethod

Get default leg styles

Source code in velorail/tour.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
@classmethod
def default(cls) -> "LegStyles":
    """
    Get default leg styles
    """
    default_styles = {
        "bike": LegStyle(
            leg_type="bike",
            point_type="knooppunt",
            color="#FF0000",  # bike - red (avoid green due to map background)
            utf8_icon="🚲",
            weight=3,
            dashArray=None,
            opacity=1.0,
        ),
        "train": LegStyle(
            leg_type="train",
            point_type="train_station",
            color="#555555",  # train - dark gray (improves contrast over black)
            utf8_icon="🚂",
            weight=3,
            dashArray="10,10",
            opacity=1.0,
        ),
        "car": LegStyle(
            leg_type="car",
            point_type="parking",
            color="#404040",  # car - medium gray
            utf8_icon="🚗",
            weight=3,
            dashArray=None,
            opacity=1.0,
        ),
        "ferry": LegStyle(
            leg_type="ferry",
            point_type="ferry_terminal",
            color="#1E90FF",  # ferry - dodger blue for visibility
            utf8_icon="⛴️",
            weight=3,
            dashArray="15,10",
            opacity=0.8,
        ),
        "foot": LegStyle(
           leg_type="foot",
           point_type="waypoint",
           color="#FFD700", #  yellow
           utf8_icon="👣",
           weight=2,
           dashArray="5,5",
           opacity=0.9,
        ),
        "bus": LegStyle(
            leg_type="bus",
            point_type="bus_stop",
            color="#FF4500",  # bus - orange-red for distinctiveness
            utf8_icon="🚌",
            weight=3,
            dashArray=None,
            opacity=1.0,
        ),
        "plane": LegStyle(
            leg_type="plane",
            point_type="airport",
            color="#4B0082",  # plane - indigo for uniqueness
            utf8_icon="✈️",
            weight=3,
            dashArray="20,10,5,10",
            opacity=0.7,
        ),
    }
    return cls(styles=default_styles)

get_style(leg_type)

Get style for given leg type

Source code in velorail/tour.py
104
105
106
107
108
109
def get_style(self, leg_type: str) -> LegStyle:
    """
    Get style for given leg type
    """
    leg_style = self.styles.get(leg_type)
    return leg_style

Loc

A location in a trip or tour

Source code in velorail/tour.py
17
18
19
20
21
22
23
24
25
26
27
28
@lod_storable
class Loc:
    """
    A location in a trip or tour
    """

    id: str
    coordinates: Tuple[float, float]  # latlon
    name: str
    loc_type: Optional[str] = None
    url: Optional[str] = None
    notes: Optional[str] = None

Tour

A sequence of legs connecting waypoints that form a complete journey

Source code in velorail/tour.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@lod_storable
class Tour:
    """
    A sequence of legs connecting waypoints that form a complete journey
    """

    name: str
    legs: list[Leg] = field(default_factory=list)
    description: Optional[str] = None
    url: Optional[str] = None

    def dump(self, limit: int = 10, leg_styles: "LegStyles" = None):
        """
        Print a detailed dump of the tour for debugging

        Args:
           limit: Maximum number of legs to show
        """
        if leg_styles is None:
            leg_styles = LegStyles.default()
        print(f"Tour: {self.name}")
        for i, leg in enumerate(self.legs):
            if i >= limit:
                remaining = len(self.legs) - limit
                print(f"... {remaining} more legs")
                break
            leg_style = leg_styles.get_style(leg.leg_type)
            if leg_style:
                utf8_icon = leg_style.utf8_icon
            else:
                utf8_icon = "?"
            coord_start = (
                f"({leg.start.coordinates[0]:.5f}, {leg.start.coordinates[1]:.5f})"
            )
            coord_end = f"({leg.end.coordinates[0]:.5f}, {leg.end.coordinates[1]:.5f})"
            print(f" {utf8_icon} {coord_start}{coord_end}")

dump(limit=10, leg_styles=None)

Print a detailed dump of the tour for debugging

Parameters:

Name Type Description Default
limit int

Maximum number of legs to show

10
Source code in velorail/tour.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def dump(self, limit: int = 10, leg_styles: "LegStyles" = None):
    """
    Print a detailed dump of the tour for debugging

    Args:
       limit: Maximum number of legs to show
    """
    if leg_styles is None:
        leg_styles = LegStyles.default()
    print(f"Tour: {self.name}")
    for i, leg in enumerate(self.legs):
        if i >= limit:
            remaining = len(self.legs) - limit
            print(f"... {remaining} more legs")
            break
        leg_style = leg_styles.get_style(leg.leg_type)
        if leg_style:
            utf8_icon = leg_style.utf8_icon
        else:
            utf8_icon = "?"
        coord_start = (
            f"({leg.start.coordinates[0]:.5f}, {leg.start.coordinates[1]:.5f})"
        )
        coord_end = f"({leg.end.coordinates[0]:.5f}, {leg.end.coordinates[1]:.5f})"
        print(f" {utf8_icon} {coord_start}{coord_end}")

velorail_cmd

Created on 2025-02-01

@author: wf

VeloRailCmd

Bases: WebserverCmd

command line handling for velorail

Source code in velorail/velorail_cmd.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class VeloRailCmd(WebserverCmd):
    """
    command line handling for velorail
    """

    def __init__(self):
        """
        constructor
        """
        config = VeloRailWebServer.get_config()
        WebserverCmd.__init__(self, config, VeloRailWebServer)
        pass

    def getArgParser(self, description: str, version_msg) -> ArgumentParser:
        """
        override the default argparser call
        """
        parser = super().getArgParser(description, version_msg)
        parser.add_argument(
            "-en",
            "--endpointName",
            default="wikidata",
            help=f"Name of the endpoint to use for queries. Available by default: {EndpointManager.getEndpointNames(lang='sparql')}",
        )
        parser.add_argument(
            "-v",
            "--verbose",
            action="store_true",
            help="show verbose output [default: %(default)s]",
        )
        parser.add_argument(
            "-rp",
            "--root_path",
            default=VeloRailWebServer.examples_path(),
            help="path to velorail files [default: %(default)s]",
        )
        parser.add_argument("--gpx", required=False, help="URL or path to GPX file")
        parser.add_argument(
            "--token", required=False, help="Authentication token for GPX access"
        )
        parser.add_argument(
            "--zoom",
            type=int,
            default=GPXViewer.default_zoom,
            help="zoom level (default: 11)",
        )
        parser.add_argument(
            "--center",
            nargs=2,
            type=float,
            default=GPXViewer.default_center,
            help="center lat,lon - default: Greenwich",
        )
        return parser

__init__()

constructor

Source code in velorail/velorail_cmd.py
22
23
24
25
26
27
28
def __init__(self):
    """
    constructor
    """
    config = VeloRailWebServer.get_config()
    WebserverCmd.__init__(self, config, VeloRailWebServer)
    pass

getArgParser(description, version_msg)

override the default argparser call

Source code in velorail/velorail_cmd.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def getArgParser(self, description: str, version_msg) -> ArgumentParser:
    """
    override the default argparser call
    """
    parser = super().getArgParser(description, version_msg)
    parser.add_argument(
        "-en",
        "--endpointName",
        default="wikidata",
        help=f"Name of the endpoint to use for queries. Available by default: {EndpointManager.getEndpointNames(lang='sparql')}",
    )
    parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="show verbose output [default: %(default)s]",
    )
    parser.add_argument(
        "-rp",
        "--root_path",
        default=VeloRailWebServer.examples_path(),
        help="path to velorail files [default: %(default)s]",
    )
    parser.add_argument("--gpx", required=False, help="URL or path to GPX file")
    parser.add_argument(
        "--token", required=False, help="Authentication token for GPX access"
    )
    parser.add_argument(
        "--zoom",
        type=int,
        default=GPXViewer.default_zoom,
        help="zoom level (default: 11)",
    )
    parser.add_argument(
        "--center",
        nargs=2,
        type=float,
        default=GPXViewer.default_center,
        help="center lat,lon - default: Greenwich",
    )
    return parser

main(argv=None)

main call

Source code in velorail/velorail_cmd.py
73
74
75
76
77
78
79
def main(argv: list = None):
    """
    main call
    """
    cmd = VeloRailCmd()
    exit_code = cmd.cmd_main(argv)
    return exit_code

version

Created on 2025-02-01

@author: wf

Version dataclass

Version handling for velorail

Source code in velorail/version.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@dataclass
class Version:
    """
    Version handling for velorail
    """

    name = "velorail"
    version = velorail.__version__
    date = "2025-02-01"
    updated = "2025-02-01"
    description = "Multimodal bike and train route planning support"

    authors = "Wolfgang Fahl"

    doc_url = "https://wiki.bitplan.com/index.php/velorail"
    chat_url = "https://github.com/WolfgangFahl/velorail/discussions"
    cm_url = "https://github.com/WolfgangFahl/velorail"

    license = f"""Copyright 2025 contributors. All rights reserved.

  Licensed under the Apache License 2.0
  http://www.apache.org/licenses/LICENSE-2.0

  Distributed on an "AS IS" basis without warranties
  or conditions of any kind, either express or implied."""

    longDescription = f"""{name} version {version}
{description}

  Created by {authors} on {date} last updated {updated}"""

Created on 2024-01-03

@author: wf

WikidataItemSearch

wikidata item search

Source code in velorail/wditem_search.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
class WikidataItemSearch:
    """
    wikidata item search
    """

    def __init__(
        self, solution: WebSolution, record_filter: Callable = None, lang: str = "en"
    ):
        """
        Initialize the WikidataItemSearch with the given solution.

        Args:
            solution (WebSolution): The solution to attach the search UI.
            record_filter(Callable): callback for displayed found records
        """
        self.solution = solution
        self.lang = lang
        # Get available languages
        self.languages = Lang.get_language_dict()
        self.record_filter = record_filter
        self.limit = 9
        self.wd_search = WikidataSearch(lang)
        self.search_debounce_task = None
        self.keyStrokeTime = 0.65  # minimum time in seconds to wait between keystrokes before starting searching
        self.search_result_row = None
        self.setup()

    def setup(self):
        """
        setup the user interface
        """
        with ui.card().style("width: 25%"):
            with ui.grid(rows=1, columns=4):
                # Create a label to display the chosen language with the default language
                self.lang_label = ui.label(self.lang)
                # Create a dropdown for language selection with the default language selected
                # Bind the label text to the selection's value, so it updates automatically
                ui.select(self.languages, value=self.lang).bind_value(
                    self.lang_label, "text"
                )
                ui.label("limit:")
                self.limit_slider = (
                    ui.slider(min=2, max=50, value=self.limit)
                    .props("label-always")
                    .bind_value(self, "limit")
                )
            with ui.row():
                self.search_input = ui.input(
                    label="search", on_change=self.on_search_change
                ).props("size=80")
        with ui.row() as self.search_result_row:
            self.search_result_grid = ListOfDictsGrid()

    async def on_search_change(self, _args):
        """
        react on changes in the search input
        """
        # Cancel the existing search task if it's still waiting
        if self.search_debounce_task:
            self.search_debounce_task.cancel()

        # Create a new task for the new search
        self.search_debounce_task = asyncio.create_task(self.debounced_search())

    async def debounced_search(self):
        """
        Waits for a period of inactivity and then performs the search.
        """
        try:
            # Wait for the debounce period (keyStrokeTime)
            await asyncio.sleep(self.keyStrokeTime)
            search_for = self.search_input.value
            if self.search_result_row:
                with self.search_result_row:
                    lang = self.lang_label.text
                    ui.notify(f"searching wikidata for {search_for} ({lang})...")
                    self.wd_search.language = lang
                    wd_search_result = self.wd_search.searchOptions(
                        search_for, limit=self.limit
                    )
                    view_lod = self.get_selection_view_lod(wd_search_result)
                    self.search_result_grid.load_lod(view_lod)
                    # self.search_result_grid.set_checkbox_selection("#")
                    self.search_result_grid.update()
        except asyncio.CancelledError:
            # The search was cancelled because of new input, so just quietly exit
            pass
        except BaseException as ex:
            self.solution.handle_exception(ex)

    def get_selection_view_lod(self, wd_search_result: list) -> dict:
        """
        Convert the Wikidata search result list of dict to a selection.

        Args:
            wd_search_result (List[Dict[str, Any]]): The search results from Wikidata.

        Returns:
            List[Dict[str, Any]]: The list of dictionaries formatted for view.
        """
        view_lod = []
        for qid, itemLabel, desc in wd_search_result:
            url = f"https://www.wikidata.org/wiki/{qid}"
            link = Link.create(url, qid)
            row = {
                "#": len(view_lod) + 1,
                "qid": link,
                "label": itemLabel,
                "desc": desc,
            }
            if self.record_filter:
                self.record_filter(qid, row)
            view_lod.append(row)
        return view_lod

__init__(solution, record_filter=None, lang='en')

Initialize the WikidataItemSearch with the given solution.

Parameters:

Name Type Description Default
solution WebSolution

The solution to attach the search UI.

required
record_filter(Callable)

callback for displayed found records

required
Source code in velorail/wditem_search.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def __init__(
    self, solution: WebSolution, record_filter: Callable = None, lang: str = "en"
):
    """
    Initialize the WikidataItemSearch with the given solution.

    Args:
        solution (WebSolution): The solution to attach the search UI.
        record_filter(Callable): callback for displayed found records
    """
    self.solution = solution
    self.lang = lang
    # Get available languages
    self.languages = Lang.get_language_dict()
    self.record_filter = record_filter
    self.limit = 9
    self.wd_search = WikidataSearch(lang)
    self.search_debounce_task = None
    self.keyStrokeTime = 0.65  # minimum time in seconds to wait between keystrokes before starting searching
    self.search_result_row = None
    self.setup()

Waits for a period of inactivity and then performs the search.

Source code in velorail/wditem_search.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
async def debounced_search(self):
    """
    Waits for a period of inactivity and then performs the search.
    """
    try:
        # Wait for the debounce period (keyStrokeTime)
        await asyncio.sleep(self.keyStrokeTime)
        search_for = self.search_input.value
        if self.search_result_row:
            with self.search_result_row:
                lang = self.lang_label.text
                ui.notify(f"searching wikidata for {search_for} ({lang})...")
                self.wd_search.language = lang
                wd_search_result = self.wd_search.searchOptions(
                    search_for, limit=self.limit
                )
                view_lod = self.get_selection_view_lod(wd_search_result)
                self.search_result_grid.load_lod(view_lod)
                # self.search_result_grid.set_checkbox_selection("#")
                self.search_result_grid.update()
    except asyncio.CancelledError:
        # The search was cancelled because of new input, so just quietly exit
        pass
    except BaseException as ex:
        self.solution.handle_exception(ex)

get_selection_view_lod(wd_search_result)

Convert the Wikidata search result list of dict to a selection.

Parameters:

Name Type Description Default
wd_search_result List[Dict[str, Any]]

The search results from Wikidata.

required

Returns:

Type Description
dict

List[Dict[str, Any]]: The list of dictionaries formatted for view.

Source code in velorail/wditem_search.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
def get_selection_view_lod(self, wd_search_result: list) -> dict:
    """
    Convert the Wikidata search result list of dict to a selection.

    Args:
        wd_search_result (List[Dict[str, Any]]): The search results from Wikidata.

    Returns:
        List[Dict[str, Any]]: The list of dictionaries formatted for view.
    """
    view_lod = []
    for qid, itemLabel, desc in wd_search_result:
        url = f"https://www.wikidata.org/wiki/{qid}"
        link = Link.create(url, qid)
        row = {
            "#": len(view_lod) + 1,
            "qid": link,
            "label": itemLabel,
            "desc": desc,
        }
        if self.record_filter:
            self.record_filter(qid, row)
        view_lod.append(row)
    return view_lod

on_search_change(_args) async

react on changes in the search input

Source code in velorail/wditem_search.py
69
70
71
72
73
74
75
76
77
78
async def on_search_change(self, _args):
    """
    react on changes in the search input
    """
    # Cancel the existing search task if it's still waiting
    if self.search_debounce_task:
        self.search_debounce_task.cancel()

    # Create a new task for the new search
    self.search_debounce_task = asyncio.create_task(self.debounced_search())

setup()

setup the user interface

Source code in velorail/wditem_search.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def setup(self):
    """
    setup the user interface
    """
    with ui.card().style("width: 25%"):
        with ui.grid(rows=1, columns=4):
            # Create a label to display the chosen language with the default language
            self.lang_label = ui.label(self.lang)
            # Create a dropdown for language selection with the default language selected
            # Bind the label text to the selection's value, so it updates automatically
            ui.select(self.languages, value=self.lang).bind_value(
                self.lang_label, "text"
            )
            ui.label("limit:")
            self.limit_slider = (
                ui.slider(min=2, max=50, value=self.limit)
                .props("label-always")
                .bind_value(self, "limit")
            )
        with ui.row():
            self.search_input = ui.input(
                label="search", on_change=self.on_search_change
            ).props("size=80")
    with ui.row() as self.search_result_row:
        self.search_result_grid = ListOfDictsGrid()

webserver

Created on 2025-02-01

@author: wf

VeloRailSolution

Bases: InputWebSolution

the VeloRail solution

Source code in velorail/webserver.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
class VeloRailSolution(InputWebSolution):
    """
    the VeloRail solution
    """

    def __init__(self, webserver: "VeloRailWebServer", client: Client):
        """
        Initialize the solution

        Calls the constructor of the base solution
        Args:
            webserver (VeloRailWebServer): The webserver instance associated with this context.
            client (Client): The client instance this context is associated with.
        """
        super().__init__(webserver, client)  # Call to the superclass constructor
        self.args = self.webserver.args
        self.viewer = GPXViewer(args=self.args)

    def clean_smw_artifacts(self, input_str: str) -> str:
        """
        Remove SMW artifacts ([[SMW::on]] and [[SMW::off]]) from the input string.

        Args:
            input_str (str): Input string containing SMW artifacts.

        Returns:
            str: Cleaned string without SMW markers.
        """
        # Regex to match and remove SMW markers
        return re.sub(r"\[\[SMW::(on|off)\]\]", "", input_str)

    async def show_wikidata_item(
        self,
        qid: str = None
    ):
        """
        show the given wikidata item on the map
        Args:
            qid(str): the Wikidata id of the item to analyze
        """
        def show():
            viewer=self.viewer
            # Create LocFinder and get coordinates
            locfinder = LocFinder()
            center=None
            wd_item = locfinder.get_wikidata_geo(qid)
            if wd_item:
                wd_link=wd_item.as_wd_link()
                wd_maps=wd_item.get_map_links(zoom=self.viewer.zoom)
                # create markup with links
                markup=f"{wd_link}&nbsp;{wd_maps}"
                ui.html(markup)
                center = [wd_item.lat, wd_item.lon]
            viewer.show(center=center)

        await self.setup_content_div(show)

    async def show_lines(
        self,
        lines: str = None,
        auth_token: str = None,
        zoom: int = GPXViewer.default_zoom,
    ):
        """
        Endpoint to display routes based on 'lines' parameter.
        """
        if not self.viewer:
            ui.label("Error: Viewer not initialized")
            return

        if self.viewer.args.token and auth_token != self.viewer.args.token:
            ui.label("Error: Invalid authentication token")
            return

        if not lines:
            ui.label("Error: No 'lines' parameter provided")
            return

        # Clean the lines parameter to remove SMW artifacts
        cleaned_lines = self.clean_smw_artifacts(lines)

        # Delegate logic to GPXViewer
        try:
            self.viewer.parse_lines_and_show(cleaned_lines, zoom=zoom)
        except ValueError as e:
            ui.label(f"Error processing lines: {e}")

    async def show_gpx(
        self,
        gpx: str = None,
        auth_token: str = None,
        zoom: int = GPXViewer.default_zoom,
    ):
        """
        GPX viewer page with optional gpx_url and auth_token.
        """
        viewer = self.viewer
        if not viewer:
            ui.label("Error: Viewer not initialized")
            return

        if viewer.args.token and auth_token != viewer.args.token:
            ui.label("Error: Invalid authentication token")
            return

        gpx_to_use = gpx if gpx else viewer.args.gpx
        if gpx_to_use:
            viewer.load_gpx(gpx_to_use)
            viewer.show(zoom=zoom)
        else:
            ui.label(
                "Please provide a GPX file via 'gpx' query parameter or the command line."
            )

    def prepare_ui(self):
        """
        overrideable configuration
        """
        self.endpoint_name = self.args.endpointName
        self.lang = "en"  # FIXME make configurable

    async def home(self):
        """
        provide the main content page
        """

        def record_filter(qid: str, record: dict):
            """
            filter the given search record
            """
            if "label" and "desc" in record:
                desc=record["desc"]
                label=record["label"]
                text = f"""{label}({qid})☞{desc}"""
                map_link = Link.create(f"/wd/{qid}", text)
                # getting the link to be at second position
                # is a bit tricky
                temp_items = list(record.items())
                # Add the new item in the second position
                temp_items.insert(1, ("map", map_link))

                # Clear the original dictionary and update it with the new order of items
                record.clear()
                record.update(temp_items)

        def show():
            self.wd_item_search = WikidataItemSearch(
                self, record_filter=record_filter, lang=self.lang
            )

        await self.setup_content_div(show)

__init__(webserver, client)

Initialize the solution

Calls the constructor of the base solution Args: webserver (VeloRailWebServer): The webserver instance associated with this context. client (Client): The client instance this context is associated with.

Source code in velorail/webserver.py
25
26
27
28
29
30
31
32
33
34
35
36
def __init__(self, webserver: "VeloRailWebServer", client: Client):
    """
    Initialize the solution

    Calls the constructor of the base solution
    Args:
        webserver (VeloRailWebServer): The webserver instance associated with this context.
        client (Client): The client instance this context is associated with.
    """
    super().__init__(webserver, client)  # Call to the superclass constructor
    self.args = self.webserver.args
    self.viewer = GPXViewer(args=self.args)

clean_smw_artifacts(input_str)

Remove SMW artifacts ([[SMW::on]] and [[SMW::off]]) from the input string.

Parameters:

Name Type Description Default
input_str str

Input string containing SMW artifacts.

required

Returns:

Name Type Description
str str

Cleaned string without SMW markers.

Source code in velorail/webserver.py
38
39
40
41
42
43
44
45
46
47
48
49
def clean_smw_artifacts(self, input_str: str) -> str:
    """
    Remove SMW artifacts ([[SMW::on]] and [[SMW::off]]) from the input string.

    Args:
        input_str (str): Input string containing SMW artifacts.

    Returns:
        str: Cleaned string without SMW markers.
    """
    # Regex to match and remove SMW markers
    return re.sub(r"\[\[SMW::(on|off)\]\]", "", input_str)

home() async

provide the main content page

Source code in velorail/webserver.py
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
async def home(self):
    """
    provide the main content page
    """

    def record_filter(qid: str, record: dict):
        """
        filter the given search record
        """
        if "label" and "desc" in record:
            desc=record["desc"]
            label=record["label"]
            text = f"""{label}({qid})☞{desc}"""
            map_link = Link.create(f"/wd/{qid}", text)
            # getting the link to be at second position
            # is a bit tricky
            temp_items = list(record.items())
            # Add the new item in the second position
            temp_items.insert(1, ("map", map_link))

            # Clear the original dictionary and update it with the new order of items
            record.clear()
            record.update(temp_items)

    def show():
        self.wd_item_search = WikidataItemSearch(
            self, record_filter=record_filter, lang=self.lang
        )

    await self.setup_content_div(show)

prepare_ui()

overrideable configuration

Source code in velorail/webserver.py
134
135
136
137
138
139
def prepare_ui(self):
    """
    overrideable configuration
    """
    self.endpoint_name = self.args.endpointName
    self.lang = "en"  # FIXME make configurable

show_gpx(gpx=None, auth_token=None, zoom=GPXViewer.default_zoom) async

GPX viewer page with optional gpx_url and auth_token.

Source code in velorail/webserver.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
async def show_gpx(
    self,
    gpx: str = None,
    auth_token: str = None,
    zoom: int = GPXViewer.default_zoom,
):
    """
    GPX viewer page with optional gpx_url and auth_token.
    """
    viewer = self.viewer
    if not viewer:
        ui.label("Error: Viewer not initialized")
        return

    if viewer.args.token and auth_token != viewer.args.token:
        ui.label("Error: Invalid authentication token")
        return

    gpx_to_use = gpx if gpx else viewer.args.gpx
    if gpx_to_use:
        viewer.load_gpx(gpx_to_use)
        viewer.show(zoom=zoom)
    else:
        ui.label(
            "Please provide a GPX file via 'gpx' query parameter or the command line."
        )

show_lines(lines=None, auth_token=None, zoom=GPXViewer.default_zoom) async

Endpoint to display routes based on 'lines' parameter.

Source code in velorail/webserver.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
async def show_lines(
    self,
    lines: str = None,
    auth_token: str = None,
    zoom: int = GPXViewer.default_zoom,
):
    """
    Endpoint to display routes based on 'lines' parameter.
    """
    if not self.viewer:
        ui.label("Error: Viewer not initialized")
        return

    if self.viewer.args.token and auth_token != self.viewer.args.token:
        ui.label("Error: Invalid authentication token")
        return

    if not lines:
        ui.label("Error: No 'lines' parameter provided")
        return

    # Clean the lines parameter to remove SMW artifacts
    cleaned_lines = self.clean_smw_artifacts(lines)

    # Delegate logic to GPXViewer
    try:
        self.viewer.parse_lines_and_show(cleaned_lines, zoom=zoom)
    except ValueError as e:
        ui.label(f"Error processing lines: {e}")

show_wikidata_item(qid=None) async

show the given wikidata item on the map Args: qid(str): the Wikidata id of the item to analyze

Source code in velorail/webserver.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
async def show_wikidata_item(
    self,
    qid: str = None
):
    """
    show the given wikidata item on the map
    Args:
        qid(str): the Wikidata id of the item to analyze
    """
    def show():
        viewer=self.viewer
        # Create LocFinder and get coordinates
        locfinder = LocFinder()
        center=None
        wd_item = locfinder.get_wikidata_geo(qid)
        if wd_item:
            wd_link=wd_item.as_wd_link()
            wd_maps=wd_item.get_map_links(zoom=self.viewer.zoom)
            # create markup with links
            markup=f"{wd_link}&nbsp;{wd_maps}"
            ui.html(markup)
            center = [wd_item.lat, wd_item.lon]
        viewer.show(center=center)

    await self.setup_content_div(show)

VeloRailWebServer

Bases: InputWebserver

WebServer class that manages the server for velorail

Source code in velorail/webserver.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
class VeloRailWebServer(InputWebserver):
    """WebServer class that manages the server for velorail"""

    @classmethod
    def get_config(cls) -> WebserverConfig:
        copy_right = "(c)2025 velorail team"
        config = WebserverConfig(
            copy_right=copy_right,
            version=Version(),
            default_port=9876,
            short_name="velorail",
        )
        server_config = WebserverConfig.get(config)
        server_config.solution_class = VeloRailSolution
        return server_config

    def __init__(self):
        """Constructs all the necessary attributes for the WebServer object."""
        InputWebserver.__init__(self, config=VeloRailWebServer.get_config())

        @ui.page("/wd/{qid}")
        async def wikidata_item_page(client: Client, qid: str):
            """
            show the given wikidata item on the map
            """
            await self.page(
                client,VeloRailSolution.show_wikidata_item, qid
            )

        @ui.page("/lines")
        async def lines_page(
            client: Client,
            lines: str = None,
            auth_token: str = None,
            zoom: int = GPXViewer.default_zoom,
        ):
            """
            Endpoint to display routes based on 'lines' parameter.
            """
            await self.page(
                client, VeloRailSolution.show_lines, lines, auth_token, zoom
            )

        @ui.page("/gpx")
        async def gpx_page(
            client: Client,
            gpx: str = None,
            auth_token: str = None,
            zoom: int = GPXViewer.default_zoom,
        ):
            """
            GPX viewer page with optional gpx_url and auth_token.
            """
            await self.page(client, VeloRailSolution.show_gpx, gpx, auth_token, zoom)

    def configure_run(self):
        root_path = (
            self.args.root_path
            if self.args.root_path
            else VeloRailWebServer.examples_path()
        )
        self.root_path = os.path.abspath(root_path)
        self.allowed_urls = [
            "https://raw.githubusercontent.com/WolfgangFahl/velorail/main/velorail_examples/",
            self.examples_path(),
            self.root_path,
        ]

    @classmethod
    def examples_path(cls) -> str:
        # the root directory (default: examples)
        path = os.path.join(os.path.dirname(__file__), "../velorail_examples")
        path = os.path.abspath(path)
        return path

__init__()

Constructs all the necessary attributes for the WebServer object.

Source code in velorail/webserver.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
def __init__(self):
    """Constructs all the necessary attributes for the WebServer object."""
    InputWebserver.__init__(self, config=VeloRailWebServer.get_config())

    @ui.page("/wd/{qid}")
    async def wikidata_item_page(client: Client, qid: str):
        """
        show the given wikidata item on the map
        """
        await self.page(
            client,VeloRailSolution.show_wikidata_item, qid
        )

    @ui.page("/lines")
    async def lines_page(
        client: Client,
        lines: str = None,
        auth_token: str = None,
        zoom: int = GPXViewer.default_zoom,
    ):
        """
        Endpoint to display routes based on 'lines' parameter.
        """
        await self.page(
            client, VeloRailSolution.show_lines, lines, auth_token, zoom
        )

    @ui.page("/gpx")
    async def gpx_page(
        client: Client,
        gpx: str = None,
        auth_token: str = None,
        zoom: int = GPXViewer.default_zoom,
    ):
        """
        GPX viewer page with optional gpx_url and auth_token.
        """
        await self.page(client, VeloRailSolution.show_gpx, gpx, auth_token, zoom)