Skip to content

Commit f5f51a9

Browse files
authored
feat: Add bounding box and pmtiles information at feed level (#1355)
1 parent b17e4c0 commit f5f51a9

File tree

13 files changed

+142
-13
lines changed

13 files changed

+142
-13
lines changed

api/src/feeds/impl/models/gtfs_feed_impl.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from feeds.impl.models.bounding_box_impl import BoundingBoxImpl
12
from feeds.impl.models.feed_impl import FeedImpl
23
from shared.database_gen.sqlacodegen_models import Gtfsfeed as GtfsfeedOrm
34
from feeds.impl.models.latest_dataset_impl import LatestDatasetImpl
@@ -26,4 +27,8 @@ def from_orm(cls, feed: GtfsfeedOrm | None) -> GtfsFeed | None:
2627
(dataset for dataset in feed.gtfsdatasets if dataset is not None and dataset.latest), None
2728
)
2829
gtfs_feed.latest_dataset = LatestDatasetImpl.from_orm(latest_dataset)
30+
gtfs_feed.bounding_box = BoundingBoxImpl.from_orm(feed.bounding_box)
31+
gtfs_feed.visualization_dataset_id = (
32+
feed.visualization_dataset.stable_id if feed.visualization_dataset else None
33+
)
2934
return gtfs_feed

api/src/shared/common/db_utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def get_gtfs_feeds_query(
8686
contains_eager(Gtfsfeed.gtfsdatasets)
8787
.joinedload(Gtfsdataset.validation_reports)
8888
.joinedload(Validationreport.features),
89+
joinedload(Gtfsfeed.visualization_dataset),
8990
*get_joinedload_options(),
9091
).order_by(Gtfsfeed.provider, Gtfsfeed.stable_id)
9192

@@ -182,6 +183,7 @@ def get_all_gtfs_feeds(
182183
contains_eager(Gtfsfeed.gtfsdatasets)
183184
.joinedload(Gtfsdataset.validation_reports)
184185
.joinedload(Validationreport.features),
186+
joinedload(Gtfsfeed.visualization_dataset),
185187
*get_joinedload_options(include_extracted_location_entities=True),
186188
)
187189
)

api/tests/integration/cascade_delete/test_cascade_delete.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,10 @@ def test_delete_feed_cascadeto_gtfsdataset(test_database):
119119

120120
with test_database.start_db_session() as session:
121121
feed = Feed(id="f1")
122+
session.add(feed)
123+
session.flush()
122124
dataset = Gtfsdataset(id="d1", feed_id="f1")
123-
session.add_all([feed, dataset])
125+
session.add(dataset)
124126
session.commit()
125127

