0
# Geospatial Operations
1
2
Geographic analysis, spatial filtering, and coordinate system transformations.
3
4
## Geometry Conversion
5
6
{ .api }
7
```python
8
def geometrize_stops(stops: pd.DataFrame, *, use_utm: bool = False) -> gpd.GeoDataFrame:
9
"""
10
Convert stops DataFrame to GeoDataFrame with Point geometries.
11
12
Args:
13
stops: Stops DataFrame with stop_lat and stop_lon columns
14
use_utm: If True, convert to appropriate UTM projection for accurate distance calculations
15
16
Returns:
17
GeoDataFrame with Point geometries created from coordinates
18
"""
19
20
def ungeometrize_stops(stops_g: gpd.GeoDataFrame) -> pd.DataFrame:
21
"""
22
Convert stops GeoDataFrame back to regular DataFrame with lat/lon columns.
23
24
Args:
25
stops_g: Stops GeoDataFrame with Point geometries
26
27
Returns:
28
Regular DataFrame with stop_lat and stop_lon columns extracted from geometries
29
"""
30
31
def geometrize_shapes(shapes: pd.DataFrame, *, use_utm: bool = False) -> gpd.GeoDataFrame:
32
"""
33
Convert shapes DataFrame to GeoDataFrame with LineString geometries.
34
35
Args:
36
shapes: Shapes DataFrame with shape_pt_lat, shape_pt_lon, shape_pt_sequence
37
use_utm: If True, convert to appropriate UTM projection
38
39
Returns:
40
GeoDataFrame with LineString geometries for each shape_id
41
"""
42
43
def ungeometrize_shapes(shapes_g: gpd.GeoDataFrame) -> pd.DataFrame:
44
"""
45
Convert shapes GeoDataFrame back to regular DataFrame with lat/lon points.
46
47
Args:
48
shapes_g: Shapes GeoDataFrame with LineString geometries
49
50
Returns:
51
Regular DataFrame with individual shape points and coordinates
52
"""
53
```
54
55
## Spatial Data Access
56
57
{ .api }
58
```python
59
def get_stops(feed: Feed, date: str | None = None, trip_ids: list[str] | None = None,
60
route_ids: list[str] | None = None, *, in_stations: bool = False,
61
as_gdf: bool = False, use_utm: bool = False) -> pd.DataFrame:
62
"""
63
Get stops data with optional filtering and geographic conversion.
64
65
Args:
66
feed: Feed object containing stops data
67
date: Date string to filter by active service (optional)
68
trip_ids: List of trip IDs to filter stops by (optional)
69
route_ids: List of route IDs to filter stops by (optional)
70
in_stations: If True, include parent stations for grouped stops
71
as_gdf: If True, return as GeoDataFrame with Point geometries
72
use_utm: If True and as_gdf=True, convert to UTM projection
73
74
Returns:
75
DataFrame or GeoDataFrame with stops information and optional geometries
76
"""
77
78
def get_shapes(feed: Feed, *, as_gdf: bool = False, use_utm: bool = False) -> pd.DataFrame | None:
79
"""
80
Get shapes data with optional geographic conversion.
81
82
Args:
83
feed: Feed object containing shapes data
84
as_gdf: If True, return as GeoDataFrame with LineString geometries
85
use_utm: If True and as_gdf=True, convert to UTM projection
86
87
Returns:
88
DataFrame, GeoDataFrame, or None if no shapes data exists
89
"""
90
91
def get_routes(feed: Feed, date: str | None = None, time: str | None = None,
92
*, as_gdf: bool = False, use_utm: bool = False,
93
split_directions: bool = False) -> pd.DataFrame:
94
"""
95
Get routes data with optional filtering and shape geometries.
96
97
Args:
98
feed: Feed object containing route data
99
date: Date string to filter by active service (optional)
100
time: Time string to filter by active trips at that time (optional)
101
as_gdf: If True, include route shape geometries
102
use_utm: If True and as_gdf=True, convert to UTM projection
103
split_directions: If True, create separate records for each direction
104
105
Returns:
106
DataFrame or GeoDataFrame with route information and optional geometries
107
"""
108
109
def get_trips(feed: Feed, date: str | None = None, time: str | None = None,
110
*, as_gdf: bool = False, use_utm: bool = False) -> pd.DataFrame:
111
"""
112
Get trips data with optional filtering and shape geometries.
113
114
Args:
115
feed: Feed object containing trip data
116
date: Date string to filter by active service (optional)
117
time: Time string to filter by active trips at that time (optional)
118
as_gdf: If True, include trip shape geometries
119
use_utm: If True and as_gdf=True, convert to UTM projection
120
121
Returns:
122
DataFrame or GeoDataFrame with trip information and optional geometries
123
"""
124
```
125
126
## Spatial Analysis Functions
127
128
{ .api }
129
```python
130
def compute_bounds(feed: Feed, stop_ids: list[str] | None = None) -> np.array:
131
"""
132
Compute bounding box of stops in the feed.
133
134
Args:
135
feed: Feed object containing stops data
136
stop_ids: List of stop IDs to include, or None for all stops
137
138
Returns:
139
Array with [min_lon, min_lat, max_lon, max_lat] bounding box
140
"""
141
142
def compute_convex_hull(feed: Feed, stop_ids: list[str] | None = None) -> sg.Polygon:
143
"""
144
Compute convex hull polygon containing all specified stops.
145
146
Args:
147
feed: Feed object containing stops data
148
stop_ids: List of stop IDs to include, or None for all stops
149
150
Returns:
151
Shapely Polygon representing the convex hull of stop locations
152
"""
153
154
def compute_centroid(feed: Feed, stop_ids: list[str] | None = None) -> sg.Point:
155
"""
156
Compute centroid point of the convex hull of specified stops.
157
158
Args:
159
feed: Feed object containing stops data
160
stop_ids: List of stop IDs to include, or None for all stops
161
162
Returns:
163
Shapely Point representing the centroid of the stop convex hull
164
"""
165
166
def get_stops_in_area(feed: Feed, area: sg.Polygon) -> pd.DataFrame:
167
"""
168
Get all stops that fall within a polygonal area.
169
170
Args:
171
feed: Feed object containing stops data
172
area: Shapely Polygon defining the area boundary
173
174
Returns:
175
DataFrame with stops that intersect the specified area
176
"""
177
178
def get_shapes_intersecting_geometry(feed: Feed, geometry: sg.base.BaseGeometry,
179
shapes_g: gpd.GeoDataFrame | None = None,
180
*, as_gdf: bool = False) -> pd.DataFrame | None:
181
"""
182
Get shapes that intersect with a given geometry.
183
184
Args:
185
feed: Feed object containing shapes data
186
geometry: Shapely geometry to test intersection with
187
shapes_g: Pre-computed shapes GeoDataFrame (optional, will compute if None)
188
as_gdf: If True, return as GeoDataFrame with geometries
189
190
Returns:
191
DataFrame or GeoDataFrame with shapes intersecting the geometry, or None if no shapes
192
"""
193
```
194
195
## Geometry Building Functions
196
197
{ .api }
198
```python
199
def build_geometry_by_shape(feed: Feed, shape_ids: list[str] | None = None,
200
*, use_utm: bool = False) -> dict:
201
"""
202
Build mapping from shape IDs to their LineString geometries.
203
204
Args:
205
feed: Feed object containing shapes data
206
shape_ids: List of shape IDs to include, or None for all shapes
207
use_utm: If True, convert geometries to UTM projection
208
209
Returns:
210
Dictionary mapping shape_id -> Shapely LineString geometry
211
"""
212
213
def build_geometry_by_stop(feed: Feed, stop_ids: list[str] | None = None,
214
*, use_utm: bool = False) -> dict:
215
"""
216
Build mapping from stop IDs to their Point geometries.
217
218
Args:
219
feed: Feed object containing stops data
220
stop_ids: List of stop IDs to include, or None for all stops
221
use_utm: If True, convert geometries to UTM projection
222
223
Returns:
224
Dictionary mapping stop_id -> Shapely Point geometry
225
"""
226
```
227
228
## Feed Spatial Operations
229
230
{ .api }
231
```python
232
def restrict_to_area(feed: Feed, area: sg.Polygon) -> Feed:
233
"""
234
Restrict feed to only trips that intersect with a polygonal area.
235
236
Args:
237
feed: Feed object to restrict
238
area: Shapely Polygon defining the area boundary
239
240
Returns:
241
New Feed object containing only data for trips intersecting the area
242
"""
243
```
244
245
## Distance and Shape Operations
246
247
{ .api }
248
```python
249
def append_dist_to_shapes(feed: Feed) -> Feed:
250
"""
251
Add shape_dist_traveled field to shapes table based on cumulative distances.
252
253
Args:
254
feed: Feed object with shapes data
255
256
Returns:
257
New Feed object with shape_dist_traveled added to shapes table
258
"""
259
260
def append_dist_to_stop_times(feed: Feed) -> Feed:
261
"""
262
Add shape_dist_traveled field to stop_times table based on shape distances.
263
264
Args:
265
feed: Feed object with stop_times and shapes data
266
267
Returns:
268
New Feed object with shape_dist_traveled added to stop_times table
269
"""
270
271
def create_shapes(feed: Feed, *, all_trips: bool = False) -> Feed:
272
"""
273
Create shape geometries by connecting stops in trip order.
274
275
Args:
276
feed: Feed object containing stops and trips data
277
all_trips: If True, create shapes for all trips; if False, only for trips without existing shapes
278
279
Returns:
280
New Feed object with generated shapes table
281
"""
282
```
283
284
## Coordinate System and Units
285
286
{ .api }
287
```python
288
def convert_dist(feed: Feed, new_dist_units: str) -> Feed:
289
"""
290
Convert all distance measurements in feed to new units.
291
292
Args:
293
feed: Feed object to convert
294
new_dist_units: Target distance units ("km", "m", "mi", "ft")
295
296
Returns:
297
New Feed object with distances converted to new units
298
"""
299
300
# Distance utility constants and functions
301
WGS84: str = "EPSG:4326" # Standard geographic coordinate system
302
DIST_UNITS: list = ["ft", "mi", "m", "km"] # Valid distance units
303
304
def is_metric(dist_units: str) -> bool:
305
"""Check if distance units are metric (m or km)."""
306
307
def get_convert_dist(dist_units_in: str, dist_units_out: str) -> Callable:
308
"""Get conversion function between distance units."""
309
```
310
311
## GeoJSON Export Functions
312
313
{ .api }
314
```python
315
def routes_to_geojson(feed: Feed, route_ids: list[str] | None = None,
316
*, split_directions: bool = False, include_stops: bool = False) -> dict:
317
"""
318
Convert routes to GeoJSON format for web mapping.
319
320
Args:
321
feed: Feed object containing routes and shapes data
322
route_ids: List of route IDs to export, or None for all routes
323
split_directions: If True, create separate features for each direction
324
include_stops: If True, include stop markers in the GeoJSON
325
326
Returns:
327
GeoJSON dictionary with route LineString features and optional stop Point features
328
"""
329
330
def stops_to_geojson(feed: Feed, stop_ids: list[str] | None = None) -> dict:
331
"""
332
Convert stops to GeoJSON format for web mapping.
333
334
Args:
335
feed: Feed object containing stops data
336
stop_ids: List of stop IDs to export, or None for all stops
337
338
Returns:
339
GeoJSON dictionary with stop Point features and properties
340
"""
341
342
def trips_to_geojson(feed: Feed, trip_ids: list[str] | None = None,
343
*, include_stops: bool = False) -> dict:
344
"""
345
Convert trips to GeoJSON format showing trip paths.
346
347
Args:
348
feed: Feed object containing trips and shapes data
349
trip_ids: List of trip IDs to export, or None for all trips
350
include_stops: If True, include stop sequences for each trip
351
352
Returns:
353
GeoJSON dictionary with trip LineString features and optional stop sequences
354
"""
355
356
def shapes_to_geojson(feed: Feed, shape_ids: list[str] | None = None) -> dict:
357
"""
358
Convert shapes to GeoJSON format.
359
360
Args:
361
feed: Feed object containing shapes data
362
shape_ids: List of shape IDs to export, or None for all shapes
363
364
Returns:
365
GeoJSON dictionary with shape LineString features
366
"""
367
368
def stop_times_to_geojson(feed: Feed, trip_ids: list[str] | None = None) -> dict:
369
"""
370
Convert stop times to GeoJSON format showing scheduled stops.
371
372
Args:
373
feed: Feed object containing stop_times and stops data
374
trip_ids: List of trip IDs to export stop times for
375
376
Returns:
377
GeoJSON dictionary with stop time Point features and schedule properties
378
"""
379
```
380
381
## Interactive Mapping Functions
382
383
{ .api }
384
```python
385
def map_routes(feed: Feed, route_ids: list[str] | None = None,
386
route_short_names: list[str] | None = None,
387
color_palette: list[str] | None = None, *, show_stops: bool = False) -> None:
388
"""
389
Create interactive Folium map displaying routes.
390
391
Args:
392
feed: Feed object containing routes and shapes data
393
route_ids: List of route IDs to map, or None for all routes
394
route_short_names: List of route short names to map (alternative to route_ids)
395
color_palette: List of colors for route visualization
396
show_stops: If True, display stops along routes
397
398
Returns:
399
Folium map object (displays in Jupyter notebooks)
400
"""
401
402
def map_stops(feed: Feed, stop_ids: list[str] | None = None,
403
stop_style: dict | None = None) -> None:
404
"""
405
Create interactive Folium map displaying stops.
406
407
Args:
408
feed: Feed object containing stops data
409
stop_ids: List of stop IDs to map, or None for all stops
410
stop_style: Dictionary with Leaflet circleMarker style parameters
411
412
Returns:
413
Folium map object showing stop locations and information
414
"""
415
416
def map_trips(feed: Feed, trip_ids: list[str] | None = None,
417
color_palette: list[str] | None = None, *, show_stops: bool = False,
418
show_direction: bool = False) -> None:
419
"""
420
Create interactive Folium map displaying trip paths.
421
422
Args:
423
feed: Feed object containing trips and shapes data
424
trip_ids: List of trip IDs to map, or None for sample of trips
425
color_palette: List of colors for trip visualization
426
show_stops: If True, display stops along trip paths
427
show_direction: If True, show directional arrows on trip paths
428
429
Returns:
430
Folium map object showing trip routes and optional stops/directions
431
"""
432
433
# Map styling constants
434
STOP_STYLE: dict # Default Leaflet circleMarker parameters for stops
435
```
436
437
## Screen Line Analysis
438
439
{ .api }
440
```python
441
def compute_screen_line_counts(feed: Feed, screen_lines: list[sg.LineString],
442
dates: list[str]) -> pd.DataFrame:
443
"""
444
Count trip crossings of screen lines (geographic barriers for transit analysis).
445
446
Args:
447
feed: Feed object containing trip and shape data
448
screen_lines: List of Shapely LineString geometries representing screen lines
449
dates: List of dates to analyze crossings for
450
451
Returns:
452
DataFrame with crossing counts by screen line, route, direction, and date
453
"""
454
```
455
456
## Usage Examples
457
458
### Basic Geometric Operations
459
460
```python
461
import gtfs_kit as gk
462
import geopandas as gpd
463
from shapely.geometry import Point, Polygon
464
465
# Load feed
466
feed = gk.read_feed("data/gtfs.zip")
467
468
# Convert stops to GeoDataFrame
469
stops_gdf = gk.geometrize_stops(feed.stops, use_utm=True)
470
print(f"Created {len(stops_gdf)} stop geometries")
471
print("CRS:", stops_gdf.crs)
472
473
# Convert shapes to GeoDataFrame
474
if feed.shapes is not None:
475
shapes_gdf = gk.geometrize_shapes(feed.shapes, use_utm=True)
476
print(f"Created {len(shapes_gdf)} shape geometries")
477
478
# Convert back to regular DataFrame
479
stops_df = gk.ungeometrize_stops(stops_gdf)
480
print("Converted back to lat/lon coordinates")
481
```
482
483
### Spatial Analysis
484
485
```python
486
# Compute feed bounds and centroid
487
bounds = gk.compute_bounds(feed)
488
print(f"Feed bounds: {bounds}") # [min_lon, min_lat, max_lon, max_lat]
489
490
hull = gk.compute_convex_hull(feed)
491
print(f"Convex hull area: {hull.area}")
492
493
centroid = gk.compute_centroid(feed)
494
print(f"Feed centroid: ({centroid.x}, {centroid.y})")
495
496
# Analyze specific area
497
area_of_interest = Polygon([
498
(bounds[0], bounds[1]),
499
(bounds[2], bounds[1]),
500
(bounds[2], bounds[3]),
501
(bounds[0], bounds[3])
502
])
503
504
stops_in_area = gk.get_stops_in_area(feed, area_of_interest)
505
print(f"Stops in area: {len(stops_in_area)}")
506
```
507
508
### Spatial Data Retrieval
509
510
```python
511
# Get stops as GeoDataFrame
512
sample_date = gk.get_dates(feed)[0]
513
stops_gdf = gk.get_stops(feed, date=sample_date, as_gdf=True, use_utm=False)
514
print(f"Active stops on {sample_date}: {len(stops_gdf)}")
515
516
# Get routes with geometries
517
routes_gdf = gk.get_routes(feed, date=sample_date, as_gdf=True, split_directions=True)
518
if len(routes_gdf) > 0:
519
print(f"Active routes: {len(routes_gdf)}")
520
print("Route geometry types:", routes_gdf.geometry.geom_type.value_counts())
521
522
# Get trips with shapes
523
trips_gdf = gk.get_trips(feed, date=sample_date, as_gdf=True)
524
trips_with_shapes = trips_gdf[~trips_gdf.geometry.isna()]
525
print(f"Trips with shapes: {len(trips_with_shapes)}")
526
```
527
528
### Feed Spatial Restriction
529
530
```python
531
# Define area of interest (e.g., city center)
532
city_center = Point(centroid.x, centroid.y).buffer(0.01) # ~1km buffer
533
534
# Restrict feed to area
535
area_feed = gk.restrict_to_area(feed, city_center)
536
print(f"Original feed: {len(feed.trips)} trips")
537
print(f"Restricted feed: {len(area_feed.trips)} trips")
538
539
# Compare service coverage
540
original_stops = len(feed.stops)
541
restricted_stops = len(area_feed.stops)
542
print(f"Stops: {original_stops} -> {restricted_stops} ({restricted_stops/original_stops:.1%})")
543
```
544
545
### Shape Analysis and Creation
546
547
```python
548
# Add distance information to shapes
549
if feed.shapes is not None:
550
feed_with_dists = gk.append_dist_to_shapes(feed)
551
print("Added shape_dist_traveled to shapes")
552
553
# Also add to stop_times
554
feed_with_dists = gk.append_dist_to_stop_times(feed_with_dists)
555
print("Added shape_dist_traveled to stop_times")
556
else:
557
# Create shapes from stop connections
558
feed_with_shapes = gk.create_shapes(feed, all_trips=True)
559
print(f"Created shapes for {len(feed_with_shapes.shapes)} shape sequences")
560
```
561
562
### Geometry Mapping and Intersection
563
564
```python
565
# Build geometry mappings
566
shape_geometries = gk.build_geometry_by_shape(feed, use_utm=True)
567
stop_geometries = gk.build_geometry_by_stop(feed, use_utm=True)
568
569
print(f"Built geometries for {len(shape_geometries)} shapes")
570
print(f"Built geometries for {len(stop_geometries)} stops")
571
572
# Find shapes intersecting with area
573
if feed.shapes is not None:
574
intersecting_shapes = gk.get_shapes_intersecting_geometry(feed, city_center, as_gdf=True)
575
if intersecting_shapes is not None:
576
print(f"Shapes intersecting area: {len(intersecting_shapes)}")
577
```
578
579
### Distance Unit Conversion
580
581
```python
582
# Check current units
583
print(f"Current distance units: {feed.dist_units}")
584
585
# Convert to different units
586
feed_metric = gk.convert_dist(feed, "km")
587
print(f"Converted to: {feed_metric.dist_units}")
588
589
# Convert to imperial
590
feed_imperial = gk.convert_dist(feed, "mi")
591
print(f"Converted to: {feed_imperial.dist_units}")
592
593
# Verify conversion functions
594
is_metric_km = gk.is_metric("km")
595
is_metric_mi = gk.is_metric("mi")
596
print(f"km is metric: {is_metric_km}, mi is metric: {is_metric_mi}")
597
```
598
599
### GeoJSON Export
600
601
```python
602
# Export routes to GeoJSON
603
routes_geojson = gk.routes_to_geojson(feed, split_directions=True, include_stops=True)
604
print(f"Routes GeoJSON has {len(routes_geojson['features'])} features")
605
606
# Export specific stops
607
major_stops = gk.get_stops(feed, as_gdf=True).nlargest(10, 'stop_id')['stop_id'].tolist()
608
stops_geojson = gk.stops_to_geojson(feed, stop_ids=major_stops)
609
610
# Export trip paths
611
sample_trips = feed.trips['trip_id'].head(5).tolist()
612
trips_geojson = gk.trips_to_geojson(feed, trip_ids=sample_trips, include_stops=True)
613
614
# Export shapes
615
if feed.shapes is not None:
616
shapes_geojson = gk.shapes_to_geojson(feed)
617
print(f"Shapes GeoJSON has {len(shapes_geojson['features'])} features")
618
619
# Export stop times for specific trips
620
stop_times_geojson = gk.stop_times_to_geojson(feed, trip_ids=sample_trips)
621
```
622
623
### Interactive Mapping
624
625
```python
626
# Create route map
627
gk.map_routes(feed,
628
route_ids=None, # All routes
629
color_palette=['red', 'blue', 'green', 'orange', 'purple'],
630
show_stops=True)
631
632
# Map specific stops with custom styling
633
custom_style = {
634
'radius': 8,
635
'fillColor': 'blue',
636
'color': 'black',
637
'weight': 2,
638
'fillOpacity': 0.7
639
}
640
641
gk.map_stops(feed,
642
stop_ids=major_stops,
643
stop_style=custom_style)
644
645
# Map trip paths with directions
646
gk.map_trips(feed,
647
trip_ids=sample_trips,
648
color_palette=['red', 'blue', 'green'],
649
show_stops=True,
650
show_direction=True)
651
652
# Use default stop styling
653
print("Default stop style:", gk.STOP_STYLE)
654
```
655
656
### Screen Line Analysis
657
658
```python
659
from shapely.geometry import LineString
660
661
# Define screen lines (e.g., major barriers like rivers or highways)
662
screen_line1 = LineString([(centroid.x - 0.01, centroid.y - 0.01),
663
(centroid.x + 0.01, centroid.y + 0.01)])
664
screen_line2 = LineString([(centroid.x - 0.01, centroid.y + 0.01),
665
(centroid.x + 0.01, centroid.y - 0.01)])
666
667
screen_lines = [screen_line1, screen_line2]
668
669
# Analyze crossings
670
dates = gk.get_dates(feed)[:7] # First week
671
crossings = gk.compute_screen_line_counts(feed, screen_lines, dates)
672
673
if len(crossings) > 0:
674
print("Screen line crossings:")
675
print(crossings.groupby(['screen_line_id', 'date'])['num_crossings'].sum())
676
```