Skip to content

Commit bcc2299

Browse files
committed
nodes: add a query to get transferable nodes between paths
This query takes 2 arrays of paths: one for the unboarding nodes and another for the boarding nodes. It will find all the pairs of nodes that can transfer from one path to the other, returning the transfer travel time and distance. If many nodes are reachable from a path/node pair, it will return the one with the smallest time to reach.
1 parent 42845f0 commit bcc2299

File tree

2 files changed

+296
-1
lines changed

2 files changed

+296
-1
lines changed

packages/transition-backend/src/models/db/__tests__/transitNodeTransferable.db.test.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import knex from 'chaire-lib-backend/lib/config/shared/db.config';
99

1010
import dbQueries from '../transitNodeTransferable.db.queries';
1111
import nodesDbQueries from '../transitNodes.db.queries';
12+
import pathDbQueries from '../transitPaths.db.queries';
13+
import linesDbQueries from '../transitLines.db.queries';
14+
import agencyDbQueries from '../transitAgencies.db.queries'
1215

1316
const objectName = 'transferable node';
1417

@@ -42,6 +45,7 @@ beforeAll(async () => {
4245

4346
afterAll(async () => {
4447
await dbQueries.truncate();
48+
await agencyDbQueries.truncate();
4549
await nodesDbQueries.truncate();
4650
await knex.destroy();
4751
});
@@ -187,4 +191,177 @@ describe(`${objectName}`, () => {
187191
expect(transferableNodes).toEqual([]);
188192
});
189193

194+
});
195+
196+
describe('getTransferableNodePairs', () => {
197+
// Make another line of nodes
198+
const otherNodeAttributes = [0, 1, 2, 3, 4, 5].map(idx => ({
199+
id: uuidV4(),
200+
code: `000${idx + 10}`,
201+
name: `NewNode ${idx + 10}`,
202+
internal_id: `Test${idx + 10}`,
203+
integer_id: idx + 10,
204+
geography: {
205+
type: "Point" as const,
206+
coordinates: [-72.995 - 0.001 * idx, 45 + 0.001 * idx]
207+
},
208+
color: '#ffff00',
209+
is_enabled: true,
210+
is_frozen: false,
211+
description: `New node description ${idx}`,
212+
default_dwell_time_seconds: 25,
213+
routing_radius_meters: 50,
214+
data: {
215+
foo: 'bar'
216+
}
217+
}));
218+
const agencyId = uuidV4();
219+
const lineIds = [uuidV4(), uuidV4()];
220+
const pathIds = [uuidV4(), uuidV4(), uuidV4(), uuidV4()];
221+
const pathNodes = [
222+
// Path stops at each node
223+
nodeAttributes.map(node => node.id),
224+
// Path stops at nodes with even indexes
225+
nodeAttributes.filter((node, idx) => idx % 2 === 0).map(node => node.id),
226+
// Path stops at each other node node
227+
otherNodeAttributes.map(node => node.id),
228+
// Path stops at other nodes with even indexes
229+
otherNodeAttributes.filter((node, idx) => idx % 2 === 0).map(node => node.id)
230+
]
231+
beforeAll(async () => {
232+
await dbQueries.truncate();
233+
// This should delete also lines and paths
234+
await agencyDbQueries.truncate();
235+
await nodesDbQueries.truncate();
236+
await nodesDbQueries.createMultiple([...nodeAttributes, ...otherNodeAttributes]);
237+
// Create an agency, 2 lines and 4 paths
238+
await agencyDbQueries.create({
239+
id: agencyId,
240+
name: 'test',
241+
acronym: 'test'
242+
} as any);
243+
await linesDbQueries.createMultiple([{
244+
id: lineIds[0],
245+
shortname: 'test1',
246+
agency_id: agencyId,
247+
color: '#ffffff',
248+
}, {
249+
id: lineIds[1],
250+
shortname: 'test2',
251+
agency_id: agencyId,
252+
color: '#ffffff',
253+
}] as any);
254+
await pathDbQueries.createMultiple([{
255+
id: pathIds[0],// Path stops at each other node node
256+
line_id: lineIds[0],
257+
nodes: pathNodes[0],
258+
}, {
259+
id: pathIds[1],
260+
line_id: lineIds[0],
261+
nodes: pathNodes[1],
262+
}, {
263+
id: pathIds[2],
264+
line_id: lineIds[1],
265+
nodes: pathNodes[2],
266+
}, {
267+
id: pathIds[3],
268+
line_id: lineIds[1],
269+
nodes: pathNodes[3],
270+
}] as any);
271+
272+
// For each node in nodeAttributes and otherNodeAttributes, fake some
273+
// transferable nodes data, the real number does not matter,
274+
// even-indexed nodes from the 2 series are not tranferrable
275+
for (let i = 0; i < nodeAttributes.length; i++) {
276+
const node = nodeAttributes[i];
277+
// Ignore the node itself from the array, and add transfers from the other series, ignoring even if the current index is even
278+
const transferableNodeIds = [...nodeAttributes.filter((n, idx) => idx !== i).map(n => n.id), ...otherNodeAttributes.filter((n, idx) => i % 2 !== 0 || idx % 2 !== 0).map(n => n.id)];
279+
await dbQueries.saveForNode(node.id, {
280+
nodesIds: transferableNodeIds,
281+
walkingTravelTimesSeconds: transferableNodeIds.map((id, idx) => (100 + i) * idx),
282+
walkingDistancesMeters: transferableNodeIds.map((id, idx) => (150 + i) * idx),
283+
});
284+
}
285+
for (let i = 0; i < otherNodeAttributes.length; i++) {
286+
const node = otherNodeAttributes[i];
287+
// Ignore the node itself from the array, and add transfers from the other series, ignoring even if the current index is even
288+
const transferableNodeIds = [...otherNodeAttributes.filter((n, idx) => idx !== i).map(n => n.id), ...nodeAttributes.filter((n, idx) => i % 2 !== 0 || idx % 2 !== 0).map(n => n.id)];
289+
await dbQueries.saveForNode(node.id, {
290+
nodesIds: transferableNodeIds,
291+
walkingTravelTimesSeconds: transferableNodeIds.map((id, idx) => (120 + i) * idx),
292+
walkingDistancesMeters: transferableNodeIds.map((id, idx) => (180 + i) * idx),
293+
});
294+
}
295+
});
296+
297+
afterAll(async () => {
298+
await dbQueries.truncate();
299+
// This should delete also lines and paths
300+
await agencyDbQueries.truncate();
301+
await nodesDbQueries.truncate();
302+
});
303+
304+
test('From/to paths no in database', async () => {
305+
expect(await dbQueries.getTransferableNodePairs({
306+
pathsFrom: [uuidV4(), uuidV4()],
307+
pathsTo: [uuidV4()]
308+
})).toEqual([]);
309+
});
310+
311+
test('No transferable nodes between paths', async () => {
312+
expect(await dbQueries.getTransferableNodePairs({
313+
pathsFrom: [pathIds[1]],
314+
pathsTo: [pathIds[3]]
315+
})).toEqual([]);
316+
});
317+
318+
test('Transferable nodes between paths', async () => {
319+
const transferrableNodes = await dbQueries.getTransferableNodePairs({
320+
pathsFrom: [pathIds[0]],
321+
pathsTo: [pathIds[2]]
322+
});
323+
// There should be one transferable node per node of the path
324+
expect(transferrableNodes.length).toEqual(pathNodes[0].length);
325+
326+
// Make sure there are no duplicate entries for 2 paths and a node
327+
const noDuplicates1 = new Set(transferrableNodes.map(pair => `${pair.from.pathId}-${pair.from.nodeId}-${pair.to.pathId}`));
328+
expect(noDuplicates1.size).toBe(transferrableNodes.length);
329+
330+
// pathIds[1] has less node, so there should be less transferable nodes
331+
const transferrableNodesLess = await dbQueries.getTransferableNodePairs({
332+
pathsFrom: [pathIds[1]],
333+
pathsTo: [pathIds[2]]
334+
})
335+
expect(transferrableNodesLess.length).toEqual(pathNodes[1].length);
336+
337+
const noDuplicates2 = new Set(transferrableNodes.map(pair => `${pair.from.pathId}-${pair.from.nodeId}-${pair.to.pathId}`));
338+
expect(noDuplicates2.size).toBe(transferrableNodes.length);
339+
});
340+
341+
test('Transferable nodes between multiple paths', async () => {
342+
const transferrableNodes = await dbQueries.getTransferableNodePairs({
343+
pathsFrom: [pathIds[0], pathIds[1]],
344+
pathsTo: [pathIds[2], pathIds[3]]
345+
});
346+
// pathIds[1] and pathIds[3] have no transferable nodes, and some nodes of pathIds[0] don't transfer with some nodes of pathIds[3]
347+
expect(transferrableNodes.length).toEqual(pathNodes[0].length + pathNodes[1].length + pathNodes[3].length);
348+
349+
// Make sure there are no duplicate entries for 2 paths and a node
350+
const noDuplicates1 = new Set(transferrableNodes.map(pair => `${pair.from.pathId}-${pair.from.nodeId}-${pair.to.pathId}`));
351+
expect(noDuplicates1.size).toBe(transferrableNodes.length);
352+
});
353+
354+
test('Invalid path ids', async () => {
355+
await expect(dbQueries.getTransferableNodePairs({
356+
pathsFrom: ['not a uuid'],
357+
pathsTo: ['not a uuid again']
358+
})).rejects.toThrow(expect.anything());
359+
});
360+
361+
test('empty path ids', async () => {
362+
await expect(dbQueries.getTransferableNodePairs({
363+
pathsFrom: [],
364+
pathsTo: []
365+
})).rejects.toThrow(expect.anything());
366+
});
190367
});