126128
delete_and_assert(

docs/DatabaseCatalogAPI.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,13 @@ components:
472472
$ref: "#/components/schemas/Locations"
473473
latest_dataset:
474474
$ref: "#/components/schemas/LatestDataset"
475+
bounding_box:
476+
$ref: "#/components/schemas/BoundingBox"
477+
visualization_dataset_id:
478+
description: >
479+
The dataset ID of the dataset used to compute the visualization files.
480+
type: string
481+
example: mdb-1210-202402121801
475482

476483
GbfsFeed:
477484
allOf:

functions-python/pmtiles_builder/src/main.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@
2424
import subprocess
2525
import tempfile
2626
from enum import Enum
27+
28+
import flask
29+
import functions_framework
2730
from google.cloud import storage
31+
from sqlalchemy.orm import Session
2832

2933
from csv_cache import (
3034
CsvCache,
@@ -35,12 +39,11 @@
3539
AGENCY_FILE,
3640
SHAPES_FILE,
3741
)
38-
from shared.helpers.runtime_metrics import track_metrics
39-
from shared.helpers.logger import get_logger, init_logger
4042
from gtfs_stops_to_geojson import convert_stops_to_geojson
41-
42-
import flask
43-
import functions_framework
43+
from shared.database_gen.sqlacodegen_models import Gtfsdataset, Gtfsfeed
44+
from shared.helpers.logger import get_logger, init_logger
45+
from shared.helpers.runtime_metrics import track_metrics
46+
from shared.database.database import with_db_session
4447

4548
init_logger()
4649

@@ -198,6 +201,7 @@ def build_pmtiles(self):
198201

199202
files_to_upload = ["routes.pmtiles", "stops.pmtiles", "routes.json"]
200203
self._upload_files_to_gcs(files_to_upload)
204+
self._update_database()
201205

202206
return self.OperationStatus.SUCCESS, "success"
203207

@@ -521,3 +525,30 @@ def _load_agencies(self):
521525
agencies[agency_id] = agency_name
522526

523527
return agencies
528+
529+
@with_db_session
530+
def _update_database(self, db_session: Session = None):
531+
dataset = (
532+
db_session.query(Gtfsdataset)
533+
.filter(Gtfsdataset.stable_id == self.dataset_stable_id)
534+
.one_or_none()
535+
)
536+
if not dataset:
537+
self.logger.error(
538+
"Dataset %s not found in database, cannot update pmtiles_generated.",
539+
self.dataset_stable_id,
540+
)
541+
return
542+
543+
# fetch the subclass row that shares the same PK as Feed
544+
gtfsfeed = db_session.get(Gtfsfeed, dataset.feed_id)
545+
if not gtfsfeed:
546+
self.logger.error(
547+
"Gtfsfeed(id=%s) not found (but Feed exists) — cannot set visualization_dataset.",
548+
dataset.feed_id,
549+
)
550+
return
551+
552+
# set the relationship on the subclass
553+
gtfsfeed.visualization_dataset = dataset
554+
db_session.commit()

functions-python/reverse_geolocation/src/reverse_geolocation_processor.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ def get_storage_client():
213213
@with_db_session
214214
@track_metrics(metrics=("time", "memory", "cpu"))
215215
def update_dataset_bounding_box(
216-
dataset_id: str, stops_df: pd.DataFrame, logger: logging.Logger, db_session: Session
216+
dataset_id: str, stops_df: pd.DataFrame, db_session: Session
217217
) -> shapely.Polygon:
218218
"""
219219
Update the bounding box of the dataset using the stops DataFrame.
@@ -240,7 +240,15 @@ def update_dataset_bounding_box(
240240
)
241241
if not gtfs_dataset:
242242
raise ValueError(f"Dataset {dataset_id} does not exist in the database.")
243+
gtfs_feed = db_session.get(Gtfsfeed, gtfs_dataset.feed_id)
244+
if not gtfs_feed:
245+
raise ValueError(
246+
f"GTFS feed for dataset {dataset_id} does not exist in the database."
247+
)
248+
gtfs_feed.bounding_box = bounding_box
249+
gtfs_feed.bounding_box_dataset = gtfs_dataset
243250
gtfs_dataset.bounding_box = bounding_box
251+
244252
return to_shape(bounding_box)
245253

246254

@@ -323,7 +331,7 @@ def reverse_geolocation_process(
323331

324332
try:
325333
# Update the bounding box of the dataset
326-
bounding_box = update_dataset_bounding_box(dataset_id, stops_df, logger)
334+
bounding_box = update_dataset_bounding_box(dataset_id, stops_df)
327335

328336
location_groups = reverse_geolocation(
329337
strategy=strategy,

functions-python/reverse_geolocation/tests/test_reverse_geolocation_processor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ def test_update_dataset_bounding_box_success(self, db_session):
375375

376376
# Call the function
377377
bounding_box = update_dataset_bounding_box(
378-
dataset_id, stops_df, db_session=db_session, logger=logger
378+
dataset_id, stops_df, db_session=db_session
379379
)
380380

381381
# Expected bounding box: POLYGON((30 10, 40 10, 40 20, 30 20, 30 10))

liquibase/changelog.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,6 @@
6666
<include file="changes/feat_1265_cascade_delete.sql" relativeToChangelogFile="true"/>
6767
<include file="changes/feat_1259.sql" relativeToChangelogFile="true"/>
6868
<include file="changes/feat_1260.sql" relativeToChangelogFile="true"/>
69+
<include file="changes/feat_1333.sql" relativeToChangelogFile="true"/>
6970
<include file="changes/feat_pt_152.sql" relativeToChangelogFile="true"/>
7071
</databaseChangeLog>

liquibase/changes/feat_1333.sql

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
-- Drop old columns if they exist (cleanup for consistency / schema reset)
2+
ALTER TABLE Gtfsfeed DROP COLUMN IF EXISTS visualization_dataset_id;
3+
ALTER TABLE Gtfsfeed DROP COLUMN IF EXISTS bounding_box_dataset_id;
4+
ALTER TABLE Gtfsfeed DROP COLUMN IF EXISTS bounding_box;
5+
6+
-- Add new columns with proper definitions
7+
-- visualization_dataset_id: references the dataset used for UI/visualization
8+
ALTER TABLE Gtfsfeed
9+
ADD COLUMN visualization_dataset_id VARCHAR(255) REFERENCES Gtfsdataset(id);
10+
11+
-- bounding_box_dataset_id: references the dataset that provided the bounding box
12+
ALTER TABLE Gtfsfeed
13+
ADD COLUMN bounding_box_dataset_id VARCHAR(255) REFERENCES Gtfsdataset(id);
14+
15+
-- bounding_box: stores the geometry for the feed’s bounding box (WGS84 / SRID 4326)
16+
ALTER TABLE Gtfsfeed
17+
ADD COLUMN bounding_box geometry(Polygon, 4326);
18+
19+
-- Populate the bounding box info
20+
-- For each feed, find the most recent dataset that has a bounding box
21+
-- (ROW_NUMBER() = 1 keeps only the latest per feed_id)
22+
WITH latest AS (
23+
SELECT
24+
id,
25+
feed_id,
26+
bounding_box,
27+
ROW_NUMBER() OVER (
28+
PARTITION BY feed_id
29+
ORDER BY downloaded_at DESC NULLS LAST, id DESC
30+
) AS rn
31+
FROM Gtfsdataset
32+
WHERE bounding_box IS NOT NULL
33+
)
34+
UPDATE Gtfsfeed gf
35+
SET
36+
bounding_box_dataset_id = l.id, -- link feed to the chosen dataset
37+
bounding_box = l.bounding_box -- copy its bounding box geometry
38+
FROM latest l
39+
WHERE l.rn = 1 -- keep only the latest dataset per feed
40+
AND gf.id = l.feed_id; -- match feed with its dataset
41+

web-app/src/app/screens/Feed/components/FullMapView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import {
1515
StyledMapControlPanel,
1616
} from '../Map.styles';
1717
import {
18-
selectBoundingBoxFromLatestDataset,
1918
selectGtfsDatasetRoutesJson,
2019
selectGtfsDatasetRouteTypes,
20+
selectGtfsFeedBoundingBox,
2121
selectLatestDatasetsData,
2222
} from '../../../store/selectors';
2323
import { useSelector } from 'react-redux';
@@ -40,7 +40,7 @@ export default function FullMapView(): React.ReactElement {
4040
useState<boolean>(false);
4141
const { config } = useRemoteConfig();
4242
const latestDataset = useSelector(selectLatestDatasetsData);
43-
const boundingBox = useSelector(selectBoundingBoxFromLatestDataset);
43+
const boundingBox = useSelector(selectGtfsFeedBoundingBox);
4444
const routes = useSelector(selectGtfsDatasetRoutesJson);
4545
const routeTypes = useSelector(selectGtfsDatasetRouteTypes);
4646
const dispatch = useAppDispatch();

0 commit comments

Comments
 (0)