backend/src/services/pis.services.ts
Fred Boniface c147d9c50c TimetableAPI-Upgrade (#64)
Partial implementation of: #47

The 2022.2.1 release currently live is based off of the TimetableAPI-Upgrade branch.

Partial PIS code matching is now implemented in cases where x number of stops need skipping at the start of a route.

Reviewed-on: #64
2024-02-11 15:53:12 +00:00

131 lines
3.9 KiB
TypeScript

// Finds PIS Codes using DB Lookups
const db = require("../services/dbAccess.services");
const clean = require("../utils/sanitizer.utils");
import type { OB_Pis_SimpleObject } from "@owlboard/ts-types";
import { logger } from "../utils/logger.utils";
import { queryAggregate } from "./dbAccess.services";
import {
getPartialEndTiplocMatchPipeline,
getFullTiplocMatchPipeline,
} from "../utils/pis.utils";
import { Document } from "mongodb";
export const supported = ["GW", "UK"];
async function findPisByOrigDest(start: string, end: string) {
logger.debug(
`pisServices.findPisByOrigDest: Searching for Orig: ${start}, Dest: ${end}`
);
const firstCrs = clean.cleanApiEndpointTxt(start.toLowerCase());
const lastCrs = clean.cleanApiEndpointTxt(end.toLowerCase());
const query = {
stops: {
$all: [
{ $elemMatch: { $eq: firstCrs } },
{ $elemMatch: { $eq: lastCrs } },
],
},
$expr: {
$and: [
{ $eq: [{ $arrayElemAt: ["$stops", -1] }, lastCrs] },
{ $eq: [{ $arrayElemAt: ["$stops", 0] }, firstCrs] },
],
},
};
const search = await db.query("pis", query);
// Check for results, if none then try partial match
return search;
}
async function findPisByCode(
code: string
): Promise<OB_Pis_SimpleObject | null> {
logger.debug(`pisServices.findPisByCode: Searching for PIS code: ${code}`);
const cleanCode = clean.removeNonNumeric(code);
const query = {
code: parseInt(cleanCode),
};
const search = db.query("pis", query);
return await search;
}
// Queries the database for PIS codes that match the given TIPLOC array
export async function findByTiplocArray(
tiplocArray: string[]
): Promise<OB_Pis_SimpleObject | null> {
// Firstly fix errant TIPLOCS such as RDNG4AB which will never return a result
// currently only works with RDNG4AB - checks for presence of RDNG4AB first to
// avoid iterating over every array ever searched for.
if (tiplocArray.includes("RDNG4AB")) {
fixErrantTiplocs(tiplocArray);
}
// PERFORMANCE NOTE:
// The majority of queries will return a full match,
// this means that a more performant pipeline is used
// to find a full match and only then are more
// complicated pipelines used to find partial matches
// if the first pipeline returns nothing.
try {
const exactMatch = await findExactMatchByTiploc(tiplocArray);
if (exactMatch) {
return convertDocument(exactMatch, "none");
} else {
const partialEnd = await findPartialEndMatchByTiploc(tiplocArray);
if (partialEnd) {
return convertDocument(partialEnd, "first");
} else {
// Here, I should search for a partialStart match. For now return null.
return null;
}
}
} catch (err) {
logger.error(err, "Error in findByTiplocArray");
return null;
}
}
// Uses a pipeline to find an exact match
async function findExactMatchByTiploc(array: string[]): Promise<Document> {
const pipeline = getFullTiplocMatchPipeline(array);
const res = await queryAggregate("pis", pipeline);
return res[0];
}
// Uses a pipeline to find a partial match - only supporting
// codes ending with the correct stops for now.
async function findPartialEndMatchByTiploc(array: string[]): Promise<Document> {
const pipeline = getPartialEndTiplocMatchPipeline(array);
const res = await queryAggregate("pis", pipeline);
return res[0];
}
function convertDocument(doc: Document, skipType: string): OB_Pis_SimpleObject {
return {
code: doc.code.toString(),
toc: doc.toc,
skipCount: doc.skipStops,
skipType: skipType,
};
}
// Changes any instance of 'RDNG4AB' to 'RDNGSTN'
function fixErrantTiplocs(input: string[]): void {
input.forEach((value, index, array) => {
if (value === "RDNG4AB") {
array[index] = "RDNGSTN";
}
// Additional substitutions can be applied here
});
}
module.exports = {
supported,
findPisByOrigDest,
findPisByCode,
findByTiplocArray,
};