packages/transition-backend/src/models/db/transitNodeTransferable.db.queries.ts

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,128 @@ const getToNode = async (nodeId: string): Promise<string[]> => {
144144
}
145145
};
146146

147+
type TransferableNodePair = {
148+
from: { pathId: string; nodeId: string };
149+
to: { pathId: string; nodeId: string };
150+
walkingTravelTimeSeconds: number;
151+
walkingTravelDistanceMeters: number;
152+
};
153+
const getTransferableNodePairs = async ({
154+
pathsFrom,
155+
pathsTo
156+
}: {
157+
pathsFrom: string[];
158+
pathsTo: string[];
159+
}): Promise<TransferableNodePair[]> => {
160+
if (pathsFrom.length === 0 || pathsTo.length === 0) {
161+
throw new TrError(
162+
'Cannot get transferable node pairs, pathsFrom and pathsTo must not be empty',
163+
'DBTNSN0003',
164+
'CannotGetTransferableNodePairsBecauseEmptyPaths'
165+
);
166+
}
167+
if (pathsFrom.some((pathId) => !uuidValidate(pathId)) || pathsTo.some((pathId) => !uuidValidate(pathId))) {
168+
throw new TrError(
169+
'Cannot get transferable node pairs, pathsFrom and pathsTo must contain valid UUIDs',
170+
'DBTNSN0004',
171+
'CannotGetTransferableNodePairsBecauseInvalidPathIds'
172+
);
173+
}
174+
try {
175+
// Query to run:
176+
// WITH path_node AS (
177+
// SELECT id, unnest(nodes) AS nid
178+
// FROM demo_transition.tr_transit_paths
179+
// WHERE id IN ('4d3bdbc3-645f-4dc1-87ff-0e75738327fe', '59abf1c4-29f1-477d-a6bc-f41571d044c6',
180+
// '0f3abf1f-06da-41a9-84e6-8ed9289abb4d', '18bc98f0-f291-40af-b1de-532f2592fd4f')
181+
// ),
182+
// ranked_transfers AS (
183+
// SELECT
184+
// pno.id AS origin_path_id,
185+
// pnd.id AS destination_path_id,
186+
// tn.*,
187+
// ROW_NUMBER() OVER (
188+
// PARTITION BY pno.id, pnd.id, tn.origin_node_id
189+
// ORDER BY tn.walking_travel_time_seconds
190+
// ) AS rn
191+
// FROM demo_transition.tr_transit_node_transferable tn
192+
// INNER JOIN path_node pno ON pno.nid = tn.origin_node_id
193+
// INNER JOIN path_node pnd ON pnd.nid = tn.destination_node_id
194+
// WHERE pno.id IN ('4d3bdbc3-645f-4dc1-87ff-0e75738327fe', '59abf1c4-29f1-477d-a6bc-f41571d044c6')
195+
// AND pnd.id IN ('0f3abf1f-06da-41a9-84e6-8ed9289abb4d', '18bc98f0-f291-40af-b1de-532f2592fd4f')
196+
// )
197+
// SELECT
198+
// origin_path_id,
199+
// destination_path_id,
200+
// origin_node_id,
201+
// destination_node_id,
202+
// walking_travel_time_seconds,
203+
// walking_travel_time_seconds
204+
// FROM ranked_transfers
205+
// WHERE rn = 1;
206+
const withPathName = 'path_node';
207+
const withRankedNodesName = 'ranked_nodes';
208+
const transferableNodePairs = await knex
209+
.with(withPathName, (qb) => {
210+
qb.select('id', knex.raw('unnest(nodes) as nid'))
211+
.from('tr_transit_paths')
212+
.whereIn('id', [...pathsFrom, ...pathsTo]);
213+
})
214+
.with(withRankedNodesName, (qb) => {
215+
qb.select(
216+
'pno.id as origin_path_id',
217+
'pnd.id as destination_path_id',
218+
'tn.*',
219+
knex.raw(
220+
'ROW_NUMBER() OVER (PARTITION BY pno.id, pnd.id, tn.origin_node_id ORDER BY tn.walking_travel_time_seconds) as rn'
221+
)
222+
)
223+
.from(`${tableName} as tn`)
224+
.innerJoin(`${withPathName} as pno`, 'pno.nid', 'tn.origin_node_id')
225+
.innerJoin(`${withPathName} as pnd`, 'pnd.nid', 'tn.destination_node_id')
226+
.whereIn('pno.id', pathsFrom)
227+
.whereIn('pnd.id', pathsTo);
228+
})
229+
.select(
230+
'origin_path_id',
231+
'destination_path_id',
232+
'origin_node_id',
233+
'destination_node_id',
234+
'walking_travel_time_seconds',
235+
'walking_travel_distance_meters'
236+
)
237+
.from(withRankedNodesName)
238+
.where('rn', 1);
239+
return transferableNodePairs.map((pair) => {
240+
const {
241+
origin_path_id,
242+
destination_path_id,
243+
origin_node_id,
244+
destination_node_id,
245+
walking_travel_time_seconds,
246+
walking_travel_distance_meters
247+
} = pair;
248+
return {
249+
from: { pathId: origin_path_id, nodeId: origin_node_id },
250+
to: { pathId: destination_path_id, nodeId: destination_node_id },
251+
walkingTravelTimeSeconds: walking_travel_time_seconds,
252+
walkingTravelDistanceMeters: walking_travel_distance_meters
253+
};
254+
});
255+
} catch (error) {
256+
throw new TrError(
257+
`Cannot get transferable node pairs (knex error: ${error})`,
258+
'DBTNSN0002',
259+
'CannotGetTransferableNodePairsBecauseDatabaseError'
260+
);
261+
}
262+
};
263+
147264
export default {
148265
saveForNode,
149266
getFromNode,
150267
getToNode,
151268
truncate: truncate.bind(null, knex, tableName),
152-
destroy: destroy.bind(null, knex)
269+
destroy: destroy.bind(null, knex),
270+
getTransferableNodePairs
153271
};

0 commit comments

Comments
 (0)