routx 1.0.2__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
routx/__init__.py ADDED
@@ -0,0 +1,41 @@
1
+ # (c) Copyright 2025 Mikołaj Kuranowski
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from .wrapper import (
5
+ DEFAULT_STEP_LIMIT,
6
+ Edge,
7
+ Graph,
8
+ KDTree,
9
+ Node,
10
+ OsmCustomProfile,
11
+ OsmFormat,
12
+ OsmLoadingError,
13
+ OsmPenalty,
14
+ OsmProfile,
15
+ StepLimitExceeded,
16
+ earth_distance,
17
+ )
18
+
19
+ __all__ = [
20
+ "DEFAULT_STEP_LIMIT",
21
+ "Edge",
22
+ "Graph",
23
+ "KDTree",
24
+ "Node",
25
+ "OsmCustomProfile",
26
+ "OsmFormat",
27
+ "OsmLoadingError",
28
+ "OsmPenalty",
29
+ "OsmProfile",
30
+ "StepLimitExceeded",
31
+ "earth_distance",
32
+ ]
33
+
34
+ __title__ = "routx"
35
+ __description__ = "Simple routing over OpenStreetMap data "
36
+ __url__ = "https://github.com/MKuranowski/routx-python"
37
+ __author__ = "Mikołaj Kuranowski"
38
+ __copyright__ = "© Copyright 2025 Mikołaj Kuranowski"
39
+ __license__ = "MIT"
40
+ __version__ = "1.0.2"
41
+ __email__ = "mkuranowski+pypackages@gmail.com"
routx/wrapper.py ADDED
@@ -0,0 +1,873 @@
1
+ # (c) Copyright 2025 Mikołaj Kuranowski
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ import logging
5
+ import sys
6
+ from collections.abc import Iterator, MutableMapping
7
+ from ctypes import (
8
+ CFUNCTYPE,
9
+ POINTER,
10
+ Structure,
11
+ Union,
12
+ byref,
13
+ c_bool,
14
+ c_char,
15
+ c_char_p,
16
+ c_float,
17
+ c_int,
18
+ c_int64,
19
+ c_size_t,
20
+ c_uint32,
21
+ c_void_p,
22
+ )
23
+ from ctypes import cast as c_cast
24
+ from ctypes import cdll, pointer
25
+ from dataclasses import dataclass
26
+ from enum import IntEnum
27
+ from os import PathLike
28
+ from pathlib import Path
29
+ from typing import Any, Final, NamedTuple
30
+
31
+ from typing_extensions import Self
32
+
33
+ # Figure out where the shared library is and get a handle to it
34
+
35
+ if sys.platform.startswith("win32"):
36
+ lib_filename = "libroutx.dll"
37
+ else:
38
+ lib_filename = "libroutx.so"
39
+
40
+ wheel_lib_path = Path(__file__).parent.parent / ".routx.mesonpy.libs" / lib_filename
41
+ local_lib_path = Path(__file__).with_name(lib_filename)
42
+ lib_path = wheel_lib_path if wheel_lib_path.exists() else local_lib_path
43
+ lib = cdll.LoadLibrary(str(lib_path))
44
+
45
+ # C-level definitions
46
+
47
+ c_char_p_p = POINTER(c_char_p)
48
+
49
+ _Graph_p = c_void_p
50
+ _GraphIterator_p = c_void_p
51
+ _KDTree_p = c_void_p
52
+ _LoggingCallback = CFUNCTYPE(None, c_void_p, c_int, c_char_p, c_char_p)
53
+ _LoggingFlushCallback = CFUNCTYPE(None, c_void_p)
54
+
55
+
56
+ class _Node(Structure):
57
+ _fields_ = [
58
+ ("id", c_int64),
59
+ ("osm_id", c_int64),
60
+ ("lat", c_float),
61
+ ("lon", c_float),
62
+ ]
63
+
64
+
65
+ class _Edge(Structure):
66
+ _fields_ = [
67
+ ("to", c_int64),
68
+ ("cost", c_float),
69
+ ]
70
+
71
+
72
+ class _OsmProfilePenalty(Structure):
73
+ _fields_ = [
74
+ ("key", c_char_p),
75
+ ("value", c_char_p),
76
+ ("penalty", c_float),
77
+ ]
78
+
79
+
80
+ class _OsmProfile(Structure):
81
+ _fields_ = [
82
+ ("name", c_char_p),
83
+ ("penalties", POINTER(_OsmProfilePenalty)),
84
+ ("penalties_len", c_size_t),
85
+ ("access", c_char_p_p),
86
+ ("access_len", c_size_t),
87
+ ("disallow_motorroad", c_bool),
88
+ ("disable_restrictions", c_bool),
89
+ ]
90
+
91
+
92
+ class _OsmOptions(Structure):
93
+ _fields_ = [
94
+ ("profile", POINTER(_OsmProfile)),
95
+ ("file_format", c_int),
96
+ ("bbox", c_float * 4),
97
+ ]
98
+
99
+
100
+ class _RouteResultOk(Structure):
101
+ _fields_ = [
102
+ ("nodes", POINTER(c_int64)),
103
+ ("len", c_uint32),
104
+ ("capacity", c_uint32),
105
+ ]
106
+
107
+
108
+ class _RouteResultInvalidRef(Structure):
109
+ _fields_ = [
110
+ ("invalid_node_id", c_int64),
111
+ ]
112
+
113
+
114
+ class _RouteResultUnion(Union):
115
+ _fields_ = [
116
+ ("as_ok", _RouteResultOk),
117
+ ("as_invalid_reference", _RouteResultInvalidRef),
118
+ ]
119
+
120
+
121
+ class _RouteResult(Structure):
122
+ _anonymous_ = ["u"]
123
+ _fields_ = [
124
+ ("u", _RouteResultUnion),
125
+ ("type", c_int),
126
+ ]
127
+
128
+
129
+ lib.routx_set_logging_callback.argtypes = [
130
+ _LoggingCallback,
131
+ _LoggingFlushCallback,
132
+ c_void_p,
133
+ c_int,
134
+ ]
135
+ lib.routx_set_logging_callback.restype = None
136
+
137
+ lib.routx_graph_new.argtypes = []
138
+ lib.routx_graph_new.restype = _Graph_p
139
+
140
+ lib.routx_graph_delete.argtypes = [_Graph_p]
141
+ lib.routx_graph_delete.restype = None
142
+
143
+ lib.routx_graph_get_nodes.argtypes = [_Graph_p, POINTER(_GraphIterator_p)]
144
+ lib.routx_graph_get_nodes.restype = c_size_t
145
+
146
+ lib.routx_graph_iterator_next.argtypes = [_GraphIterator_p]
147
+ lib.routx_graph_iterator_next.restype = _Node
148
+
149
+ lib.routx_graph_iterator_delete.argtypes = [_GraphIterator_p]
150
+ lib.routx_graph_iterator_delete.restype = None
151
+
152
+ lib.routx_graph_get_node.argtypes = [_Graph_p, c_int64]
153
+ lib.routx_graph_get_node.restype = _Node
154
+
155
+ lib.routx_graph_set_node.argtypes = [_Graph_p, _Node]
156
+ lib.routx_graph_set_node.restype = bool
157
+
158
+ lib.routx_graph_delete_node.argtypes = [_Graph_p, c_int64]
159
+ lib.routx_graph_delete_node.restype = bool
160
+
161
+ lib.routx_graph_find_nearest_node.argtypes = [_Graph_p, c_float, c_float]
162
+ lib.routx_graph_find_nearest_node.restype = _Node
163
+
164
+ lib.routx_graph_get_edges.argtypes = [_Graph_p, c_int64, POINTER(POINTER(_Edge))]
165
+ lib.routx_graph_get_edges.restype = c_size_t
166
+
167
+ lib.routx_graph_get_edge.argtypes = [_Graph_p, c_int64, c_int64]
168
+ lib.routx_graph_get_edge.restype = c_float
169
+
170
+ lib.routx_graph_set_edge.argtypes = [_Graph_p, c_int64, _Edge]
171
+ lib.routx_graph_set_edge.restype = c_bool
172
+
173
+ lib.routx_graph_delete_edge.argtypes = [_Graph_p, c_int64, c_int64]
174
+ lib.routx_graph_delete_edge.restype = c_bool
175
+
176
+ lib.routx_graph_add_from_osm_file.argtypes = [_Graph_p, POINTER(_OsmOptions), c_char_p]
177
+ lib.routx_graph_add_from_osm_file.restype = c_bool
178
+
179
+ lib.routx_graph_add_from_osm_memory.argtypes = [
180
+ _Graph_p,
181
+ POINTER(_OsmOptions),
182
+ c_char_p,
183
+ c_size_t,
184
+ ]
185
+ lib.routx_graph_add_from_osm_memory.restype = c_bool
186
+
187
+ lib.routx_find_route.argtypes = [_Graph_p, c_int64, c_int64, c_size_t]
188
+ lib.routx_find_route.restype = _RouteResult
189
+
190
+ lib.routx_find_route_without_turn_around.argtypes = [_Graph_p, c_int64, c_int64, c_size_t]
191
+ lib.routx_find_route_without_turn_around.restype = _RouteResult
192
+
193
+ lib.routx_route_result_delete.argtypes = [_RouteResult]
194
+ lib.routx_route_result_delete.restype = None
195
+
196
+ lib.routx_kd_tree_new.argtypes = [_Graph_p]
197
+ lib.routx_kd_tree_new.restype = _KDTree_p
198
+
199
+ lib.routx_kd_tree_delete.argtypes = [_KDTree_p]
200
+ lib.routx_kd_tree_delete.restype = None
201
+
202
+ lib.routx_kd_tree_find_nearest_node.argtypes = [_KDTree_p, c_float, c_float]
203
+ lib.routx_kd_tree_find_nearest_node.restype = _Node
204
+
205
+ lib.routx_earth_distance.argtypes = [c_float, c_float, c_float, c_float]
206
+ lib.routx_earth_distance.restype = c_float
207
+
208
+
209
+ # Wire up logging
210
+
211
+
212
+ @_LoggingCallback
213
+ def _builtin_log_handler(_: Any, level: int, target_b: bytes, message_b: bytes) -> None:
214
+ target = target_b.decode("utf-8").replace("::", ".")
215
+ message = message_b.decode("utf-8")
216
+ logging.getLogger(target).log(level, message)
217
+
218
+
219
+ lib.routx_set_logging_callback(_builtin_log_handler, _LoggingFlushCallback(), None, 10)
220
+
221
+
222
+ # High-level Python definitions
223
+
224
+
225
+ DEFAULT_STEP_LIMIT: Final[int] = 1000000
226
+ """Recommended A* step limit for Graph.find_route."""
227
+
228
+
229
+ class StepLimitExceeded(ValueError):
230
+ """Graph.find_route exceeded its step limit."""
231
+
232
+ pass
233
+
234
+
235
+ class OsmLoadingError(ValueError):
236
+ """Raised with the underlying library has failed to load OSM data data. See logs for details."""
237
+
238
+ pass
239
+
240
+
241
+ class Node(NamedTuple):
242
+ """
243
+ An element of a Graph.
244
+
245
+ Due to turn restriction processing, one OpenStreetMap node may be represented by
246
+ multiple nodes in the graph. If that is the case, a "canonical" node
247
+ (not bound by any turn restriction) will have `id == osm_id`.
248
+
249
+ Nodes with `id == 0` are used by the underlying library to signify the absence of a node,
250
+ are considered false-y and must not be used by consumers.
251
+ """
252
+
253
+ id: int
254
+ osm_id: int
255
+ lat: float
256
+ lon: float
257
+
258
+ @property
259
+ def is_canonical(self) -> bool:
260
+ return self.id == self.osm_id
261
+
262
+ def __bool__(self) -> bool:
263
+ return self.id != 0
264
+
265
+
266
+ class Edge(NamedTuple):
267
+ """
268
+ Outgoing (one-way) connection from a Node.
269
+
270
+ `cost` must be greater than the crow-flies distance between the two nodes.
271
+ """
272
+
273
+ to: int
274
+ cost: float
275
+
276
+
277
+ class OsmPenalty(NamedTuple):
278
+ """Numeric multiplier for OSM ways with specific keys and values."""
279
+
280
+ key: str
281
+ """
282
+ Key of an OSM way for which this penalty applies,
283
+ used for `value` comparison (e.g. "highway" or "railway").
284
+ """
285
+
286
+ value: str
287
+ """
288
+ Value under `key` of an OSM way for which this penalty applies.
289
+ E.g. "motorway", "residential", or "rail".
290
+ """
291
+
292
+ penalty: float
293
+ """
294
+ Multiplier of the length, to express preference for a specific way.
295
+ Must be not less than one and be finite.
296
+ """
297
+
298
+
299
+ class OsmProfile(IntEnum):
300
+ """Predefined OSM conversion profiles."""
301
+
302
+ CAR = 1
303
+ """
304
+ Car routing profile.
305
+
306
+ Penalties:
307
+ | Tag | Penalty |
308
+ |------------------------|---------|
309
+ | highway=motorway | 1.0 |
310
+ | highway=motorway_link | 1.0 |
311
+ | highway=trunk | 2.0 |
312
+ | highway=trunk_link | 2.0 |
313
+ | highway=primary | 5.0 |
314
+ | highway=primary_link | 5.0 |
315
+ | highway=secondary | 6.5 |
316
+ | highway=secondary_link | 6.5 |
317
+ | highway=tertiary | 10.0 |
318
+ | highway=tertiary_link | 10.0 |
319
+ | highway=unclassified | 10.0 |
320
+ | highway=minor | 10.0 |
321
+ | highway=residential | 15.0 |
322
+ | highway=living_street | 20.0 |
323
+ | highway=track | 20.0 |
324
+ | highway=service | 20.0 |
325
+
326
+ Access tags: `access`, `vehicle`, `motor_vehicle`, `motorcar`.
327
+
328
+ Allows [motorroads](https://wiki.openstreetmap.org/wiki/Key:motorroad) and considers turn restrictions.
329
+ """
330
+
331
+ BUS = 2
332
+ """
333
+ Bus routing profile.
334
+
335
+ Penalties:
336
+ | Tag | Penalty |
337
+ |------------------------|---------|
338
+ | highway=motorway | 1.0 |
339
+ | highway=motorway_link | 1.0 |
340
+ | highway=trunk | 1.0 |
341
+ | highway=trunk_link | 1.0 |
342
+ | highway=primary | 1.1 |
343
+ | highway=primary_link | 1.1 |
344
+ | highway=secondary | 1.15 |
345
+ | highway=secondary_link | 1.15 |
346
+ | highway=tertiary | 1.15 |
347
+ | highway=tertiary_link | 1.15 |
348
+ | highway=unclassified | 1.5 |
349
+ | highway=minor | 1.5 |
350
+ | highway=residential | 2.5 |
351
+ | highway=living_street | 2.5 |
352
+ | highway=track | 5.0 |
353
+ | highway=service | 5.0 |
354
+
355
+ Access tags: `access`, `vehicle`, `motor_vehicle`, `psv`, `bus`, `routing:ztm`.
356
+
357
+ Allows [motorroads](https://wiki.openstreetmap.org/wiki/Key:motorroad) and considers turn restrictions.
358
+ """
359
+
360
+ BICYCLE = 3
361
+ """
362
+ Bicycle routing profile.
363
+
364
+ Penalties:
365
+ | Tag | Penalty |
366
+ |------------------------|---------|
367
+ | highway=trunk | 50.0 |
368
+ | highway=trunk_link | 50.0 |
369
+ | highway=primary | 10.0 |
370
+ | highway=primary_link | 10.0 |
371
+ | highway=secondary | 3.0 |
372
+ | highway=secondary_link | 3.0 |
373
+ | highway=tertiary | 2.5 |
374
+ | highway=tertiary_link | 2.5 |
375
+ | highway=unclassified | 2.5 |
376
+ | highway=minor | 2.5 |
377
+ | highway=cycleway | 1.0 |
378
+ | highway=residential | 1.0 |
379
+ | highway=living_street | 1.5 |
380
+ | highway=track | 2.0 |
381
+ | highway=service | 2.0 |
382
+ | highway=bridleway | 3.0 |
383
+ | highway=footway | 3.0 |
384
+ | highway=steps | 5.0 |
385
+ | highway=path | 2.0 |
386
+
387
+ Access tags: `access`, `vehicle`, `bicycle`.
388
+
389
+ Disallows [motorroads](https://wiki.openstreetmap.org/wiki/Key:motorroad) and considers turn restrictions.
390
+ """
391
+
392
+ FOOT = 4
393
+ """
394
+ Pedestrian routing profile.
395
+
396
+ Penalties:
397
+ | Tag | Penalty |
398
+ |---------------------------|---------|
399
+ | highway=trunk | 4.0 |
400
+ | highway=trunk_link | 4.0 |
401
+ | highway=primary | 2.0 |
402
+ | highway=primary_link | 2.0 |
403
+ | highway=secondary | 1.3 |
404
+ | highway=secondary_link | 1.3 |
405
+ | highway=tertiary | 1.2 |
406
+ | highway=tertiary_link | 1.2 |
407
+ | highway=unclassified | 1.2 |
408
+ | highway=minor | 1.2 |
409
+ | highway=residential | 1.2 |
410
+ | highway=living_street | 1.2 |
411
+ | highway=track | 1.2 |
412
+ | highway=service | 1.2 |
413
+ | highway=bridleway | 1.2 |
414
+ | highway=footway | 1.05 |
415
+ | highway=path | 1.05 |
416
+ | highway=steps | 1.15 |
417
+ | highway=pedestrian | 1.0 |
418
+ | highway=platform | 1.1 |
419
+ | railway=platform | 1.1 |
420
+ | public_transport=platform | 1.1 |
421
+
422
+ Access tags: `access`, `foot`.
423
+
424
+ Disallows [motorroads](https://wiki.openstreetmap.org/wiki/Key:motorroad).
425
+
426
+ One-way is only considered when explicitly tagged with `oneway:foot` or on
427
+ `highway=footway`, `highway=path`, `highway=steps`, `highway/public_transport/railway=platform`.
428
+
429
+ Turn restrictions are only considered when explicitly tagged with `restriction:foot`.
430
+ """
431
+
432
+ RAILWAY = 5
433
+ """
434
+ Railway routing profile.
435
+
436
+ Penalties:
437
+ | Tag | Penalty |
438
+ |----------------------|---------|
439
+ | railway=rail | 1.0 |
440
+ | railway=light_rail | 1.0 |
441
+ | railway=subway | 1.0 |
442
+ | railway=narrow_gauge | 1.0 |
443
+
444
+ Access tags: `access`, `train`.
445
+
446
+ Allows [motorroads](https://wiki.openstreetmap.org/wiki/Key:motorroad) and considers turn restrictions.
447
+ """
448
+
449
+ TRAM = 6
450
+ """
451
+ Tram and light rail routing profile.
452
+
453
+ Penalties:
454
+ | Tag | Penalty |
455
+ |----------------------|---------|
456
+ | railway=tram | 1.0 |
457
+ | railway=light_rail | 1.0 |
458
+
459
+ Access tags: `access`, `tram`.
460
+
461
+ Allows [motorroads](https://wiki.openstreetmap.org/wiki/Key:motorroad) and considers turn restrictions.
462
+ """
463
+
464
+ SUBWAY = 7
465
+ """
466
+ Subway routing profile.
467
+
468
+ Penalties:
469
+ | Tag | Penalty |
470
+ |----------------|---------|
471
+ | railway=subway | 1.0 |
472
+
473
+ Access tags: `access`, `subway`.
474
+
475
+ Allows [motorroads](https://wiki.openstreetmap.org/wiki/Key:motorroad) and considers turn restrictions.
476
+ """
477
+
478
+
479
+ @dataclass
480
+ class OsmCustomProfile:
481
+ """
482
+ Describes how to convert OSM data into a Graph.
483
+
484
+ If possible, usage of pre-defined OsmProfiles should be preferred.
485
+ Using custom profile involves reallocation of all arrays and strings
486
+ two times to match ABIs (first from Python to C, then from C to Rust).
487
+ This is only a constant cost incurred on call to Graph.add_from_file or Graph.add_from_memory.
488
+ """
489
+
490
+ name: str
491
+ """Human readable name of the routing profile,
492
+ customary the most specific [access tag](https://wiki.openstreetmap.org/wiki/Key:access).
493
+
494
+ This value is not used for actual OSM data interpretation,
495
+ except when set to "foot", which adds the following logic:
496
+ - `oneway` tags are ignored - only `oneway:foot` tags are considered, except on:
497
+ - `highway=footway`,
498
+ - `highway=path`,
499
+ - `highway=steps`,
500
+ - `highway=platform`
501
+ - `public_transport=platform`,
502
+ - `railway=platform`;
503
+ - only `restriction:foot` turn restrictions are considered.
504
+ """
505
+
506
+ penalties: list[OsmPenalty]
507
+ """
508
+ Tags of OSM ways which can be used for routing.
509
+
510
+ A way is matched against all OsmPenalty objects in order, and once an exact key and value match
511
+ is found; the way is used for routing, and each connection between two nodes gets
512
+ a resulting cost equal to the distance between nodes multiplied the penalty.
513
+
514
+ All penalties must be normal and not less than zero.
515
+
516
+ For example, if there are two penalties:
517
+ 1. highway=motorway, penalty=1
518
+ 2. highway=trunk, penalty=1.5
519
+
520
+ This will result in:
521
+ - a highway=motorway stretch of 100 meters will be used for routing with a cost of 100.
522
+ - a highway=trunk motorway of 100 meters will be used for routing with a cost of 150.
523
+ - a highway=motorway_link or highway=primary won't be used for routing, as they do not any penalty.
524
+ """
525
+
526
+ access: list[str]
527
+ """
528
+ List of OSM [access tags](https://wiki.openstreetmap.org/wiki/Key:access#Land-based_transportation)
529
+ (in order from least to most specific) to consider when checking for road prohibitions.
530
+
531
+ This list is used mainly to follow the access tags, but also to follow mode-specific one-way
532
+ and turn restrictions.
533
+ """
534
+
535
+ disallow_motorroad: bool = False
536
+ """Force no routing over [motorroad=yes](https://wiki.openstreetmap.org/wiki/Key:motorroad) ways."""
537
+
538
+ disable_restrictions: bool = False
539
+ """Force ignoring of [turn restrictions](https://wiki.openstreetmap.org/wiki/Turn_restriction)."""
540
+
541
+
542
+ class OsmFormat(IntEnum):
543
+ """Format of the input OSM file."""
544
+
545
+ UNKNOWN = 0
546
+ """Unknown format - guess based on content."""
547
+
548
+ XML = 1
549
+ """Force uncompressed [OSM XML](https://wiki.openstreetmap.org/wiki/OSM_XML)"""
550
+
551
+ XML_GZ = 2
552
+ """
553
+ Force [OSM XML](https://wiki.openstreetmap.org/wiki/OSM_XML)
554
+ with [gzip](https://en.wikipedia.org/wiki/Gzip) compression
555
+ """
556
+
557
+ XML_BZ2 = 3
558
+ """
559
+ Force [OSM XML](https://wiki.openstreetmap.org/wiki/OSM_XML)
560
+ with [bzip2](https://en.wikipedia.org/wiki/Bzip2) compression
561
+ """
562
+
563
+ PBF = 4
564
+ """Force [OSM PBF](https://wiki.openstreetmap.org/wiki/PBF_Format)"""
565
+
566
+
567
+ class Graph(MutableMapping[int, Node]):
568
+ """
569
+ OpenStreetMap-based network representation as a set of nodes and edges between them.
570
+
571
+ Node access is implemented through the standard
572
+ [MutableMapping interface](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)
573
+ from ids (integers) to nodes.
574
+
575
+ Note that overwriting existing nodes preserves all outgoing and incoming edges. Thus updating
576
+ a node position might result in violation of the Edge invariant and break route finding.
577
+ It **is discouraged** to update nodes, and it is the caller's responsibility not to break
578
+ this invariant.
579
+
580
+ Edge access is implemented through custom get_edges, get_edge, set_edge and delete_edge methods.
581
+ """
582
+
583
+ handle: _Graph_p
584
+
585
+ def __init__(self) -> None:
586
+ self.handle = lib.routx_graph_new()
587
+
588
+ def __del__(self) -> None:
589
+ lib.routx_graph_delete(self.handle)
590
+
591
+ def __getitem__(self, key: int) -> Node:
592
+ n = _node_from_c(lib.routx_graph_get_node(self.handle, key))
593
+ if n.id == 0:
594
+ raise KeyError(key)
595
+ return n
596
+
597
+ def __setitem__(self, key: int, value: Node) -> None:
598
+ if key != value.id:
599
+ raise ValueError(f"attempt to save node with id {value.id} under different id, {key}")
600
+ lib.routx_graph_set_node(self.handle, _node_to_c(value))
601
+
602
+ def __delitem__(self, key: int) -> None:
603
+ deleted = lib.routx_graph_delete_node(self.handle, key)
604
+ if not deleted:
605
+ raise KeyError(key)
606
+
607
+ def __iter__(self) -> Iterator[int]:
608
+ it_handle = _GraphIterator_p()
609
+ try:
610
+ lib.routx_graph_get_nodes(self.handle, byref(it_handle))
611
+ while n := _node_from_c(lib.routx_graph_iterator_next(it_handle)):
612
+ yield n.id
613
+ finally:
614
+ lib.routx_graph_iterator_delete(it_handle)
615
+
616
+ def __len__(self) -> int:
617
+ return lib.routx_graph_get_nodes(self.handle, None)
618
+
619
+ def find_nearest_node(self, lat: float, lon: float) -> Node:
620
+ """
621
+ Find the closest canonical (`id == osm_id`) Node to the given position.
622
+
623
+ This function requires computing distance to every Node in the graph
624
+ and is not suitable for large graphs or for multiple searches.
625
+ Use KDTree for faster NN finding.
626
+
627
+ If the graph is empty, raises KeyError.
628
+ """
629
+ n = _node_from_c(lib.routx_graph_find_nearest_node(self.handle, lat, lon))
630
+ if not n:
631
+ raise KeyError("find_nearest_node on empty Graph")
632
+ return n
633
+
634
+ def get_edges(self, from_: int) -> list[Edge]:
635
+ """Gets all outgoing edges from a node with a given id."""
636
+ c_ptr = POINTER(_Edge)()
637
+ c_ptr_len = lib.routx_graph_get_edges(self.handle, from_, byref(c_ptr))
638
+ return [_edge_from_c(c_ptr[i]) for i in range(c_ptr_len)]
639
+
640
+ def get_edge(self, from_: int, to: int) -> float:
641
+ """
642
+ Gets the cost of traversing an edge between nodes with provided ids.
643
+ Returns positive infinity when the provided edge does not exist.
644
+ """
645
+ return lib.routx_graph_get_edge(self.handle, from_, to)
646
+
647
+ def set_edge(self, from_: int, to: int, cost: float) -> bool:
648
+ """
649
+ Creates or updates an Edge from one node to another.
650
+
651
+ The `cost` must not be smaller than the crow-flies distance between nodes,
652
+ as this would violate the A* invariant and break route finding. It is the
653
+ caller's responsibility to uphold this invariant.
654
+
655
+ Returns True if an existing edge was overwritten, False otherwise.
656
+
657
+ Note that given an `Edge` object, this method may be called with `g.set_edge(from_, *edge)`.
658
+ """
659
+ return lib.routx_graph_set_edge(self.handle, from_, _Edge(to=to, cost=cost))
660
+
661
+ def delete_edge(self, from_: int, to: int, /, missing_ok: bool = True) -> None:
662
+ """
663
+ Ensures an Edge from one node to another does not exist.
664
+
665
+ If no such edge exists and `missing_ok` is set to `False`, raises KeyError.
666
+ """
667
+ removed = lib.routx_graph_delete_edge(self.handle, from_, to)
668
+ if not removed and not missing_ok:
669
+ raise KeyError((from_, to))
670
+
671
+ def find_route(
672
+ self,
673
+ from_: int,
674
+ to: int,
675
+ /,
676
+ without_turn_around: bool = True,
677
+ step_limit: int = DEFAULT_STEP_LIMIT,
678
+ ) -> list[int]:
679
+ """
680
+ Finds the cheapest way between two nodes using the [A* algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm).
681
+ Returns a list node IDs of such route. The list may be empty if no route exists.
682
+
683
+ `without_turn_around` defaults to `True` and prevents the algorithm from circumventing
684
+ turn restrictions by suppressing unrealistic turn-around instructions (A-B-A).
685
+ This introduces an extra dimension to the search space, so if the graph doesn't contain
686
+ any turn restriction, this parameter should be set to `False`.
687
+
688
+ `step_limit` limits how many nodes can be expanded during search before raising StepLimitExceeded.
689
+ Concluding that no route exists requires expanding all nodes accessible from the start,
690
+ which is usually very time consuming, especially on large datasets.
691
+ """
692
+ func = (
693
+ lib.routx_find_route_without_turn_around
694
+ if without_turn_around
695
+ else lib.routx_find_route
696
+ )
697
+ res = func(self.handle, from_, to, step_limit)
698
+ try:
699
+ if res.type == 0:
700
+ # TODO: Could we return a memoryview with type "q"?
701
+ return [res.as_ok.nodes[i] for i in range(res.as_ok.len)]
702
+ elif res.type == 1:
703
+ raise KeyError(res.as_invalid_reference.invalid_node_id)
704
+ elif res.type == 2:
705
+ raise StepLimitExceeded()
706
+ else:
707
+ raise RuntimeError(f"routx_find_route returned unexpected result type: {res.type}")
708
+ finally:
709
+ lib.routx_route_result_delete(res)
710
+
711
+ def add_from_osm_file(
712
+ self,
713
+ filename: str | bytes | PathLike[str] | PathLike[bytes],
714
+ profile: OsmProfile | OsmCustomProfile,
715
+ /,
716
+ format: OsmFormat = OsmFormat.UNKNOWN,
717
+ bbox: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
718
+ ) -> None:
719
+ """
720
+ Parses OSM data from the provided file and adds them to this graph.
721
+
722
+ `profile` describes how the OSM data should be interpreted.
723
+
724
+ `format` describes the file format of the input OSM data. Defaults to auto-detection.
725
+
726
+ `bbox` filters features by a specific bounding box, in order:
727
+ left (min lon), bottom (min lat), right (max lon), top (max lat).
728
+ Ignored if all values are zero (default).
729
+ """
730
+ if isinstance(filename, PathLike):
731
+ filename = filename.__fspath__()
732
+ if isinstance(filename, str):
733
+ filename = filename.encode("utf-8")
734
+
735
+ ok = lib.routx_graph_add_from_osm_file(
736
+ self.handle,
737
+ _osm_options_to_c(profile, format, bbox),
738
+ filename,
739
+ )
740
+ if not ok:
741
+ raise OsmLoadingError()
742
+
743
+ def add_from_osm_memory(
744
+ self,
745
+ mv: memoryview,
746
+ profile: OsmProfile | OsmCustomProfile,
747
+ /,
748
+ format: OsmFormat = OsmFormat.UNKNOWN,
749
+ bbox: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
750
+ ) -> None:
751
+ """
752
+ Parses OSM data from the provided contents of a memory file.
753
+ The buffer must be contiguous and also mutable (for reasons only known to
754
+ [ctypes](https://docs.python.org/3/library/ctypes.html#ctypes._CData.from_buffer),
755
+ because the underlying library takes a const pointer).
756
+
757
+ `profile` describes how the OSM data should be interpreted.
758
+
759
+ `format` describes the file format of the input OSM data. Defaults to auto-detection.
760
+
761
+ `bbox` filters features by a specific bounding box, in order:
762
+ left (min lon), bottom (min lat), right (max lon), top (max lat).
763
+ Ignored if all values are zero (default).
764
+ """
765
+ mv = mv.cast("B")
766
+ ptr = (c_char * len(mv)).from_buffer(mv)
767
+ ok = lib.routx_graph_add_from_osm_memory(
768
+ self.handle,
769
+ _osm_options_to_c(profile, format, bbox),
770
+ ptr,
771
+ len(mv),
772
+ )
773
+ if not ok:
774
+ raise OsmLoadingError()
775
+
776
+
777
+ class KDTree:
778
+ """
779
+ [k-d tree data structure](https://en.wikipedia.org/wiki/K-d_tree) which can be used to
780
+ speed up nearest-neighbor search for large datasets.
781
+
782
+ Create with `KDTree.build`, as the constructors takes a raw C handler.
783
+ """
784
+
785
+ _handle: _KDTree_p
786
+
787
+ def __init__(self, handle: _KDTree_p) -> None:
788
+ self._handle = handle
789
+
790
+ def __del__(self) -> None:
791
+ lib.routx_kd_tree_delete(self._handle)
792
+
793
+ @classmethod
794
+ def build(cls, graph: Graph) -> Self:
795
+ """
796
+ Builds a k-d tree with all canonical (`id == osm_id`) nodes contained in the provided graph.
797
+ """
798
+ return cls(lib.routx_kd_tree_new(graph.handle))
799
+
800
+ def find_nearest_node(self, lat: float, lon: float) -> Node:
801
+ """
802
+ Find the closest node to the provided position and returns its id.
803
+
804
+ Raises KeyError when the k-d tree contains no nodes.
805
+ """
806
+ nd = _node_from_c(lib.routx_kd_tree_find_nearest_node(self._handle, lat, lon))
807
+ if nd.id == 0:
808
+ raise KeyError("find_nearest_node on empty KDTree")
809
+ return nd
810
+
811
+
812
+ def earth_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
813
+ """
814
+ Calculates the great-circle distance between two positions using the
815
+ [haversine formula](https://en.wikipedia.org/wiki/Haversine_formula).
816
+
817
+ Returns the result in kilometers.
818
+ """
819
+ return lib.routx_earth_distance(lat1, lon1, lat2, lon2)
820
+
821
+
822
+ def _node_to_c(o: Node) -> _Node:
823
+ return _Node(id=o.id, osm_id=o.osm_id, lat=o.lat, lon=o.lon)
824
+
825
+
826
+ def _node_from_c(o: _Node) -> Node:
827
+ return Node(id=o.id, osm_id=o.osm_id, lat=o.lat, lon=o.lon)
828
+
829
+
830
+ def _edge_from_c(o: _Edge) -> Edge:
831
+ return Edge(to=o.to, cost=o.cost)
832
+
833
+
834
+ def _osm_profile_to_c(profile: OsmProfile | OsmCustomProfile):
835
+ if isinstance(profile, OsmProfile):
836
+ return c_cast(c_void_p(profile.value), POINTER(_OsmProfile))
837
+
838
+ c_profile = _OsmProfile()
839
+ c_profile.name = profile.name.encode("utf-8")
840
+
841
+ c_profile.penalties = (_OsmProfilePenalty * len(profile.penalties))()
842
+ for i, (key, value, penalty) in enumerate(profile.penalties):
843
+ c_profile.penalties[i] = _OsmProfilePenalty(
844
+ key=key.encode("utf-8"),
845
+ value=value.encode("utf-8"),
846
+ penalty=penalty,
847
+ )
848
+ c_profile.penalties_len = len(profile.penalties)
849
+
850
+ c_profile.access = (c_char_p * len(profile.access))()
851
+ for i, access_key in enumerate(profile.access):
852
+ c_profile.access[i] = access_key.encode("utf-8")
853
+ c_profile.access_len = len(profile.access)
854
+
855
+ c_profile.disallow_motorroad = profile.disallow_motorroad
856
+ c_profile.disable_restrictions = profile.disable_restrictions
857
+
858
+ return pointer(c_profile)
859
+
860
+
861
+ def _osm_options_to_c(
862
+ profile: OsmProfile | OsmCustomProfile,
863
+ format: OsmFormat,
864
+ bbox: tuple[float, float, float, float],
865
+ ):
866
+ o = _OsmOptions()
867
+ o.profile = _osm_profile_to_c(profile)
868
+ o.file_format = format.value
869
+ o.bbox[0] = bbox[0]
870
+ o.bbox[1] = bbox[1]
871
+ o.bbox[2] = bbox[2]
872
+ o.bbox[3] = bbox[3]
873
+ return pointer(o)
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.1
2
+ Name: routx
3
+ Version: 1.0.2
4
+ Author-Email: =?utf-8?q?Miko=C5=82aj_Kuranowski?= <mkuranowski@gmail.com>
5
+ Requires-Python: ~=3.10
6
+ Requires-Dist: typing-extensions~=4.15
7
+
@@ -0,0 +1,6 @@
1
+ routx-1.0.2.dist-info/METADATA,sha256=1fabSUghlXWXiFm1_i36-cVyW1AIRv2dPJ9qBUlGDMU,187
2
+ routx-1.0.2.dist-info/WHEEL,sha256=eGbHOVnVkNk5N_A188Q13-gtiwyWWQjeSFRh9oxazXU,84
3
+ .routx.mesonpy.libs/libroutx.dll,sha256=EwUPrw8mi1WyOo8IsehAVAIE2ovuj8KIagIjVgIwToY,740864
4
+ routx/__init__.py,sha256=shuZ01RoaXzL9TLKS6samxJ_1Maz_NszA5B1WtZM8oo,854
5
+ routx/wrapper.py,sha256=nDACNt37uHsamKlalKaCcDvDQGzhitFB_71NbiKtYJA,28068
6
+ routx-1.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: meson
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_amd64
5
+