@golemio/public-transport 0.1.0-dev.2901322475 → 0.1.0-dev.2906983689
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.
- package/dist/output-gateway/gtfs/models/GTFSShapesModel.d.ts +6 -0
- package/dist/output-gateway/gtfs/models/GTFSShapesModel.js +10 -0
- package/dist/output-gateway/gtfs/models/GTFSShapesModel.js.map +1 -1
- package/dist/output-gateway/public/service/helpers/trip-scope/strategy/ShapesTripScopeHandler.d.ts +4 -1
- package/dist/output-gateway/public/service/helpers/trip-scope/strategy/ShapesTripScopeHandler.js +20 -12
- package/dist/output-gateway/public/service/helpers/trip-scope/strategy/ShapesTripScopeHandler.js.map +1 -1
- package/docs/follow-ups.md +9 -7
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { IShapeDto } from "../../../schema-definitions/gtfs/interfaces/IShapeDto";
|
|
1
2
|
import { SequelizeModel } from "@golemio/core/dist/output-gateway";
|
|
2
3
|
import { Feature, LineString } from "@golemio/core/dist/shared/geojson";
|
|
3
4
|
import { IGTFSModels } from "./interfaces/IGTFSModels";
|
|
@@ -28,6 +29,11 @@ export declare class GTFSShapesModel extends SequelizeModel {
|
|
|
28
29
|
id: string;
|
|
29
30
|
feedId?: number;
|
|
30
31
|
}) => Promise<ShapeLineStringFeature | null>;
|
|
32
|
+
/** Retrieves the points of one feed's shape in vertex order, empty when the shape does not exist */
|
|
33
|
+
GetOrderedPoints: (options: {
|
|
34
|
+
id: string;
|
|
35
|
+
feedId: number;
|
|
36
|
+
}) => Promise<IShapeDto[]>;
|
|
31
37
|
GetOne: (id: number) => Promise<object | null>;
|
|
32
38
|
private findOrderedPoints;
|
|
33
39
|
}
|
|
@@ -58,6 +58,16 @@ class GTFSShapesModel extends output_gateway_1.SequelizeModel {
|
|
|
58
58
|
throw new golemio_errors_1.GeneralError("Database error ~ GetLineString", "GTFSShapesModel", err, 500);
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
|
+
/** Retrieves the points of one feed's shape in vertex order, empty when the shape does not exist */
|
|
62
|
+
this.GetOrderedPoints = async (options) => {
|
|
63
|
+
try {
|
|
64
|
+
const data = await this.findOrderedPoints(options);
|
|
65
|
+
return data.map((point) => point.get({ plain: true }));
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
throw new golemio_errors_1.GeneralError("Database error ~ GetOrderedPoints", "GTFSShapesModel", err, 500);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
61
71
|
this.GetOne = async (id) => {
|
|
62
72
|
return null;
|
|
63
73
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GTFSShapesModel.js","sourceRoot":"","sources":["../../../../src/output-gateway/gtfs/models/GTFSShapesModel.ts"],"names":[],"mappings":";;;AAAA,6DAAuC;AACvC,2DAAiC;
|
|
1
|
+
{"version":3,"file":"GTFSShapesModel.js","sourceRoot":"","sources":["../../../../src/output-gateway/gtfs/models/GTFSShapesModel.ts"],"names":[],"mappings":"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"}
|
package/dist/output-gateway/public/service/helpers/trip-scope/strategy/ShapesTripScopeHandler.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { IPublicApiDetailedTrip } from "../../../../domain/PublicApiDetailedTripInterfaces";
|
|
2
2
|
import { IDelayComputationRepository } from "../../../../domain/repository/IDelayComputationRepository";
|
|
3
|
+
import { PublicGtfsTripShapesTransformation } from "../../../transformations/gtfs-trip-scopes/PublicGtfsTripShapesTransformation";
|
|
3
4
|
import { GTFSShapesModel } from "../../../../../gtfs/models/GTFSShapesModel";
|
|
4
5
|
import { IPublicApiCacheDto } from "../../../../../../schema-definitions/vehicle-positions/redis/interfaces/IPublicApiCacheDto";
|
|
5
6
|
import { AbstractDetailedTripScopeHandler } from "./AbstractDetailedTripScopeHandler";
|
|
6
7
|
export declare class ShapesTripScopeHandler extends AbstractDetailedTripScopeHandler {
|
|
7
8
|
private delayComputationRepository;
|
|
9
|
+
private gtfsShapesTransformation;
|
|
8
10
|
private tripShapesTransformation;
|
|
9
11
|
protected shapeModel: GTFSShapesModel;
|
|
10
|
-
constructor(delayComputationRepository: IDelayComputationRepository);
|
|
12
|
+
constructor(delayComputationRepository: IDelayComputationRepository, gtfsShapesTransformation: PublicGtfsTripShapesTransformation);
|
|
11
13
|
handle(output: IPublicApiDetailedTrip, vehiclePosition: IPublicApiCacheDto): Promise<IPublicApiDetailedTrip>;
|
|
14
|
+
private getShapeFeatures;
|
|
12
15
|
}
|
package/dist/output-gateway/public/service/helpers/trip-scope/strategy/ShapesTripScopeHandler.js
CHANGED
|
@@ -16,37 +16,45 @@ exports.ShapesTripScopeHandler = void 0;
|
|
|
16
16
|
const PublicCacheKeys_1 = require("../../../../../../schema-definitions/vehicle-positions/redis/PublicCacheKeys");
|
|
17
17
|
const OgModuleToken_1 = require("../../../../ioc/OgModuleToken");
|
|
18
18
|
const PublicVPTripShapesTransformation_1 = require("../../../transformations/scopes/PublicVPTripShapesTransformation");
|
|
19
|
+
const PublicGtfsTripShapesTransformation_1 = require("../../../transformations/gtfs-trip-scopes/PublicGtfsTripShapesTransformation");
|
|
19
20
|
const models_1 = require("../../../../../gtfs/models");
|
|
20
21
|
const Geo_1 = require("@golemio/core/dist/output-gateway/Geo");
|
|
21
22
|
const tsyringe_1 = require("@golemio/core/dist/shared/tsyringe");
|
|
22
23
|
const AbstractDetailedTripScopeHandler_1 = require("./AbstractDetailedTripScopeHandler");
|
|
23
24
|
let ShapesTripScopeHandler = class ShapesTripScopeHandler extends AbstractDetailedTripScopeHandler_1.AbstractDetailedTripScopeHandler {
|
|
24
|
-
constructor(delayComputationRepository) {
|
|
25
|
+
constructor(delayComputationRepository, gtfsShapesTransformation) {
|
|
25
26
|
super();
|
|
26
27
|
this.delayComputationRepository = delayComputationRepository;
|
|
28
|
+
this.gtfsShapesTransformation = gtfsShapesTransformation;
|
|
27
29
|
this.tripShapesTransformation = new PublicVPTripShapesTransformation_1.PublicVPTripShapesTransformation();
|
|
28
30
|
this.shapeModel = models_1.models.GTFSShapesModel;
|
|
29
31
|
}
|
|
30
32
|
async handle(output, vehiclePosition) {
|
|
31
33
|
// same feed source as DetailedTripFacade: vehicle ids are <feedId>-<feedLocalId>
|
|
32
34
|
const feedId = PublicCacheKeys_1.PublicCacheKeys.feedIdFromVehicleId(vehiclePosition.vehicle_id);
|
|
33
|
-
|
|
34
|
-
? null
|
|
35
|
-
: await this.delayComputationRepository.getDelayComputationCacheProperty(feedId, vehiclePosition.gtfs_trip_id, "shapes");
|
|
36
|
-
// only CIS/MPV pipelines compute delays (UpdateDelayTask); polled GTFS-RT feeds have none, so the
|
|
37
|
-
// scope is empty instead of failing the whole detail response
|
|
38
|
-
if (!delayComputationCache) {
|
|
39
|
-
output.shapes = (0, Geo_1.buildGeojsonFeatureCollection)([]);
|
|
40
|
-
return output;
|
|
41
|
-
}
|
|
42
|
-
output.shapes = (0, Geo_1.buildGeojsonFeatureCollection)(delayComputationCache.shapes ? this.tripShapesTransformation.transformArray(delayComputationCache.shapes) : []);
|
|
35
|
+
output.shapes = (0, Geo_1.buildGeojsonFeatureCollection)(feedId === null ? [] : await this.getShapeFeatures(feedId, vehiclePosition));
|
|
43
36
|
return output;
|
|
44
37
|
}
|
|
38
|
+
async getShapeFeatures(feedId, vehiclePosition) {
|
|
39
|
+
const delayComputationCache = await this.delayComputationRepository.getDelayComputationCacheProperty(feedId, vehiclePosition.gtfs_trip_id, "shapes");
|
|
40
|
+
if (delayComputationCache) {
|
|
41
|
+
return delayComputationCache.shapes ? this.tripShapesTransformation.transformArray(delayComputationCache.shapes) : [];
|
|
42
|
+
}
|
|
43
|
+
// only CIS/MPV pipelines compute delays (UpdateDelayTask); polled GTFS-RT feeds have none, so their
|
|
44
|
+
// shapes come straight from the feed's static GTFS
|
|
45
|
+
const shapeId = vehiclePosition.detailed_info.shape_id;
|
|
46
|
+
if (!shapeId) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const shapePoints = await this.shapeModel.GetOrderedPoints({ id: shapeId, feedId });
|
|
50
|
+
return this.gtfsShapesTransformation.transformArray(shapePoints);
|
|
51
|
+
}
|
|
45
52
|
};
|
|
46
53
|
exports.ShapesTripScopeHandler = ShapesTripScopeHandler;
|
|
47
54
|
exports.ShapesTripScopeHandler = ShapesTripScopeHandler = __decorate([
|
|
48
55
|
(0, tsyringe_1.injectable)(),
|
|
49
56
|
__param(0, (0, tsyringe_1.inject)(OgModuleToken_1.OgModuleToken.DelayComputationRepository)),
|
|
50
|
-
|
|
57
|
+
__param(1, (0, tsyringe_1.inject)(OgModuleToken_1.OgModuleToken.PublicGtfsTripShapesTransformation)),
|
|
58
|
+
__metadata("design:paramtypes", [Object, PublicGtfsTripShapesTransformation_1.PublicGtfsTripShapesTransformation])
|
|
51
59
|
], ShapesTripScopeHandler);
|
|
52
60
|
//# sourceMappingURL=ShapesTripScopeHandler.js.map
|
package/dist/output-gateway/public/service/helpers/trip-scope/strategy/ShapesTripScopeHandler.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ShapesTripScopeHandler.js","sourceRoot":"","sources":["../../../../../../../src/output-gateway/public/service/helpers/trip-scope/strategy/ShapesTripScopeHandler.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAEA,kHAA+E;AAC/E,iEAA6D;AAC7D,uHAA8H;AAC9H,uDAAyC;AAGzC,+
|
|
1
|
+
{"version":3,"file":"ShapesTripScopeHandler.js","sourceRoot":"","sources":["../../../../../../../src/output-gateway/public/service/helpers/trip-scope/strategy/ShapesTripScopeHandler.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAEA,kHAA+E;AAC/E,iEAA6D;AAC7D,uHAA8H;AAC9H,qIAA4I;AAC5I,uDAAyC;AAGzC,+DAAuG;AACvG,iEAAwE;AACxE,yFAAsF;AAG/E,IAAM,sBAAsB,GAA5B,MAAM,sBAAuB,SAAQ,mEAAgC;IAIxE,YAC8D,0BAAuD,EAEzG,wBAA4D;QAEpE,KAAK,EAAE,CAAC;QAJkD,+BAA0B,GAA1B,0BAA0B,CAA6B;QAEzG,6BAAwB,GAAxB,wBAAwB,CAAoC;QAGpE,IAAI,CAAC,wBAAwB,GAAG,IAAI,mEAAgC,EAAE,CAAC;QACvE,IAAI,CAAC,UAAU,GAAG,eAAM,CAAC,eAAe,CAAC;IAC7C,CAAC;IAEM,KAAK,CAAC,MAAM,CAAC,MAA8B,EAAE,eAAmC;QACnF,iFAAiF;QACjF,MAAM,MAAM,GAAG,iCAAe,CAAC,mBAAmB,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;QAC/E,MAAM,CAAC,MAAM,GAAG,IAAA,mCAA6B,EACzC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,EAAE,eAAe,CAAC,CAC9E,CAAC;QACF,OAAO,MAAM,CAAC;IAClB,CAAC;IAEO,KAAK,CAAC,gBAAgB,CAAC,MAAc,EAAE,eAAmC;QAC9E,MAAM,qBAAqB,GAAG,MAAM,IAAI,CAAC,0BAA0B,CAAC,gCAAgC,CAChG,MAAM,EACN,eAAe,CAAC,YAAY,EAC5B,QAAQ,CACX,CAAC;QACF,IAAI,qBAAqB,EAAE,CAAC;YACxB,OAAO,qBAAqB,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,wBAAwB,CAAC,cAAc,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1H,CAAC;QAED,oGAAoG;QACpG,mDAAmD;QACnD,MAAM,OAAO,GAAG,eAAe,CAAC,aAAa,CAAC,QAAQ,CAAC;QACvD,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,EAAE,CAAC;QACd,CAAC;QAED,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;QACpF,OAAO,IAAI,CAAC,wBAAwB,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;IACrE,CAAC;CACJ,CAAA;AA3CY,wDAAsB;iCAAtB,sBAAsB;IADlC,IAAA,qBAAU,GAAE;IAMJ,WAAA,IAAA,iBAAM,EAAC,6BAAa,CAAC,0BAA0B,CAAC,CAAA;IAChD,WAAA,IAAA,iBAAM,EAAC,6BAAa,CAAC,kCAAkC,CAAC,CAAA;6CACvB,uEAAkC;GAP/D,sBAAsB,CA2ClC"}
|
package/docs/follow-ups.md
CHANGED
|
@@ -9,7 +9,8 @@ Resolved and removed so far: gtfs_metadata feed scoping; the OG active endpoints
|
|
|
9
9
|
run numbers; the shapes.txt staging limit (stage ingests the full 193 MB fine; the local dev Redis
|
|
10
10
|
still truncates such files — a local-environment quirk, not a code bug); the destructive-migration
|
|
11
11
|
convention (schema changes now pair the squashed init edit with an incremental migration — see
|
|
12
|
-
CLAUDE.md, first instance `20260820000000-widen-trip-number`)
|
|
12
|
+
CLAUDE.md, first instance `20260820000000-widen-trip-number`); the empty public VP detail `shapes` scope for
|
|
13
|
+
polled GTFS-RT feeds (now read from static GTFS, see #13).
|
|
13
14
|
|
|
14
15
|
---
|
|
15
16
|
|
|
@@ -37,9 +38,9 @@ Disabled-router leftovers to address when these move/return:
|
|
|
37
38
|
|
|
38
39
|
**Trigger: when feed metadata (validity window, publisher) is needed per feed.**
|
|
39
40
|
|
|
40
|
-
The GTFS storage import
|
|
41
|
+
The GTFS storage import accepts `feed_info.txt` (optional) in the feed zip but there is no
|
|
41
42
|
`gtfs_feed_info` table or transformation — the file is skipped after validation
|
|
42
|
-
(`ImportGtfsFromStorageTask`, `
|
|
43
|
+
(`ImportGtfsFromStorageTask`, `OPTIONAL_GTFS_FILES`).
|
|
43
44
|
|
|
44
45
|
## 3. apidocs-test-input cannot pass (tooling)
|
|
45
46
|
|
|
@@ -201,10 +202,11 @@ own `refreshPublicTripCache {feedCode}` loop under a per-feed mutex; key format
|
|
|
201
202
|
Vehicle ids are `<feedId>-<vehicle>` for every feed so they stay unique across feeds: the stop-time cache keys
|
|
202
203
|
and the detail endpoint (which derives the feed from the id) rely on it. The detail
|
|
203
204
|
`stop_times`/`shapes` scopes read the delay computation of the vehicle's own feed; only the CIS/MPV pipelines
|
|
204
|
-
(`UpdateDelayTask`) write it,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
205
|
+
(`UpdateDelayTask`) write it. Without it, `shapes` falls back to the feed's static GTFS shape (by
|
|
206
|
+
`(feed_id, shape_id)`, raw GTFS points instead of the resampled ones), but `stop_times` comes back empty for
|
|
207
|
+
polled GTFS-RT feeds (fill it from static GTFS by `(feed_id, trip_id)` when a polled feed needs it). A
|
|
208
|
+
vehicle without identity in its feed (no registration or CIS number, e.g. polled GTFS-RT) is keyed by its VP
|
|
209
|
+
trip, so it gets a new `vehicle_id` when its trip changes.
|
|
208
210
|
|
|
209
211
|
Left as is:
|
|
210
212
|
- **`vehicle_descriptor` scope** joins descriptors by registration number only (see #6).
|