diff --git a/example/css/style.css b/example/css/style.css
new file mode 100644
index 00000000..34916d3c
--- /dev/null
+++ b/example/css/style.css
@@ -0,0 +1,121 @@
+html,
+body {
+ margin: 0;
+ height: 100%;
+ width: 100%;
+}
+
+#mapid {
+ height: 100%;
+}
+
+#results {
+ position: absolute;
+ left: 10px;
+ bottom: 10px;
+ border-radius: 4px;
+ display: flex;
+ flex-direction: column;
+ font-family: sans-serif;
+ max-height: 500px;
+ z-index: 500;
+ overflow-y: scroll;
+ width: 500px;
+}
+
+#results > .path {
+ margin: 10px;
+ background: rgba(255, 255, 255, .8);
+ border: 1px solid #ccc;
+ border-top: none;
+ border-radius: 4px;
+ box-shadow: 0 4px 10px -2px rgba(0, 0, 0, 0.2);
+ flex-wrap: wrap;
+}
+
+#results > .path > .header {
+ padding: 10px;
+ background: rgba(0, 0, 0, .25);
+ color: #fff;
+ text-shadow: 0 0 1px black;
+}
+
+#results > .path > .step {
+ display: flex;
+ flex-direction: row;
+ margin: 10px;
+}
+
+.travelMode {
+ width: 45px;
+ flex-shrink: 0;
+ background-size: 20px 20px;
+ background-repeat: no-repeat;
+}
+
+.details {
+}
+
+.travelMode.walking {
+ background-image: url(data:image/svg+xml;base64,PHN2ZyBhcmlhLWhpZGRlbj0idHJ1ZSIgZGF0YS1wcmVmaXg9ImZhcyIgZGF0YS1pY29uPSJ3YWxraW5nIiBjbGFzcz0ic3ZnLWlubGluZS0tZmEgZmEtd2Fsa2luZyBmYS13LTEwIiByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDMyMCA1MTIiPjxwYXRoIGZpbGw9ImN1cnJlbnRDb2xvciIgZD0iTTIwOCA5NmMyNi41IDAgNDgtMjEuNSA0OC00OFMyMzQuNSAwIDIwOCAwcy00OCAyMS41LTQ4IDQ4IDIxLjUgNDggNDggNDh6bTk0LjUgMTQ5LjFsLTIzLjMtMTEuOC05LjctMjkuNGMtMTQuNy00NC42LTU1LjctNzUuOC0xMDIuMi03NS45LTM2LS4xLTU1LjkgMTAuMS05My4zIDI1LjItMjEuNiA4LjctMzkuMyAyNS4yLTQ5LjcgNDYuMkwxNy42IDIxM2MtNy44IDE1LjgtMS41IDM1IDE0LjIgNDIuOSAxNS42IDcuOSAzNC42IDEuNSA0Mi41LTE0LjNMODEgMjI4YzMuNS03IDkuMy0xMi41IDE2LjUtMTUuNGwyNi44LTEwLjgtMTUuMiA2MC43Yy01LjIgMjAuOC40IDQyLjkgMTQuOSA1OC44bDU5LjkgNjUuNGM3LjIgNy45IDEyLjMgMTcuNCAxNC45IDI3LjdsMTguMyA3My4zYzQuMyAxNy4xIDIxLjcgMjcuNiAzOC44IDIzLjMgMTcuMS00LjMgMjcuNi0yMS43IDIzLjMtMzguOGwtMjIuMi04OWMtMi42LTEwLjMtNy43LTE5LjktMTQuOS0yNy43bC00NS41LTQ5LjcgMTcuMi02OC43IDUuNSAxNi41YzUuMyAxNi4xIDE2LjcgMjkuNCAzMS43IDM3bDIzLjMgMTEuOGMxNS42IDcuOSAzNC42IDEuNSA0Mi41LTE0LjMgNy43LTE1LjcgMS40LTM1LjEtMTQuMy00M3pNNzMuNiAzODUuOGMtMy4yIDguMS04IDE1LjQtMTQuMiAyMS41bC01MCA1MC4xYy0xMi41IDEyLjUtMTIuNSAzMi44IDAgNDUuM3MzMi43IDEyLjUgNDUuMiAwbDU5LjQtNTkuNGM2LjEtNi4xIDEwLjktMTMuNCAxNC4yLTIxLjVsMTMuNS0zMy44Yy01NS4zLTYwLjMtMzguNy00MS44LTQ3LjQtNTMuN2wtMjAuNyA1MS41eiI+PC9wYXRoPjwvc3ZnPg==);
+}
+
+.travelMode.train {
+ background-image: url(data:image/svg+xml;base64,PHN2ZyBhcmlhLWhpZGRlbj0idHJ1ZSIgZGF0YS1wcmVmaXg9ImZhcyIgZGF0YS1pY29uPSJ0cmFpbiIgY2xhc3M9InN2Zy1pbmxpbmUtLWZhIGZhLXRyYWluIGZhLXctMTQiIHJvbGU9ImltZyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDQ4IDUxMiI+PHBhdGggZmlsbD0iY3VycmVudENvbG9yIiBkPSJNNDQ4IDk2djI1NmMwIDUxLjgxNS02MS42MjQgOTYtMTMwLjAyMiA5Nmw2Mi45OCA0OS43MjFDMzg2LjkwNSA1MDIuNDE3IDM4My41NjIgNTEyIDM3NiA1MTJINzJjLTcuNTc4IDAtMTAuODkyLTkuNTk0LTQuOTU3LTE0LjI3OUwxMzAuMDIyIDQ0OEM2MS44MiA0NDggMCA0MDMuOTU0IDAgMzUyVjk2QzAgNDIuOTgxIDY0IDAgMTI4IDBoMTkyYzY1IDAgMTI4IDQyLjk4MSAxMjggOTZ6bS00OCAxMzZWMTIwYzAtMTMuMjU1LTEwLjc0NS0yNC0yNC0yNEg3MmMtMTMuMjU1IDAtMjQgMTAuNzQ1LTI0IDI0djExMmMwIDEzLjI1NSAxMC43NDUgMjQgMjQgMjRoMzA0YzEzLjI1NSAwIDI0LTEwLjc0NSAyNC0yNHptLTE3NiA2NGMtMzAuOTI4IDAtNTYgMjUuMDcyLTU2IDU2czI1LjA3MiA1NiA1NiA1NiA1Ni0yNS4wNzIgNTYtNTYtMjUuMDcyLTU2LTU2LTU2eiI+PC9wYXRoPjwvc3ZnPg==);
+}
+
+.travelMode.bus {
+ background-image: url(data:image/svg+xml;base64,PHN2ZyBhcmlhLWhpZGRlbj0idHJ1ZSIgZm9jdXNhYmxlPSJmYWxzZSIgZGF0YS1wcmVmaXg9ImZhcyIgZGF0YS1pY29uPSJidXMiIGNsYXNzPSJzdmctaW5saW5lLS1mYSBmYS1idXMgZmEtdy0xNiIgcm9sZT0iaW1nIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik00ODggMTI4aC04VjgwYzAtNDQuOC05OS4yLTgwLTIyNC04MFMzMiAzNS4yIDMyIDgwdjQ4aC04Yy0xMy4yNSAwLTI0IDEwLjc0LTI0IDI0djgwYzAgMTMuMjUgMTAuNzUgMjQgMjQgMjRoOHYxNjBjMCAxNy42NyAxNC4zMyAzMiAzMiAzMnYzMmMwIDE3LjY3IDE0LjMzIDMyIDMyIDMyaDMyYzE3LjY3IDAgMzItMTQuMzMgMzItMzJ2LTMyaDE5MnYzMmMwIDE3LjY3IDE0LjMzIDMyIDMyIDMyaDMyYzE3LjY3IDAgMzItMTQuMzMgMzItMzJ2LTMyaDYuNGMxNiAwIDI1LjYtMTIuOCAyNS42LTI1LjZWMjU2aDhjMTMuMjUgMCAyNC0xMC43NSAyNC0yNHYtODBjMC0xMy4yNi0xMC43NS0yNC0yNC0yNHpNMTEyIDQwMGMtMTcuNjcgMC0zMi0xNC4zMy0zMi0zMnMxNC4zMy0zMiAzMi0zMiAzMiAxNC4zMyAzMiAzMi0xNC4zMyAzMi0zMiAzMnptMTYtMTEyYy0xNy42NyAwLTMyLTE0LjMzLTMyLTMyVjEyOGMwLTE3LjY3IDE0LjMzLTMyIDMyLTMyaDI1NmMxNy42NyAwIDMyIDE0LjMzIDMyIDMydjEyOGMwIDE3LjY3LTE0LjMzIDMyLTMyIDMySDEyOHptMjcyIDExMmMtMTcuNjcgMC0zMi0xNC4zMy0zMi0zMnMxNC4zMy0zMiAzMi0zMiAzMiAxNC4zMyAzMiAzMi0xNC4zMyAzMi0zMiAzMnoiPjwvcGF0aD48L3N2Zz4=);
+}
+
+.duration {
+ margin-bottom: 5px;
+ margin-top: 5px;
+}
+
+.enterConnectionId,
+.exitConnectionId {
+ opacity: 0.5;
+}
+
+#actions {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ z-index: 500;
+}
+
+#actions > button {
+ border-radius: 100px;
+ background: rgba(255, 255, 255, .8);
+ font-size: 2em;
+ padding: .25em 1em;
+}
+
+#prefetch {
+ position: absolute;
+ top: 10px;
+ left: 54px;
+ z-index: 500;
+}
+
+.prefetch-view {
+ height: 6px;
+ margin-bottom: 3px;
+ border-radius: 30px;
+ background: #3556a9;
+ color: #fff;
+ font-family: sans-serif;
+ padding: 3px 5px;
+ transition: 50ms width ease-in-out;
+}
+
+.prefetch-view::after {
+ content: attr(data-last);
+ color: #fff;
+ float: right;
+}
+
+#prefetch-bar {
+ height: 18px;
+}
diff --git a/example/index.html b/example/index.html
new file mode 100644
index 00000000..12ee0426
--- /dev/null
+++ b/example/index.html
@@ -0,0 +1,26 @@
+
+
+
+
+ Map
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/js/index.js b/example/js/index.js
new file mode 100644
index 00000000..a0343ce5
--- /dev/null
+++ b/example/js/index.js
@@ -0,0 +1,475 @@
+const map = L.map("mapid").setView([51.050043, 3.719926], 10);
+
+L.tileLayer(
+ "https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}",
+ {
+ attribution:
+ "Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © Mapbox",
+ maxZoom: 18,
+ id: "mapbox.streets",
+ accessToken:
+ "pk.eyJ1IjoibWF4aW10bWFydGluIiwiYSI6ImNqcHdqbjdhaDAzYzc0Mm04eDFhamkzenMifQ.0uNbKJ2WHATkKBBSADuhyQ"
+ }
+).addTo(map);
+
+const planner = new Planner();
+
+planner.prefetchStops();
+planner.prefetchConnections();
+
+let plannerResult;
+const resetButton = document.querySelector("#reset");
+const results = document.querySelector("#results");
+const prefetchWrapper = document.querySelector("#prefetch");
+const prefetchBar = document.querySelector("#prefetch-bar");
+
+let lines = [];
+let polyLines = [];
+let resultObjects = [];
+let query = [];
+let allStops = [];
+let prefetchViews = [];
+
+let firstPrefetch;
+
+const removeLines = () => {
+ for (const line of polyLines) {
+ line.remove();
+ }
+
+ lines = [];
+ polyLines = [];
+ lines = [];
+};
+
+const removeResultObjects = () => {
+ for (const obj of resultObjects) {
+ obj.remove();
+ }
+
+ resultObjects = [];
+};
+
+const removePrefetchView = () => {
+ const view = document.getElementById("prefetch");
+
+ if (!view.hasChildNodes()) {
+ return;
+ }
+
+ for (const child of [...view.childNodes]) {
+ if (!child.id) {
+ child.parentNode.removeChild(child);
+ }
+ }
+};
+
+resetButton.onclick = e => {
+ removeLines();
+ removeResultObjects();
+ query = [];
+ results.innerHTML = "";
+
+ for (const stop of allStops) {
+ stop.addTo(map);
+ }
+
+ if (plannerResult) {
+ plannerResult.close();
+ }
+
+ removePrefetchView();
+};
+
+const pxPerMs = .00005;
+const getPrefetchViewWidth = (start, stop) => {
+ if (!start || !stop) {
+ return 0;
+ }
+
+ return (stop.valueOf() - start.valueOf()) * pxPerMs;
+};
+
+planner.getAllStops().then(stops => {
+ for (const stop of stops) {
+ if (stop["http://semweb.mmlab.be/ns/stoptimes#avgStopTimes"] > 100) {
+ const marker = L.marker([stop.latitude, stop.longitude]).addTo(map);
+
+ marker.bindPopup(stop.name);
+
+ allStops.push(marker);
+
+ marker.on("click", e => {
+ selectRoute(e, stop.id);
+ });
+ }
+ }
+});
+
+planner
+ .on("query", query => {
+ console.log("Query", query);
+ })
+ .on("sub-query", query => {
+ const { minimumDepartureTime, maximumArrivalTime, maximumTravelDuration } = query;
+
+ console.log(
+ "[Subquery]",
+ minimumDepartureTime,
+ maximumArrivalTime,
+ maximumArrivalTime - minimumDepartureTime,
+ maximumTravelDuration,
+ );
+
+ removeLines();
+ })
+ .on("initial-reachable-stops", reachableStops => {
+ console.log("initial", reachableStops);
+ reachableStops.map(({ stop }) => {
+ const startMarker = L.marker([stop.latitude, stop.longitude]).addTo(map);
+
+ startMarker.bindPopup("initialreachable: " + stop.name);
+
+ resultObjects.push(startMarker);
+ });
+ })
+ .on("final-reachable-stops", reachableStops => {
+ console.log("final", reachableStops);
+
+ reachableStops.map(({ stop }) => {
+ const startMarker = L.marker([stop.latitude, stop.longitude]).addTo(map);
+
+ startMarker.bindPopup("finalreachable: " + stop.name);
+
+ resultObjects.push(startMarker);
+ });
+ })
+ .on("added-new-transfer-profile", ({ departureStop, arrivalStop, amountOfTransfers }) => {
+
+ const newLine = [
+ [departureStop.latitude, departureStop.longitude],
+ [arrivalStop.latitude, arrivalStop.longitude]
+ ];
+
+ let lineExists = lines.length > 0 && lines
+ .some((line) =>
+ line[0][0] === newLine[0][0]
+ && line[0][1] === newLine[0][1]
+ && line[1][0] === newLine[1][0]
+ && line[1][1] === newLine[1][1]
+ );
+
+ if (!lineExists) {
+ const polyline = new L.Polyline(newLine, {
+ color: "#000",
+ weight: 1,
+ smoothFactor: 1,
+ opacity: 0.5,
+ dashArray: "10 10"
+ }).addTo(map);
+
+ lines.push(newLine);
+ polyLines.push(polyline);
+ }
+ })
+ .on("connection-prefetch", (departureTime) => {
+ if (!firstPrefetch) {
+ firstPrefetch = departureTime;
+
+ prefetchBar.innerHTML = departureTime.toLocaleTimeString();
+
+ } else {
+ const width = getPrefetchViewWidth(firstPrefetch, departureTime);
+
+ prefetchBar.style.width = `${width}px`;
+ prefetchBar.setAttribute("data-last", departureTime.toLocaleTimeString());
+ }
+ })
+ .on("connection-iterator-view", (lowerBound, upperBound, completed) => {
+ if (!lowerBound || !upperBound) {
+ return;
+ }
+
+ if (!completed) {
+ const width = getPrefetchViewWidth(lowerBound, upperBound);
+ const offset = getPrefetchViewWidth(firstPrefetch, lowerBound);
+
+ const prefetchView = document.createElement("div");
+ prefetchView.className = "prefetch-view";
+ prefetchView.style.marginLeft = `${offset}px`;
+ prefetchView.style.width = `${width}px`;
+ prefetchView.style.backgroundColor = "red";
+
+ prefetchWrapper.appendChild(prefetchView);
+ prefetchViews.push({ lowerBound, upperBound, elem: prefetchView });
+
+ } else {
+ const { elem } = prefetchViews
+ .find((view) => view.lowerBound === lowerBound && view.upperBound === upperBound);
+
+ if (!elem) {
+ console.warn("Wut");
+ return;
+ }
+
+ elem.style.backgroundColor = "limegreen";
+ }
+ });
+
+function onMapClick(e) {
+ selectRoute(e);
+}
+
+function selectRoute(e, id) {
+ if (query.length === 2) {
+ return;
+ }
+
+ let marker = L.marker(e.latlng).addTo(map);
+
+ resultObjects.push(marker);
+
+ if (query.length < 2) {
+ const { lat, lng } = e.latlng;
+
+ let item = {
+ latitude: lat,
+ longitude: lng
+ };
+
+ if (id) {
+ item.id = id;
+ }
+
+ query.push(item);
+ }
+
+ if (query.length === 2) {
+ runQuery(query);
+
+ for (const marker of allStops) {
+ marker.remove();
+ }
+ }
+}
+
+function dateToTimeString(date) {
+ const hours = date.getHours();
+ const minutes = date.getMinutes();
+ return `${hours < 10 ? "0" + hours : hours}:${minutes < 10 ? "0" + minutes : minutes}`;
+}
+
+map.on("click", onMapClick);
+
+function getRandomColor() {
+ const letters = "0123456789ABCDEF";
+ let color = "#";
+ for (let i = 0; i < 6; i++) {
+ color += letters[Math.floor(Math.random() * 16)];
+ }
+ return color;
+}
+
+function getTravelTime(path) {
+ return path.steps.reduce((time, step) => time + step.duration.minimum, 0) / 60000;
+}
+
+function getTransferTime(path) {
+ let time = 0;
+
+ if (path.steps.length < 2) {
+ return time;
+ }
+
+ for (let i = 0; i < path.steps.length - 1; i++) {
+ let stepX = path.steps[i];
+ let stepY = path.steps[i + 1];
+
+ time += stepY.startTime - stepX.stopTime;
+ }
+
+ return time / 60000;
+}
+
+function addResultPanel(path, color) {
+ const pathElement = document.createElement("div");
+ pathElement.className = "path";
+
+ const firstStep = path.steps[0];
+ const lastStep = path.steps[path.steps.length - 1];
+
+ const headerElement = document.createElement("div");
+ headerElement.className = "header";
+
+ headerElement.innerHTML = `
+ Departure: ${dateToTimeString(firstStep.startTime)}
+ Arrival: ${dateToTimeString(lastStep.stopTime)}
+ Travel time: ${getTravelTime(path)} min
+ Transfer time: ${getTransferTime(path)} min
+ `;
+
+ pathElement.appendChild(headerElement);
+
+ path.steps.forEach(step => {
+ const stepElement = document.createElement("div");
+ stepElement.className = "step";
+
+ const travelMode = document.createElement("div");
+ travelMode.className = "travelMode " + step.travelMode;
+ stepElement.appendChild(travelMode);
+
+ const details = document.createElement("div");
+ details.className = "details";
+ stepElement.appendChild(details);
+
+ const startLocation = document.createElement("div");
+ startLocation.className = "startLocation";
+ startLocation.innerHTML =
+ "Start location: " + step.startLocation.name;
+ details.appendChild(startLocation);
+
+ if (step.startTime) {
+ const startTime = document.createElement("div");
+ startTime.className = "startTime";
+ startTime.innerHTML = step.startTime;
+ details.appendChild(startTime);
+ }
+
+ if (step.enterConnectionId) {
+ const enterConnectionId = document.createElement("div");
+ enterConnectionId.className = "enterConnectionId";
+ enterConnectionId.innerHTML =
+ "Enter connection: " + step.enterConnectionId;
+ details.appendChild(enterConnectionId);
+ }
+
+ if (step.duration) {
+ const duration = document.createElement("div");
+ duration.className = "duration";
+ duration.innerHTML =
+ "Duration: minimum " +
+ step.duration.minimum / (60 * 1000) +
+ "min";
+ details.appendChild(duration);
+ }
+
+ const stopLocation = document.createElement("div");
+ stopLocation.className = "stopLocation";
+ stopLocation.innerHTML = "Stop location: " + step.stopLocation.name;
+ details.appendChild(stopLocation);
+
+ if (step.stopTime) {
+ const stopTime = document.createElement("div");
+ stopTime.className = "stopTime";
+ stopTime.innerHTML = step.stopTime;
+ details.appendChild(stopTime);
+ }
+
+ if (step.exitConnectionId) {
+ const exitConnectionId = document.createElement("div");
+ exitConnectionId.className = "exitConnectionId";
+ exitConnectionId.innerHTML =
+ "Exit connection: " + step.exitConnectionId;
+ details.appendChild(exitConnectionId);
+ }
+
+ pathElement.style.borderLeft = "5px solid " + color;
+
+ pathElement.appendChild(stepElement);
+ });
+
+ results.appendChild(pathElement);
+}
+
+function addResultToMap(path, color) {
+ path.steps.forEach(step => {
+ const { startLocation, stopLocation, travelMode } = step;
+
+ const startMarker = L.marker([
+ startLocation.latitude,
+ startLocation.longitude
+ ]).addTo(map);
+
+ startMarker.bindPopup(startLocation.name);
+
+ const stopMarker = L.marker([
+ stopLocation.latitude,
+ stopLocation.longitude
+ ]).addTo(map);
+
+ stopMarker.bindPopup(stopLocation.name);
+ const line = [
+ [startLocation.latitude, startLocation.longitude],
+ [stopLocation.latitude, stopLocation.longitude]
+ ];
+
+ const polyline = new L.Polyline(line, {
+ color,
+ weight: 5,
+ smoothFactor: 1,
+ opacity: 0.7,
+ dashArray: travelMode === "walking" ? "8 8" : null
+ }).addTo(map);
+
+ resultObjects.push(startMarker, stopMarker, polyline);
+ });
+}
+
+function runQuery(query) {
+ console.log(query);
+
+ const maximumWalkingDistance = 200;
+
+ const departureCircle = L.circle([query[0].latitude, query[0].longitude], {
+ color: "limegreen",
+ fillColor: "limegreen",
+ fillOpacity: 0.5,
+ radius: maximumWalkingDistance
+ }).addTo(map);
+
+ const arrivalCircle = L.circle([query[1].latitude, query[1].longitude], {
+ color: "red",
+ fillColor: "red",
+ fillOpacity: 0.5,
+ radius: maximumWalkingDistance
+ }).addTo(map);
+
+ resultObjects.push(departureCircle, arrivalCircle);
+
+ let i = 0;
+ let amount = 4;
+
+ planner
+ .query({
+ publicTransportOnly: true,
+ from: query[0], // Brussels North
+ to: query[1], // Ghent-Sint-Pieters
+ minimumDepartureTime: new Date(),
+ maximumWalkingDistance,
+ maximumTransferDuration: 30 * 60 * 1000, // 30 minutes
+ minimumWalkingSpeed: 3
+ })
+ .take(amount)
+ .on("error", (error) => {
+ console.error(error);
+ })
+ .on("data", path => {
+ i++;
+
+ const color = getRandomColor();
+
+ addResultPanel(path, color);
+ addResultToMap(path, color);
+
+ })
+ .on("end", () => {
+ if (i < amount) {
+ const noMore = document.createElement("div");
+ noMore.className = "path";
+ noMore.style.padding = "10px";
+ noMore.innerHTML = "No more results";
+
+ results.appendChild(noMore);
+ }
+ });
+}
diff --git a/package-lock.json b/package-lock.json
index 4164c09c..86eb6fa8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "plannerjs",
- "version": "0.0.1-alpha",
+ "version": "0.0.2-alpha",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -33,18 +33,21 @@
}
},
"@rdfjs/data-model": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@rdfjs/data-model/-/data-model-1.1.0.tgz",
- "integrity": "sha512-vK7TlSFBJQihqMoMgXK/VtcRV+rCQSOLB9VGAfv4Pr6BzUbBcR0CeM/RpkD4bHGX3816eaaURH1CNgN7togWdw=="
- },
- "@types/asynciterator": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@types/asynciterator/-/asynciterator-1.1.1.tgz",
- "integrity": "sha512-KgjXxTtWbMW7UA4oZauIfg2rCl5+5LbsoVF5DwwpXVQxzSbez2PQ9NAlbJlBjIHqKLOpWcdjqL+wyrNepvTZOg==",
- "dev": true,
+ "resolved": "https://registry.npmjs.org/@rdfjs/data-model/-/data-model-1.1.1.tgz",
+ "integrity": "sha512-4jb1zc77f27u/MLVhpE/zHR1uvdH4XElXG63rJP/kVnvKoHtVfyJSEqN9oRLANgqHJ9SNwKj9FXeNFZ4+GGfiw==",
"requires": {
- "@types/events": "*",
- "@types/node": "*"
+ "@types/rdf-js": "^2.0.1"
+ },
+ "dependencies": {
+ "@types/rdf-js": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@types/rdf-js/-/rdf-js-2.0.1.tgz",
+ "integrity": "sha512-x3Qct8TPilUos4znM1gANmtTvjOFdDRItmpEM2Nu9QgAx258FN9k22OvOu2TmPzOlx8a1FLdEW3o33UXHQt5ow==",
+ "requires": {
+ "@types/node": "*"
+ }
+ }
}
},
"@types/events": {
@@ -74,9 +77,9 @@
}
},
"@types/handlebars": {
- "version": "4.0.39",
- "resolved": "https://registry.npmjs.org/@types/handlebars/-/handlebars-4.0.39.tgz",
- "integrity": "sha512-vjaS7Q0dVqFp85QhyPSZqDKnTTCemcSHNHFvDdalO1s0Ifz5KuE64jQD5xoUkfdWwF4WpqdJEl7LsWH8rzhKJA==",
+ "version": "4.0.40",
+ "resolved": "https://registry.npmjs.org/@types/handlebars/-/handlebars-4.0.40.tgz",
+ "integrity": "sha512-sGWNtsjNrLOdKha2RV1UeF8+UbQnPSG7qbe5wwbni0mw4h2gHXyPFUMOC+xwGirIiiydM/HSqjDO4rk6NFB18w==",
"dev": true
},
"@types/haversine": {
@@ -98,9 +101,9 @@
"dev": true
},
"@types/lodash": {
- "version": "4.14.118",
- "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.118.tgz",
- "integrity": "sha512-iiJbKLZbhSa6FYRip/9ZDX6HXhayXLDGY2Fqws9cOkEQ6XeKfaxB0sC541mowZJueYyMnVUmmG+al5/4fCDrgw==",
+ "version": "4.14.120",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.120.tgz",
+ "integrity": "sha512-jQ21kQ120mo+IrDs1nFNVm/AsdFxIx2+vZ347DbogHJPd/JzKNMOqU6HCYin1W6v8l5R9XSO2/e9cxmn7HAnVw==",
"dev": true
},
"@types/marked": {
@@ -118,8 +121,7 @@
"@types/node": {
"version": "10.12.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.0.tgz",
- "integrity": "sha512-3TUHC3jsBAB7qVRGxT6lWyYo2v96BMmD2PTcl47H25Lu7UXtFH/2qqmKiVrnel6Ne//0TFYf6uvNX+HW2FRkLQ==",
- "dev": true
+ "integrity": "sha512-3TUHC3jsBAB7qVRGxT6lWyYo2v96BMmD2PTcl47H25Lu7UXtFH/2qqmKiVrnel6Ne//0TFYf6uvNX+HW2FRkLQ=="
},
"@types/rdf-js": {
"version": "1.0.1",
@@ -131,9 +133,9 @@
}
},
"@types/shelljs": {
- "version": "0.8.0",
- "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.8.0.tgz",
- "integrity": "sha512-vs1hCC8RxLHRu2bwumNyYRNrU3o8BtZhLysH5A4I98iYmA2APl6R3uNQb5ihl+WiwH0xdC9LLO+vRrXLs/Kyxg==",
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.8.1.tgz",
+ "integrity": "sha512-1lQw+48BuVgp6c1+z8EMipp18IdnV2dLh6KQGwOm+kJy9nPjEkaqRKmwbDNEYf//EKBvKcwOC6V2cDrNxVoQeQ==",
"dev": true,
"requires": {
"@types/glob": "*",
@@ -843,6 +845,14 @@
"resolved": "https://registry.npmjs.org/asynciterator/-/asynciterator-2.0.1.tgz",
"integrity": "sha512-aVLheZsDNU5qpOv6jZEHnFv79GfEi+N0w/OLmMmXZfGD8XFFmPsRhkSqleNl9jS6mqy/DNoV7tXGcI0S3cUvHQ=="
},
+ "asynciterator-promiseproxy": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/asynciterator-promiseproxy/-/asynciterator-promiseproxy-2.0.0.tgz",
+ "integrity": "sha512-C0ub2jkCId4a66R9OuPa3Cvu+PF73yXEBErZc3NUlfLVXbYMmPF9vBUPzmCo3UuMdFmJLcfOjGxJpQ5a7z/G9A==",
+ "requires": {
+ "asynciterator": "^2.0.0"
+ }
+ },
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1204,11 +1214,6 @@
"integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==",
"dev": true
},
- "bindings": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.0.tgz",
- "integrity": "sha512-DpLh5EzMR2kzvX1KIlVC0VkC3iZtHKTgdtZ0a3pglBZdaQFjt5S9g9xd1lE+YvXyfd6mtCeRnrUfOLYiTMlNSw=="
- },
"bluebird": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz",
@@ -2523,9 +2528,9 @@
}
},
"follow-redirects": {
- "version": "1.5.9",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.9.tgz",
- "integrity": "sha512-Bh65EZI/RU8nx0wbYF9shkFZlqLP+6WT/5FnA3cE/djNSuKNHJEinGGZgu/cQEkeeb2GdFOgenAmn8qaqYke2w==",
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.6.1.tgz",
+ "integrity": "sha512-t2JCjbzxQpWvbhts3l6SH1DKzSrx8a+SsaVf4h6bG4kOXUuPYS/kg2Lr4gQSb7eemaHqJkOThF1BGyjlUkO1GQ==",
"requires": {
"debug": "=3.1.0"
}
@@ -4396,13 +4401,13 @@
}
},
"jsonld": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-1.1.0.tgz",
- "integrity": "sha512-tx87xNtu2hGabr7mhSyXTI8q+Fz3pl+50B/JislFPwAz8ud0KTTQpNjU74tJIegFAHve9UFYzzj4YVTIrac0Hw==",
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-1.5.0.tgz",
+ "integrity": "sha512-7jF9WXK4nuHvhz/qT6A4DEZ58tUYgrV98xBJEgHFhQ6GQaNT+oU1zqkFXKtDZsKsiEs/1K/VShNnat6SISb3jg==",
"requires": {
- "rdf-canonize": "^0.2.1",
- "request": "^2.83.0",
- "semver": "^5.5.0",
+ "rdf-canonize": "^1.0.1",
+ "request": "^2.88.0",
+ "semver": "^5.6.0",
"xmldom": "0.1.19"
},
"dependencies": {
@@ -4827,15 +4832,12 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
- "n3": {
- "version": "0.11.2",
- "resolved": "https://registry.npmjs.org/n3/-/n3-0.11.2.tgz",
- "integrity": "sha512-ICSiOmFLbZ4gI35+4H3e2vYGHDC944WZkCa1iVNRAx/mRZESEevQNFhfHaui/lhqynoZYvBVDNjM/2Tfd3TICQ=="
- },
"nan": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz",
- "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA=="
+ "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==",
+ "dev": true,
+ "optional": true
},
"nanomatch": {
"version": "1.2.13",
@@ -5469,9 +5471,9 @@
"dev": true
},
"progress": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.1.tgz",
- "integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true
},
"promise-inflight": {
@@ -5614,14 +5616,12 @@
}
},
"rdf-canonize": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-0.2.4.tgz",
- "integrity": "sha512-xwAEHJt8FTe4hP9CjFgwQPFdszu4iwEintk31+9eh0rljC28vm9EhoaIlC1rQx5LaCB5oHom4+yoei4+DTdRjQ==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-1.0.1.tgz",
+ "integrity": "sha512-vQq6q7BIUwrVQijKRYdunxlodkn0Btjv2MnJ4S3rOUELsghq7fGuDaWuqBNbXca3KRbcRS6HwTIT2hJbJej2UA==",
"requires": {
- "bindings": "^1.3.0",
- "nan": "^2.10.0",
- "node-forge": "^0.7.1",
- "semver": "^5.4.1"
+ "node-forge": "^0.7.6",
+ "semver": "^5.6.0"
}
},
"rdfa-processor": {
@@ -5633,9 +5633,9 @@
},
"dependencies": {
"n3": {
- "version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/n3/-/n3-1.0.0-beta.2.tgz",
- "integrity": "sha512-dY0Q21JhNJozPMSijKI9phfgocXb7hF9F8zOPnsRu0Dy61eQwQVKcFEw59V0sn2F/neJjNg7WHmNtGLMkPpKPA=="
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/n3/-/n3-1.0.3.tgz",
+ "integrity": "sha512-IdcCNqb/1gj9fX63hoVEn3/Z1u9iLJiYLSpesmqT3+5UrxcYG5YldUP6T2okNRZKzyVdqz+I0PExGkWkz9gcZw=="
}
}
},
@@ -5648,9 +5648,9 @@
},
"dependencies": {
"n3": {
- "version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/n3/-/n3-1.0.0-beta.2.tgz",
- "integrity": "sha512-dY0Q21JhNJozPMSijKI9phfgocXb7hF9F8zOPnsRu0Dy61eQwQVKcFEw59V0sn2F/neJjNg7WHmNtGLMkPpKPA=="
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/n3/-/n3-1.0.3.tgz",
+ "integrity": "sha512-IdcCNqb/1gj9fX63hoVEn3/Z1u9iLJiYLSpesmqT3+5UrxcYG5YldUP6T2okNRZKzyVdqz+I0PExGkWkz9gcZw=="
}
}
},
@@ -7647,9 +7647,9 @@
"dev": true
},
"typedoc": {
- "version": "0.13.0",
- "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.13.0.tgz",
- "integrity": "sha512-jQWtvPcV+0fiLZAXFEe70v5gqjDO6pJYJz4mlTtmGJeW2KRoIU/BEfktma6Uj8Xii7UakuZjbxFewl3UYOkU/w==",
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.14.2.tgz",
+ "integrity": "sha512-aEbgJXV8/KqaVhcedT7xG6d2r+mOvB5ep3eIz1KuB5sc4fDYXcepEEMdU7XSqLFO5hVPu0nllHi1QxX2h/QlpQ==",
"dev": true,
"requires": {
"@types/fs-extra": "^5.0.3",
@@ -7661,14 +7661,14 @@
"@types/shelljs": "^0.8.0",
"fs-extra": "^7.0.0",
"handlebars": "^4.0.6",
- "highlight.js": "^9.0.0",
+ "highlight.js": "^9.13.1",
"lodash": "^4.17.10",
"marked": "^0.4.0",
"minimatch": "^3.0.0",
"progress": "^2.0.0",
"shelljs": "^0.8.2",
"typedoc-default-themes": "^0.5.0",
- "typescript": "3.1.x"
+ "typescript": "3.2.x"
}
},
"typedoc-default-themes": {
@@ -7678,9 +7678,9 @@
"dev": true
},
"typescript": {
- "version": "3.1.6",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.1.6.tgz",
- "integrity": "sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA==",
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.4.tgz",
+ "integrity": "sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg==",
"dev": true
},
"uglify-js": {
diff --git a/package.json b/package.json
index d1f8ba83..a7a71109 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "plannerjs",
- "version": "0.0.2-alpha",
+ "version": "0.0.3-alpha",
"description": "The JavaScript framework for journey planning.",
"main": "lib/index.js",
"license": "MIT",
@@ -21,16 +21,17 @@
"lint": "./node_modules/tslint/bin/tslint --project .",
"webpack": "webpack --config webpack.config.js --mode=production",
"webpack-stats": "npm run webpack -- --display-modules --json > stats.json",
- "typedoc": "typedoc --options typedoc.js",
- "doc": "npm run typedoc & npm run browser && cp dist/bundle.js docs/js/planner-latest.js && cp dist/bundle.js.map docs/js/planner-latest.js.map"
+ "typedoc": "typedoc --options typedoc.config.js",
+ "doc": "npm run typedoc && npm run browser && cp dist/bundle.js docs/js/planner-latest.js && cp dist/bundle.js.map docs/js/planner-latest.js.map",
+ "example": "npm run browser && cp dist/bundle.js example/js/bundle.js && cp dist/bundle.js.map example/js/bundle.js.map"
},
"dependencies": {
"asynciterator": "^2.0.1",
+ "asynciterator-promiseproxy": "^2.0.0",
"haversine": "^1.1.0",
"inversify": "^5.0.1",
"isomorphic-fetch": "^2.2.1",
"ldfetch": "^1.1.1-alpha",
- "n3": "0.11.2",
"reflect-metadata": "^0.1.12",
"uritemplate": "^0.3.4"
},
@@ -38,7 +39,6 @@
"lint"
],
"devDependencies": {
- "@types/asynciterator": "^1.1.1",
"@types/haversine": "^1.1.0",
"@types/jest": "^23.3.7",
"@types/rdf-js": "^1.0.1",
@@ -48,8 +48,8 @@
"ts-jest": "^23.10.4",
"ts-loader": "^5.3.0",
"tslint": "^5.11.0",
- "typedoc": "^0.13.0",
- "typescript": "^3.1.6",
+ "typedoc": "^0.14.2",
+ "typescript": "^3.2.4",
"webpack": "^4.25.1",
"webpack-cli": "^3.1.2"
}
diff --git a/src/Catalog.ts b/src/Catalog.ts
index c02fdd6b..0d2aeb2c 100644
--- a/src/Catalog.ts
+++ b/src/Catalog.ts
@@ -1,26 +1,40 @@
-import TravelMode from "./TravelMode";
+import TravelMode from "./enums/TravelMode";
+/**
+ * A Catalog instance holds the stops source and connections source configs.
+ * These configs get passed to the [[StopsFetcherFactory]] and [[ConnectionsFetcherFactory]] to construct
+ * respectively [[IStopsFetcher]] and [[IConnectionsFetcher]] instances
+ */
export default class Catalog {
public static combine(...catalogs: Catalog[]): Catalog {
const combinedCatalog = new Catalog();
for (const sourceCatalog of catalogs) {
- combinedCatalog.stopsFetcherConfigs.push(...sourceCatalog.stopsFetcherConfigs);
- combinedCatalog.connectionsFetcherConfigs.push(...sourceCatalog.connectionsFetcherConfigs);
+ combinedCatalog.stopsSourceConfigs.push(...sourceCatalog.stopsSourceConfigs);
+ combinedCatalog.connectionsSourceConfigs.push(...sourceCatalog.connectionsSourceConfigs);
}
return combinedCatalog;
}
- public stopsFetcherConfigs = [];
- public connectionsFetcherConfigs = [];
+ public stopsSourceConfigs: IStopsSourceConfig[] = [];
+ public connectionsSourceConfigs: IConnectionsSourceConfig[] = [];
- public addStopsFetcher(accessUrl: string) {
- this.stopsFetcherConfigs.push({accessUrl});
+ public addStopsSource(accessUrl: string) {
+ this.stopsSourceConfigs.push({accessUrl});
}
- public addConnectionsFetcher(accessUrl: string, travelMode: TravelMode) {
- this.connectionsFetcherConfigs.push({accessUrl, travelMode});
+ public addConnectionsSource(accessUrl: string, travelMode: TravelMode) {
+ this.connectionsSourceConfigs.push({accessUrl, travelMode});
}
}
+
+export interface IStopsSourceConfig {
+ accessUrl: string;
+}
+
+export interface IConnectionsSourceConfig {
+ accessUrl: string;
+ travelMode: TravelMode;
+}
diff --git a/src/Context.ts b/src/Context.ts
index 00fb1d6e..38a6a079 100644
--- a/src/Context.ts
+++ b/src/Context.ts
@@ -1,11 +1,13 @@
// @ts-ignore
import { EventEmitter, Listener } from "events";
import { Container, injectable } from "inversify";
+import EventType from "./enums/EventType";
/**
* The Context serves as event pass through and holder of the inversify container object.
- * Proxies an internal EventEmitter because ´decorate(injectable(), EventEmitter)´ causes
- * errors when running tests in Jest
+ *
+ * It proxies an internal EventEmitter (instead of extending EventEmitter) because
+ * ´decorate(injectable(), EventEmitter)´ causes errors when running tests in Jest
*/
@injectable()
// @ts-ignore
@@ -35,6 +37,10 @@ export default class Context implements EventEmitter {
return this.emitter.emit(type, ...args);
}
+ public emitWarning(...args: any[]): boolean {
+ return this.emit(EventType.Warning, ...args);
+ }
+
public listenerCount(type: string | symbol): number {
return this.emitter.listenerCount(type);
}
diff --git a/src/Defaults.ts b/src/Defaults.ts
index 2be8ecc1..3b2d8709 100644
--- a/src/Defaults.ts
+++ b/src/Defaults.ts
@@ -1,5 +1,8 @@
import Units from "./util/Units";
+/**
+ * This class holds the default [[IQuery]]/[[IResolvedQuery]] parameters
+ */
export default class Defaults {
public static readonly defaultMinimumWalkingSpeed = 3;
public static readonly defaultMaximumWalkingSpeed = 6;
diff --git a/src/Planner.ts b/src/Planner.ts
index 441fede4..ef9108e5 100644
--- a/src/Planner.ts
+++ b/src/Planner.ts
@@ -1,8 +1,10 @@
import { AsyncIterator } from "asynciterator";
+import { PromiseProxyIterator } from "asynciterator-promiseproxy";
// @ts-ignore
import { EventEmitter, Listener } from "events";
import Context from "./Context";
-import EventType from "./EventType";
+import EventType from "./enums/EventType";
+import IConnectionsProvider from "./fetcher/connections/IConnectionsProvider";
import IStop from "./fetcher/stops/IStop";
import IStopsProvider from "./fetcher/stops/IStopsProvider";
import IPath from "./interfaces/IPath";
@@ -11,12 +13,8 @@ import defaultContainer from "./inversify.config";
import IQueryRunner from "./query-runner/IQueryRunner";
import TYPES from "./types";
-if (!Symbol.asyncIterator) {
- (Symbol as any).asyncIterator = Symbol.for("Symbol.asyncIterator");
-}
-
/**
- * Allows to ask route planning queries over our knowledge graphs
+ * Allows to ask route planning queries. Emits events defined in [[EventType]]
*/
// @ts-ignore
export default class Planner implements EventEmitter {
@@ -36,19 +34,25 @@ export default class Planner implements EventEmitter {
}
/**
- * Given an [[IQuery]], it will evaluate the query and eventually produce an [[IQueryResult]]
+ * Given an [[IQuery]], it will evaluate the query and return a promise for an AsyncIterator of [[IPath]] instances
* @param query An [[IQuery]] specifying a route planning query
- * @returns An AsyncIterator of [[IPath]]s
+ * @returns An AsyncIterator of [[IPath]] instances
*/
- public async query(query: IQuery): Promise> {
+ public query(query: IQuery): AsyncIterator {
this.emit(EventType.Query, query);
- const iterator = await this.queryRunner.run(query);
+ const iterator = new PromiseProxyIterator(() => this.queryRunner.run(query));
- this.once(EventType.QueryAbort, () => {
+ this.once(EventType.AbortQuery, () => {
iterator.close();
});
+ iterator.on("error", (e) => {
+ if (e && e.eventType) {
+ this.emit(e.eventType, e.message);
+ }
+ });
+
return iterator;
}
@@ -100,13 +104,33 @@ export default class Planner implements EventEmitter {
return this;
}
+ public prefetchStops(): void {
+ const container = this.context.getContainer();
+ const stopsProvider = container.get(TYPES.StopsProvider);
+
+ if (stopsProvider) {
+ stopsProvider.prefetchStops();
+ }
+ }
+
+ public prefetchConnections(): void {
+ const container = this.context.getContainer();
+ const connectionsProvider = container.get(TYPES.ConnectionsProvider);
+
+ if (connectionsProvider) {
+ connectionsProvider.prefetchConnections();
+ }
+ }
+
public getAllStops(): Promise {
- const provider = this.context.getContainer().get(TYPES.StopsProvider);
+ const container = this.context.getContainer();
+ const stopsProvider = container.get(TYPES.StopsProvider);
- if (provider) {
- return provider.getAllStops();
+ if (stopsProvider) {
+ return stopsProvider.getAllStops();
}
return Promise.reject();
}
+
}
diff --git a/src/asynciterator.d.ts b/src/asynciterator.d.ts
new file mode 100755
index 00000000..bcbc70b7
--- /dev/null
+++ b/src/asynciterator.d.ts
@@ -0,0 +1,195 @@
+/* tslint:disable */
+
+// Type definitions for asynciterator 2.0.1
+
+declare module "asynciterator" {
+
+ import { EventEmitter } from "events";
+
+ export abstract class AsyncIterator extends EventEmitter {
+ static STATES: ["INIT", "OPEN", "CLOSING", "CLOSED", "ENDED"];
+ static INIT: 0;
+ static OPEN: 1;
+ static CLOSING: 2;
+ static CLOSED: 3;
+ static ENDED: 4;
+
+ _state: number;
+ _readable: boolean;
+ _destination?: AsyncIterator;
+
+ readable: boolean;
+ closed: boolean;
+ ended: boolean;
+
+ constructor();
+
+ read(): T;
+
+ each(callback: (data: T) => void, self?: any): void;
+
+ close(): void;
+
+ _changeState(newState: number, eventAsync?: boolean): void;
+
+ private _hasListeners(eventName: string | symbol): boolean;
+
+ // tslint:disable-next-line ban-types
+ private _addSingleListener(eventName: string | symbol, listener: Function): void;
+
+ _end(): void;
+
+ getProperty(propertyName: string, callback?: (value: any) => void): any;
+
+ setProperty(propertyName: string, value: any): void;
+
+ getProperties(): { [id: string]: any };
+
+ setProperties(properties: { [id: string]: any }): void;
+
+ copyProperties(source: AsyncIterator, propertyNames: string[]): void;
+
+ toString(): string;
+
+ _toStringDetails(): string;
+
+ transform(options?: SimpleTransformIteratorOptions): SimpleTransformIterator;
+
+ map(mapper: (item: T) => T2, self?: object): SimpleTransformIterator;
+
+ filter(filter: (item: T) => boolean, self?: object): SimpleTransformIterator;
+
+ prepend(items: T[] | AsyncIterator): SimpleTransformIterator;
+
+ append(items: T[] | AsyncIterator): SimpleTransformIterator;
+
+ surround(prepend: T[] | AsyncIterator, append: T[] | AsyncIterator): SimpleTransformIterator;
+
+ skip(offset: number): SimpleTransformIterator;
+
+ take(limit: number): SimpleTransformIterator;
+
+ range(start: number, end: number): SimpleTransformIterator;
+
+ clone(): ClonedIterator;
+
+ static range(start?: number, end?: number, step?: number): IntegerIterator;
+ }
+
+ export class EmptyIterator extends AsyncIterator {
+ _state: 4;
+ }
+
+ export class SingletonIterator extends AsyncIterator {
+ constructor(item?: T);
+ }
+
+ export class ArrayIterator extends AsyncIterator {
+ constructor(items?: T[]);
+ }
+
+ export interface IntegerIteratorOptions {
+ step?: number;
+ end?: number;
+ start?: number;
+ }
+
+ export class IntegerIterator extends AsyncIterator {
+ _step: number;
+ _last: number;
+ _next: number;
+
+ constructor(options?: IntegerIteratorOptions);
+ }
+
+ export interface BufferedIteratorOptions {
+ maxBufferSize?: number;
+ autoStart?: boolean;
+ }
+
+ export class BufferedIterator extends AsyncIterator {
+ maxBufferSize: number;
+ _pushedCount: number;
+ _buffer: T[];
+
+ _init(autoStart: boolean): void;
+
+ _begin(done: () => void): void;
+
+ _read(count: number, done: () => void): void;
+
+ _push(item: T): void;
+
+ _fillBuffer(): void;
+
+ _completeClose(): void;
+
+ _flush(done: () => void): void;
+
+ constructor(options?: BufferedIteratorOptions);
+ }
+
+ export interface TransformIteratorOptions extends BufferedIteratorOptions {
+ optional?: boolean;
+ source?: AsyncIterator;
+ destroySource?: boolean;
+ }
+
+ export class TransformIterator extends BufferedIterator {
+ _optional: boolean;
+ source: AsyncIterator;
+
+ _validateSource(source: AsyncIterator, allowDestination?: boolean): void;
+
+ _transform(item: S, done: (result: T) => void): void;
+
+ _closeWhenDone(): void;
+
+ constructor(source?: AsyncIterator | TransformIteratorOptions, options?: TransformIteratorOptions);
+ }
+
+ export interface SimpleTransformIteratorOptions extends TransformIteratorOptions {
+ offset?: number;
+ limit?: number;
+ prepend?: T[];
+ append?: T[];
+
+ filter?(item: S): boolean;
+
+ map?(item: S): T;
+
+ transform?(item: S, callback: (result: T) => void): void;
+ }
+
+ export class SimpleTransformIterator extends TransformIterator {
+ _offset: number;
+ _limit: number;
+ _prepender?: ArrayIterator;
+ _appender?: ArrayIterator;
+
+ _filter?(item: S): boolean;
+
+ _map?(item: S): T;
+
+ _transform(item: S, done: (result: T) => void): void;
+
+ _insert(inserter: AsyncIterator, done: () => void): void;
+
+ constructor(source?: AsyncIterator | SimpleTransformIteratorOptions,
+ options?: SimpleTransformIteratorOptions);
+ }
+
+ export class MultiTransformIterator extends TransformIterator {
+ _transformerQueue: S[];
+
+ _createTransformer(element: S): AsyncIterator;
+
+ constructor(source?: AsyncIterator | TransformIteratorOptions, options?: TransformIteratorOptions);
+ }
+
+ export class ClonedIterator extends TransformIterator {
+ _readPosition: number;
+
+ constructor(source?: AsyncIterator);
+ }
+}
diff --git a/src/catalog.delijn.ts b/src/catalog.delijn.ts
index 95726afd..386a6ab1 100644
--- a/src/catalog.delijn.ts
+++ b/src/catalog.delijn.ts
@@ -1,18 +1,20 @@
import Catalog from "./Catalog";
-import TravelMode from "./TravelMode";
+import TravelMode from "./enums/TravelMode";
-const catalog = new Catalog();
+/* tslint:disable:max-line-length */
-catalog.addStopsFetcher("http://openplanner.ilabt.imec.be/delijn/Antwerpen/stops");
-catalog.addStopsFetcher("http://openplanner.ilabt.imec.be/delijn/Limburg/stops");
-catalog.addStopsFetcher("http://openplanner.ilabt.imec.be/delijn/Oost-Vlaanderen/stops");
-catalog.addStopsFetcher("http://openplanner.ilabt.imec.be/delijn/Vlaams-Brabant/stops");
-catalog.addStopsFetcher("http://openplanner.ilabt.imec.be/delijn/West-Vlaanderen/stops");
+const catalogDeLijn = new Catalog();
-catalog.addConnectionsFetcher("http://openplanner.ilabt.imec.be/delijn/Antwerpen/connections", TravelMode.Bus);
-catalog.addConnectionsFetcher("http://openplanner.ilabt.imec.be/delijn/Limburg/connections", TravelMode.Bus);
-catalog.addConnectionsFetcher("http://openplanner.ilabt.imec.be/delijn/Oost-Vlaanderen/connections", TravelMode.Bus);
-catalog.addConnectionsFetcher("http://openplanner.ilabt.imec.be/delijn/Vlaams-Brabant/connections", TravelMode.Bus);
-catalog.addConnectionsFetcher("http://openplanner.ilabt.imec.be/delijn/West-Vlaanderen/connections", TravelMode.Bus);
+catalogDeLijn.addStopsSource("https://openplanner.ilabt.imec.be/delijn/Antwerpen/stops");
+catalogDeLijn.addStopsSource("https://openplanner.ilabt.imec.be/delijn/Limburg/stops");
+catalogDeLijn.addStopsSource("https://openplanner.ilabt.imec.be/delijn/Oost-Vlaanderen/stops");
+catalogDeLijn.addStopsSource("https://openplanner.ilabt.imec.be/delijn/Vlaams-Brabant/stops");
+catalogDeLijn.addStopsSource("https://openplanner.ilabt.imec.be/delijn/West-Vlaanderen/stops");
-export default catalog;
+catalogDeLijn.addConnectionsSource("https://openplanner.ilabt.imec.be/delijn/Antwerpen/connections", TravelMode.Bus);
+catalogDeLijn.addConnectionsSource("https://openplanner.ilabt.imec.be/delijn/Limburg/connections", TravelMode.Bus);
+catalogDeLijn.addConnectionsSource("https://openplanner.ilabt.imec.be/delijn/Oost-Vlaanderen/connections", TravelMode.Bus);
+catalogDeLijn.addConnectionsSource("https://openplanner.ilabt.imec.be/delijn/Vlaams-Brabant/connections", TravelMode.Bus);
+catalogDeLijn.addConnectionsSource("https://openplanner.ilabt.imec.be/delijn/West-Vlaanderen/connections", TravelMode.Bus);
+
+export default catalogDeLijn;
diff --git a/src/catalog.nmbs.ts b/src/catalog.nmbs.ts
index f9edf2e8..7d4b1846 100644
--- a/src/catalog.nmbs.ts
+++ b/src/catalog.nmbs.ts
@@ -1,8 +1,9 @@
import Catalog from "./Catalog";
-import TravelMode from "./TravelMode";
+import TravelMode from "./enums/TravelMode";
-const catalog = new Catalog();
-catalog.addStopsFetcher("https://irail.be/stations/NMBS");
-catalog.addConnectionsFetcher("https://graph.irail.be/sncb/connections", TravelMode.Train);
+const catalogNmbs = new Catalog();
-export default catalog;
+catalogNmbs.addStopsSource("https://irail.be/stations/NMBS");
+catalogNmbs.addConnectionsSource("https://graph.irail.be/sncb/connections", TravelMode.Train);
+
+export default catalogNmbs;
diff --git a/src/demo.cli.ts b/src/demo.cli.ts
index d9bb265e..6d8e2780 100644
--- a/src/demo.cli.ts
+++ b/src/demo.cli.ts
@@ -8,6 +8,6 @@ const isDebugging = process && process.argv.includes("--debug");
}
runDemo(true)
- .then(() => console.log("Success"))
+ .then((success) => console.log(success ? "Success" : "Fail"))
.catch((e) => console.error(e));
})();
diff --git a/src/demo.ts b/src/demo.ts
index 240f8605..118cac8e 100644
--- a/src/demo.ts
+++ b/src/demo.ts
@@ -1,4 +1,4 @@
-import EventType from "./EventType";
+import EventType from "./enums/EventType";
import Planner from "./index";
import IPath from "./interfaces/IPath";
import Units from "./util/Units";
@@ -7,17 +7,34 @@ export default async (logResults) => {
const planner = new Planner();
+ planner.prefetchStops();
+ planner.prefetchConnections();
+
if (logResults) {
let scannedPages = 0;
let scannedConnections = 0;
+ // let logFetch = true;
+
+ if (logResults) {
+ console.log("Start prefetch");
+ }
+
planner
- .on(EventType.Query, (query) => {
- console.log("Query", query);
+ .on(EventType.InvalidQuery, (error) => {
+ console.log("InvalidQuery", error);
+ })
+ .on(EventType.AbortQuery, (reason) => {
+ console.log("AbortQuery", reason);
})
- .on(EventType.QueryExponential, (query) => {
+ .on(EventType.Query, (Query) => {
+ console.log("Query", Query);
+ })
+ .on(EventType.SubQuery, (query) => {
const { minimumDepartureTime, maximumArrivalTime } = query;
+ // logFetch = true;
+
console.log("Total scanned pages", scannedPages);
console.log("Total scanned connections", scannedConnections);
console.log("[Subquery]", minimumDepartureTime, maximumArrivalTime, maximumArrivalTime - minimumDepartureTime);
@@ -25,45 +42,63 @@ export default async (logResults) => {
.on(EventType.LDFetchGet, (url, duration) => {
scannedPages++;
console.log(`[GET] ${url} (${duration}ms)`);
+
+ // if (logFetch) {
+ // console.log(`[GET] ${url} (${duration}ms)`);
+ // logFetch = false;
+ // }
})
.on(EventType.ConnectionScan, (connection) => {
scannedConnections++;
+ })
+ .on(EventType.Warning, (e) => {
+ console.warn(e);
});
}
- const publicTransportResult = await planner.query({
- publicTransportOnly: true,
- // from: "https://data.delijn.be/stops/201657",
- // to: "https://data.delijn.be/stops/205910",
- // from: "https://data.delijn.be/stops/200455", // Deinze weg op Grammene +456
- // to: "https://data.delijn.be/stops/502481", // Tielt Metaalconstructie Goossens
- // from: "https://data.delijn.be/stops/509927", // Tield Rameplein perron 1
- // to: "https://data.delijn.be/stops/200455", // Deinze weg op Grammene +456
- from: "http://irail.be/stations/NMBS/008896925", // Ingelmunster
- to: "http://irail.be/stations/NMBS/008892007", // Ghent-Sint-Pieters
- minimumDepartureTime: new Date(),
- maximumTransferDuration: Units.fromHours(0.5),
- });
-
- return new Promise((resolve, reject) => {
- let i = 0;
-
- publicTransportResult.take(3)
- .on("data", (path: IPath) => {
- ++i;
-
- if (logResults) {
- console.log(i);
- console.log(JSON.stringify(path, null, " "));
- console.log("\n");
- }
-
- if (i === 3) {
- resolve(true);
- }
+ return wait(5000)
+ .then(() => new Promise((resolve, reject) => {
+ if (logResults) {
+ console.log("Start query");
+ }
+
+ const amount = 3;
+ let i = 0;
+
+ planner.query({
+ publicTransportOnly: true,
+ // from: "https://data.delijn.be/stops/201657",
+ // to: "https://data.delijn.be/stops/205910",
+ // from: "https://data.delijn.be/stops/200455", // Deinze weg op Grammene +456
+ // to: "https://data.delijn.be/stops/502481", // Tielt Metaalconstructie Goossens
+ // from: "https://data.delijn.be/stops/509927", // Tield Rameplein perron 1
+ // to: "https://data.delijn.be/stops/200455", // Deinze weg op Grammene +456
+ from: "Ingelmunster", // Ingelmunster
+ to: "http://irail.be/stations/NMBS/008892007", // Ghent-Sint-Pieters
+ minimumDepartureTime: new Date(),
+ maximumTransferDuration: Units.fromHours(0.5),
})
- .on("end", () => {
- resolve(false);
- });
- });
+ .take(amount)
+ .on("error", (error) => {
+ resolve(false);
+ })
+ .on("data", (path: IPath) => {
+ ++i;
+
+ if (logResults) {
+ console.log(i);
+ console.log(JSON.stringify(path, null, " "));
+ console.log("\n");
+ }
+
+ if (i === amount) {
+ resolve(true);
+ }
+ })
+ .on("end", () => {
+ resolve(false);
+ });
+ }));
};
+
+const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
diff --git a/src/fetcher/connections/DropOffType.ts b/src/enums/DropOffType.ts
similarity index 100%
rename from src/fetcher/connections/DropOffType.ts
rename to src/enums/DropOffType.ts
diff --git a/src/EventType.ts b/src/enums/EventType.ts
similarity index 57%
rename from src/EventType.ts
rename to src/enums/EventType.ts
index d7cfe1a7..ba92ca51 100644
--- a/src/EventType.ts
+++ b/src/enums/EventType.ts
@@ -1,8 +1,16 @@
enum EventType {
Query = "query",
- QueryExponential = "query-exponential",
- QueryAbort = "query-abort",
+ SubQuery = "sub-query",
+ AbortQuery = "abort-query",
+ InvalidQuery = "invalid-query",
+
LDFetchGet = "ldfetch-get",
+
+ Warning = "warning",
+
+ ConnectionPrefetch = "connection-prefetch",
+ ConnectionIteratorView = "connection-iterator-view",
+
ConnectionScan = "connection-scan",
FinalReachableStops = "final-reachable-stops",
InitialReachableStops = "initial-reachable-stops",
diff --git a/src/fetcher/connections/PickupType.ts b/src/enums/PickupType.ts
similarity index 100%
rename from src/fetcher/connections/PickupType.ts
rename to src/enums/PickupType.ts
diff --git a/src/planner/stops/ReachableStopsFinderMode.ts b/src/enums/ReachableStopsFinderMode.ts
similarity index 100%
rename from src/planner/stops/ReachableStopsFinderMode.ts
rename to src/enums/ReachableStopsFinderMode.ts
diff --git a/src/planner/stops/ReachableStopsSearchPhase.ts b/src/enums/ReachableStopsSearchPhase.ts
similarity index 100%
rename from src/planner/stops/ReachableStopsSearchPhase.ts
rename to src/enums/ReachableStopsSearchPhase.ts
diff --git a/src/TravelMode.ts b/src/enums/TravelMode.ts
similarity index 100%
rename from src/TravelMode.ts
rename to src/enums/TravelMode.ts
diff --git a/src/errors/InvalidQueryError.ts b/src/errors/InvalidQueryError.ts
new file mode 100644
index 00000000..13339a13
--- /dev/null
+++ b/src/errors/InvalidQueryError.ts
@@ -0,0 +1,5 @@
+import EventType from "../enums/EventType";
+
+export default class InvalidQueryError extends Error {
+ public eventType = EventType.InvalidQuery;
+}
diff --git a/src/errors/LocationResolverError.ts b/src/errors/LocationResolverError.ts
new file mode 100644
index 00000000..65175f81
--- /dev/null
+++ b/src/errors/LocationResolverError.ts
@@ -0,0 +1,3 @@
+export default class LocationResolverError extends Error {
+
+}
diff --git a/src/fetcher/LDFetch.ts b/src/fetcher/LDFetch.ts
index fa5492a7..71ab21d2 100644
--- a/src/fetcher/LDFetch.ts
+++ b/src/fetcher/LDFetch.ts
@@ -2,7 +2,7 @@ import { inject, injectable } from "inversify";
import LDFetchBase from "ldfetch";
import { Triple } from "rdf-js";
import Context from "../Context";
-import EventType from "../EventType";
+import EventType from "../enums/EventType";
import TYPES from "../types";
export interface ILDFetchResponse {
diff --git a/src/fetcher/connections/merge/ConnectionsProviderMerge.ts b/src/fetcher/connections/ConnectionsProviderMerge.ts
similarity index 70%
rename from src/fetcher/connections/merge/ConnectionsProviderMerge.ts
rename to src/fetcher/connections/ConnectionsProviderMerge.ts
index 9841fc99..7e0fb64b 100644
--- a/src/fetcher/connections/merge/ConnectionsProviderMerge.ts
+++ b/src/fetcher/connections/ConnectionsProviderMerge.ts
@@ -1,17 +1,18 @@
import { AsyncIterator } from "asynciterator";
import { inject, injectable } from "inversify";
-import Catalog from "../../../Catalog";
-import TYPES, { ConnectionsFetcherFactory } from "../../../types";
-import MergeIterator from "../../../util/iterators/MergeIterator";
-import IConnection from "../IConnection";
-import IConnectionsFetcher from "../IConnectionsFetcher";
-import IConnectionsFetcherConfig from "../IConnectionsFetcherConfig";
+import Catalog from "../../Catalog";
+import TYPES, { ConnectionsFetcherFactory } from "../../types";
+import MergeIterator from "../../util/iterators/MergeIterator";
+import IConnection from "./IConnection";
+import IConnectionsFetcher from "./IConnectionsFetcher";
+import IConnectionsIteratorOptions from "./IConnectionsIteratorOptions";
+import IConnectionsProvider from "./IConnectionsProvider";
/**
* Instantiates and merge sorts all registered connection fetchers
*/
@injectable()
-export default class ConnectionsProviderMerge implements IConnectionsFetcher {
+export default class ConnectionsProviderMerge implements IConnectionsProvider {
private static forwardsConnectionSelector(connections: IConnection[]): number {
if (connections.length === 1) {
@@ -51,7 +52,7 @@ export default class ConnectionsProviderMerge implements IConnectionsFetcher {
return latestIndex;
}
- private config: IConnectionsFetcherConfig;
+ private options: IConnectionsIteratorOptions;
private connectionsFetchers: IConnectionsFetcher[];
constructor(
@@ -60,17 +61,21 @@ export default class ConnectionsProviderMerge implements IConnectionsFetcher {
) {
this.connectionsFetchers = [];
- for (const { accessUrl, travelMode } of catalog.connectionsFetcherConfigs) {
+ for (const { accessUrl, travelMode } of catalog.connectionsSourceConfigs) {
this.connectionsFetchers.push(connectionsFetcherFactory(accessUrl, travelMode));
}
}
+ public prefetchConnections(): void {
+ return;
+ }
+
public createIterator(): AsyncIterator {
const iterators = this.connectionsFetchers
.map((fetcher) => fetcher.createIterator());
- const selector = this.config.backward ?
+ const selector = this.options.backward ?
ConnectionsProviderMerge.backwardsConnectionsSelector
:
ConnectionsProviderMerge.forwardsConnectionSelector;
@@ -78,10 +83,10 @@ export default class ConnectionsProviderMerge implements IConnectionsFetcher {
return new MergeIterator(iterators, selector, true);
}
- public setConfig(config: IConnectionsFetcherConfig): void {
- this.config = config;
+ public setIteratorOptions(options: IConnectionsIteratorOptions): void {
+ this.options = options;
this.connectionsFetchers.forEach((fetcher) => {
- fetcher.setConfig(config);
+ fetcher.setIteratorOptions(options);
});
}
}
diff --git a/src/fetcher/connections/ConnectionsProviderPassthrough.ts b/src/fetcher/connections/ConnectionsProviderPassthrough.ts
index e7c7c43c..c6718081 100644
--- a/src/fetcher/connections/ConnectionsProviderPassthrough.ts
+++ b/src/fetcher/connections/ConnectionsProviderPassthrough.ts
@@ -4,11 +4,11 @@ import Catalog from "../../Catalog";
import TYPES, { ConnectionsFetcherFactory } from "../../types";
import IConnection from "./IConnection";
import IConnectionsFetcher from "./IConnectionsFetcher";
-import IConnectionsFetcherConfig from "./IConnectionsFetcherConfig";
+import IConnectionsIteratorOptions from "./IConnectionsIteratorOptions";
import IConnectionsProvider from "./IConnectionsProvider";
/**
- * Passes through one [[IConnectionsFetcher]], the first one if there are multiple
+ * Passes through any method calls to a *single* [[IConnectionsFetcher]], the first if there are multiple source configs
* This provider is most/only useful if there is only one fetcher
*/
@injectable()
@@ -20,16 +20,20 @@ export default class ConnectionsProviderPassthrough implements IConnectionsProvi
@inject(TYPES.ConnectionsFetcherFactory) connectionsFetcherFactory: ConnectionsFetcherFactory,
@inject(TYPES.Catalog) catalog: Catalog,
) {
- const { accessUrl, travelMode } = catalog.connectionsFetcherConfigs[0];
+ const { accessUrl, travelMode } = catalog.connectionsSourceConfigs[0];
this.connectionsFetcher = connectionsFetcherFactory(accessUrl, travelMode);
}
+ public prefetchConnections(): void {
+ this.connectionsFetcher.prefetchConnections();
+ }
+
public createIterator(): AsyncIterator {
return this.connectionsFetcher.createIterator();
}
- public setConfig(config: IConnectionsFetcherConfig): void {
- this.connectionsFetcher.setConfig(config);
+ public setIteratorOptions(options: IConnectionsIteratorOptions): void {
+ this.connectionsFetcher.setIteratorOptions(options);
}
}
diff --git a/src/fetcher/connections/IConnection.ts b/src/fetcher/connections/IConnection.ts
index 1be060e7..19c9669c 100644
--- a/src/fetcher/connections/IConnection.ts
+++ b/src/fetcher/connections/IConnection.ts
@@ -1,7 +1,7 @@
+import DropOffType from "../../enums/DropOffType";
+import PickupType from "../../enums/PickupType";
+import TravelMode from "../../enums/TravelMode";
import { DurationMs } from "../../interfaces/units";
-import TravelMode from "../../TravelMode";
-import DropOffType from "./DropOffType";
-import PickupType from "./PickupType";
/**
* Interface for a Connection. This describes an actual transport vehicle going from its
diff --git a/src/fetcher/connections/IConnectionsFetcherConfig.ts b/src/fetcher/connections/IConnectionsFetcherConfig.ts
deleted file mode 100644
index cbb27f2d..00000000
--- a/src/fetcher/connections/IConnectionsFetcherConfig.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export default interface IConnectionsFetcherConfig {
- upperBoundDate?: Date;
- lowerBoundDate?: Date;
- backward?: boolean;
-}
diff --git a/src/fetcher/connections/IConnectionsIteratorOptions.ts b/src/fetcher/connections/IConnectionsIteratorOptions.ts
new file mode 100644
index 00000000..0060e196
--- /dev/null
+++ b/src/fetcher/connections/IConnectionsIteratorOptions.ts
@@ -0,0 +1,9 @@
+/**
+ * Options passed to [[IConnectionsProvider]] and [[IConnectionsFetcher]] instances
+ * for creating AsyncIterators of [[IConnection]] instances.
+ */
+export default interface IConnectionsIteratorOptions {
+ upperBoundDate?: Date;
+ lowerBoundDate?: Date;
+ backward?: boolean;
+}
diff --git a/src/fetcher/connections/IConnectionsProvider.ts b/src/fetcher/connections/IConnectionsProvider.ts
index 7d89c6c7..054c1c53 100644
--- a/src/fetcher/connections/IConnectionsProvider.ts
+++ b/src/fetcher/connections/IConnectionsProvider.ts
@@ -1,6 +1,6 @@
import { AsyncIterator } from "asynciterator";
import IConnection from "./IConnection";
-import IConnectionsFetcherConfig from "./IConnectionsFetcherConfig";
+import IConnectionsIteratorOptions from "./IConnectionsIteratorOptions";
/**
* A IConnectionsProvider serves as interface to other classes that want to use [[IConnection]] instances
@@ -8,6 +8,7 @@ import IConnectionsFetcherConfig from "./IConnectionsFetcherConfig";
* instances
*/
export default interface IConnectionsProvider {
+ prefetchConnections: () => void;
createIterator: () => AsyncIterator;
- setConfig: (config: IConnectionsFetcherConfig) => void;
+ setIteratorOptions: (options: IConnectionsIteratorOptions) => void;
}
diff --git a/src/fetcher/connections/ld-fetch/ConnectionsPageParser.ts b/src/fetcher/connections/hydra/ConnectionsPageParser.ts
similarity index 96%
rename from src/fetcher/connections/ld-fetch/ConnectionsPageParser.ts
rename to src/fetcher/connections/hydra/ConnectionsPageParser.ts
index a3a25fe9..084d34f2 100644
--- a/src/fetcher/connections/ld-fetch/ConnectionsPageParser.ts
+++ b/src/fetcher/connections/hydra/ConnectionsPageParser.ts
@@ -1,10 +1,10 @@
import { Triple } from "rdf-js";
-import TravelMode from "../../../TravelMode";
+import DropOffType from "../../../enums/DropOffType";
+import PickupType from "../../../enums/PickupType";
+import TravelMode from "../../../enums/TravelMode";
import Rdf from "../../../util/Rdf";
import Units from "../../../util/Units";
-import DropOffType from "../DropOffType";
import IConnection from "../IConnection";
-import PickupType from "../PickupType";
interface IEntity {
}
diff --git a/src/fetcher/connections/hydra/HydraPageIterator.ts b/src/fetcher/connections/hydra/HydraPageIterator.ts
new file mode 100644
index 00000000..ea962292
--- /dev/null
+++ b/src/fetcher/connections/hydra/HydraPageIterator.ts
@@ -0,0 +1,69 @@
+import { BufferedIterator } from "asynciterator";
+import LdFetch from "ldfetch";
+import UriTemplate from "uritemplate";
+import HydraPageParser2 from "./HydraPageParser";
+import IHydraPage from "./IHydraPage";
+import IHydraPageIteratorConfig from "./IHydraPageIteratorConfig";
+
+export default class HydraPageIterator extends BufferedIterator {
+ private readonly baseUrl: string;
+ private readonly ldFetch: LdFetch;
+ private readonly config: IHydraPageIteratorConfig;
+
+ private currentPage: IHydraPage;
+
+ constructor(
+ baseUrl: string,
+ ldFetch: LdFetch,
+ config: IHydraPageIteratorConfig,
+ ) {
+ super({
+ autoStart: true,
+ });
+
+ this.baseUrl = baseUrl;
+ this.ldFetch = ldFetch;
+ this.config = config;
+ }
+
+ public _begin(done: () => void): void {
+ this.ldFetch.get(this.baseUrl)
+ .then((response) => {
+ const parser = new HydraPageParser2(response.triples);
+ const searchTemplate: UriTemplate = parser.getSearchTemplate();
+
+ const firstPageIri = searchTemplate.expand(this.config.initialTemplateVariables);
+
+ this.loadPage(firstPageIri)
+ .then(() => done());
+ });
+ }
+
+ public _read(count: number, done: () => void): void {
+
+ const pageIri = this.config.backward ?
+ this.currentPage.previousPageIri : this.currentPage.nextPageIri;
+
+ this.loadPage(pageIri)
+ .then(() => done());
+ }
+
+ private async loadPage(url: string) {
+ await this.ldFetch.get(url)
+ .then((response) => {
+
+ const parser = new HydraPageParser2(response.triples);
+ const page = parser.getPage(0);
+
+ if (this.config.backward) {
+ page.previousPageIri = parser.getPreviousPageIri();
+
+ } else {
+ page.nextPageIri = parser.getNextPageIri();
+ }
+
+ this.currentPage = page;
+ this._push(this.currentPage);
+ });
+ }
+}
diff --git a/src/fetcher/connections/ld-fetch/HydraPageParser.ts b/src/fetcher/connections/hydra/HydraPageParser.ts
similarity index 87%
rename from src/fetcher/connections/ld-fetch/HydraPageParser.ts
rename to src/fetcher/connections/hydra/HydraPageParser.ts
index e28779a3..45ac8261 100644
--- a/src/fetcher/connections/ld-fetch/HydraPageParser.ts
+++ b/src/fetcher/connections/hydra/HydraPageParser.ts
@@ -1,8 +1,6 @@
import { Triple } from "rdf-js";
import UriTemplate from "uritemplate";
-import TravelMode from "../../../TravelMode";
import Rdf from "../../../util/Rdf";
-import ConnectionsPageParser from "./ConnectionsPageParser";
import IHydraPage from "./IHydraPage";
/**
@@ -18,14 +16,11 @@ export default class HydraPageParser {
this.documentIri = this.getDocumentIri();
}
- public getPage(index: number, travelMode: TravelMode): IHydraPage {
- const connectionsParser = new ConnectionsPageParser(this.documentIri, this.triples);
- const connections = connectionsParser.getConnections(travelMode);
-
+ public getPage(index: number): IHydraPage {
return {
index,
documentIri: this.documentIri,
- connections,
+ triples: this.triples,
};
}
diff --git a/src/fetcher/connections/ld-fetch/IHydraPage.ts b/src/fetcher/connections/hydra/IHydraPage.ts
similarity index 64%
rename from src/fetcher/connections/ld-fetch/IHydraPage.ts
rename to src/fetcher/connections/hydra/IHydraPage.ts
index be94cad5..a629686a 100644
--- a/src/fetcher/connections/ld-fetch/IHydraPage.ts
+++ b/src/fetcher/connections/hydra/IHydraPage.ts
@@ -1,9 +1,9 @@
-import IConnection from "../IConnection";
+import { Triple } from "rdf-js";
export default interface IHydraPage {
index: number;
documentIri: string;
nextPageIri?: string;
previousPageIri?: string;
- connections: IConnection[];
+ triples: Triple[];
}
diff --git a/src/fetcher/connections/hydra/IHydraPageIteratorConfig.ts b/src/fetcher/connections/hydra/IHydraPageIteratorConfig.ts
new file mode 100644
index 00000000..871acfe8
--- /dev/null
+++ b/src/fetcher/connections/hydra/IHydraPageIteratorConfig.ts
@@ -0,0 +1,4 @@
+export default interface IHydraPageIteratorConfig {
+ backward: boolean;
+ initialTemplateVariables: object;
+}
diff --git a/src/fetcher/connections/ld-fetch/ConnectionsFetcherLazy.ts b/src/fetcher/connections/lazy/ConnectionsFetcherLazy.ts
similarity index 74%
rename from src/fetcher/connections/ld-fetch/ConnectionsFetcherLazy.ts
rename to src/fetcher/connections/lazy/ConnectionsFetcherLazy.ts
index 18dd7e66..8e36dbd6 100644
--- a/src/fetcher/connections/ld-fetch/ConnectionsFetcherLazy.ts
+++ b/src/fetcher/connections/lazy/ConnectionsFetcherLazy.ts
@@ -1,11 +1,11 @@
import { AsyncIterator } from "asynciterator";
import { inject, injectable } from "inversify";
import LDFetch from "ldfetch";
-import TravelMode from "../../../TravelMode";
+import TravelMode from "../../../enums/TravelMode";
import TYPES from "../../../types";
import IConnection from "../IConnection";
import IConnectionsFetcher from "../IConnectionsFetcher";
-import IConnectionsFetcherConfig from "../IConnectionsFetcherConfig";
+import IConnectionsIteratorOptions from "../IConnectionsIteratorOptions";
import ConnectionsIteratorLazy from "./ConnectionsIteratorLazy";
/**
@@ -16,7 +16,7 @@ import ConnectionsIteratorLazy from "./ConnectionsIteratorLazy";
export default class ConnectionsFetcherLazy implements IConnectionsFetcher {
protected readonly ldFetch: LDFetch;
- protected config: IConnectionsFetcherConfig;
+ protected options: IConnectionsIteratorOptions;
private travelMode: TravelMode;
private accessUrl: string;
@@ -32,16 +32,20 @@ export default class ConnectionsFetcherLazy implements IConnectionsFetcher {
this.accessUrl = accessUrl;
}
+ public prefetchConnections(): void {
+ return;
+ }
+
public createIterator(): AsyncIterator {
return new ConnectionsIteratorLazy(
this.accessUrl,
this.travelMode,
this.ldFetch,
- this.config,
+ this.options,
);
}
- public setConfig(config: IConnectionsFetcherConfig): void {
- this.config = config;
+ public setIteratorOptions(options: IConnectionsIteratorOptions): void {
+ this.options = options;
}
}
diff --git a/src/fetcher/connections/ld-fetch/ConnectionsIteratorLazy.test.ts b/src/fetcher/connections/lazy/ConnectionsIteratorLazy.test.ts
similarity index 84%
rename from src/fetcher/connections/ld-fetch/ConnectionsIteratorLazy.test.ts
rename to src/fetcher/connections/lazy/ConnectionsIteratorLazy.test.ts
index 3195d75c..4366f1ed 100644
--- a/src/fetcher/connections/ld-fetch/ConnectionsIteratorLazy.test.ts
+++ b/src/fetcher/connections/lazy/ConnectionsIteratorLazy.test.ts
@@ -1,7 +1,8 @@
import "jest";
import LdFetch from "ldfetch";
-import TravelMode from "../../../TravelMode";
+import TravelMode from "../../../enums/TravelMode";
import IConnection from "../IConnection";
+import IConnectionsIteratorOptions from "../IConnectionsIteratorOptions";
import ConnectionsIteratorLazy from "./ConnectionsIteratorLazy";
const CONNECTIONS_TO_LOAD = 500; // Should be more than contained on first page
@@ -9,7 +10,7 @@ const CONNECTIONS_TO_LOAD = 500; // Should be more than contained on first page
test("[ConnectionsIteratorLazy] iterate forwards", (done) => {
jest.setTimeout(90000);
- const config = {
+ const options: IConnectionsIteratorOptions = {
backward: false,
lowerBoundDate: new Date(2018, 10, 22, 10),
};
@@ -17,7 +18,7 @@ test("[ConnectionsIteratorLazy] iterate forwards", (done) => {
"https://graph.irail.be/sncb/connections",
TravelMode.Train,
new LdFetch(),
- config,
+ options,
);
let i = 0;
diff --git a/src/fetcher/connections/lazy/ConnectionsIteratorLazy.ts b/src/fetcher/connections/lazy/ConnectionsIteratorLazy.ts
new file mode 100644
index 00000000..d3c27ec3
--- /dev/null
+++ b/src/fetcher/connections/lazy/ConnectionsIteratorLazy.ts
@@ -0,0 +1,51 @@
+import { ArrayIterator } from "asynciterator";
+import LdFetch from "ldfetch";
+import TravelMode from "../../../enums/TravelMode";
+import FlatMapIterator from "../../../util/iterators/FlatMapIterator";
+import ConnectionsPageParser from "../hydra/ConnectionsPageParser";
+import HydraPageIterator from "../hydra/HydraPageIterator";
+import IHydraPage from "../hydra/IHydraPage";
+import IHydraPageIteratorConfig from "../hydra/IHydraPageIteratorConfig";
+import IConnection from "../IConnection";
+import IConnectionsIteratorOptions from "../IConnectionsIteratorOptions";
+
+/**
+ * Base class for fetching linked connections with LDFetch and letting the caller iterate over them asynchronously
+ * through implementing the AsyncIterator protocol.
+ * LDFetch returns documents as an array of RDF triples.
+ * The meta Hydra triples are used for paginating to the next or previous page.
+ * The triples that describe linked connections get deserialized to instances of [[IConnection]]
+ */
+export default class ConnectionsIteratorLazy extends FlatMapIterator {
+ constructor(
+ baseUrl: string,
+ travelMode: TravelMode,
+ ldFetch: LdFetch,
+ options: IConnectionsIteratorOptions,
+ ) {
+ const departureTimeDate = options.backward ?
+ options.upperBoundDate : options.lowerBoundDate;
+
+ const pageIteratorConfig: IHydraPageIteratorConfig = {
+ backward: options.backward,
+ initialTemplateVariables: {
+ departureTime: departureTimeDate.toISOString(),
+ },
+ };
+
+ const pageIterator = new HydraPageIterator(baseUrl, ldFetch, pageIteratorConfig);
+ const parsePageConnections = (page: IHydraPage) => {
+ const connectionsParser = new ConnectionsPageParser(page.documentIri, page.triples);
+
+ const connections = connectionsParser.getConnections(travelMode);
+
+ if (options.backward) {
+ connections.reverse();
+ }
+
+ return new ArrayIterator(connections);
+ };
+
+ super(pageIterator, parsePageConnections);
+ }
+}
diff --git a/src/fetcher/connections/ld-fetch/ConnectionsIteratorLazy.ts b/src/fetcher/connections/ld-fetch/ConnectionsIteratorLazy.ts
deleted file mode 100644
index 5308d950..00000000
--- a/src/fetcher/connections/ld-fetch/ConnectionsIteratorLazy.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import { BufferedIterator } from "asynciterator";
-import LdFetch from "ldfetch";
-import UriTemplate from "uritemplate";
-import TravelMode from "../../../TravelMode";
-import IConnection from "../IConnection";
-import IConnectionsFetcherConfig from "../IConnectionsFetcherConfig";
-import HydraPageParser from "./HydraPageParser";
-import IHydraPage from "./IHydraPage";
-
-/**
- * Base class for fetching linked connections with LDFetch and letting the caller iterate over them asynchronously
- * through implementing the AsyncIterator protocol.
- * LDFetch returns documents as an array of RDF triples.
- * The meta Hydra triples are used for paginating to the next or previous page.
- * The triples that describe linked connections get deserialized to instances of [[IConnection]]
- */
-export default class ConnectionsIteratorLazy extends BufferedIterator {
- private readonly baseUrl: string;
- private readonly travelMode: TravelMode;
- private readonly ldFetch: LdFetch;
- private readonly config: IConnectionsFetcherConfig;
-
- private currentPage: IHydraPage;
-
- constructor(
- baseUrl: string,
- travelMode: TravelMode,
- ldFetch: LdFetch,
- config: IConnectionsFetcherConfig,
- ) {
- super({
- autoStart: true,
- });
-
- this.baseUrl = baseUrl;
- this.travelMode = travelMode;
- this.ldFetch = ldFetch;
- this.config = config;
- }
-
- public _begin(done: () => void): void {
- this.ldFetch.get(this.baseUrl)
- .then((response) => {
- const parser = new HydraPageParser(response.triples);
- const searchTemplate: UriTemplate = parser.getSearchTemplate();
-
- const departureTimeDate = this.config.backward ?
- this.config.upperBoundDate : this.config.lowerBoundDate;
-
- const firstPageIri = searchTemplate.expand({
- departureTime: departureTimeDate.toISOString(),
- });
-
- this.loadPage(firstPageIri)
- .then(() => done());
- });
- }
-
- public _read(count: number, done: () => void): void {
-
- const pageIri = this.config.backward ?
- this.currentPage.previousPageIri : this.currentPage.nextPageIri;
-
- this.loadPage(pageIri)
- .then(() => done());
- }
-
- private async loadPage(url: string) {
- await this.ldFetch.get(url)
- .then((response) => {
-
- const parser = new HydraPageParser(response.triples);
- const page = parser.getPage(0, this.travelMode);
-
- if (this.config.backward) {
- page.previousPageIri = parser.getPreviousPageIri();
-
- } else {
- page.nextPageIri = parser.getNextPageIri();
- }
-
- this.currentPage = page;
- this.pushCurrentPage();
- });
- }
-
- private pushCurrentPage(): void {
- const { connections } = this.currentPage;
-
- if (this.config.backward) {
- let c = connections.length - 1;
-
- while (c >= 0) {
- this._push(connections[c]);
- c--;
- }
-
- // Forwards
- } else {
- for (const connection of connections) {
- this._push(connection);
- }
- }
- }
-
-}
diff --git a/src/fetcher/connections/prefetch/ArrayViewIterator.test.ts b/src/fetcher/connections/prefetch/ArrayViewIterator.test.ts
new file mode 100644
index 00000000..e0fdfed6
--- /dev/null
+++ b/src/fetcher/connections/prefetch/ArrayViewIterator.test.ts
@@ -0,0 +1,80 @@
+import "jest";
+import ArrayViewIterator from "./ArrayViewIterator";
+
+describe("[ArrayViewIterator]", () => {
+
+ const sourceArray = [1, 2, 3, 4, 5, 6, 7, 8, 9];
+
+ it("step +1 and start < stop", (done) => {
+
+ const viewIterator = new ArrayViewIterator(
+ sourceArray, 1, 5, +1,
+ );
+
+ const expected = [2, 3, 4, 5, 6];
+ let currentRead = 0;
+
+ viewIterator.each((str: number) => {
+ expect(expected[currentRead++]).toBe(str);
+ });
+
+ viewIterator.on("end", () => {
+ expect(currentRead).toBe(expected.length);
+
+ done();
+ });
+ });
+
+ it("step +1 and start > stop", (done) => {
+
+ const viewIterator = new ArrayViewIterator(
+ sourceArray, 5, 1, +1,
+ );
+
+ const each = jest.fn();
+ viewIterator.each(each);
+
+ viewIterator.on("end", () => {
+ expect(each).not.toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it("step -1 and start > stop", (done) => {
+
+ const viewIterator = new ArrayViewIterator(
+ sourceArray, 5, 1, -1,
+ );
+
+ const expected = [2, 3, 4, 5, 6];
+ let currentRead = expected.length - 1;
+
+ viewIterator.each((str: number) => {
+ expect(expected[currentRead--]).toBe(str);
+ });
+
+ viewIterator.on("end", () => {
+ expect(currentRead).toBe(-1);
+
+ done();
+ });
+ });
+
+ it("step -1 and start < stop", (done) => {
+
+ const viewIterator = new ArrayViewIterator(
+ sourceArray, 1, 5, -1,
+ );
+
+ const each = jest.fn();
+ viewIterator.each(each);
+
+ viewIterator.on("end", () => {
+ expect(each).not.toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+});
diff --git a/src/fetcher/connections/prefetch/ArrayViewIterator.ts b/src/fetcher/connections/prefetch/ArrayViewIterator.ts
new file mode 100644
index 00000000..b8d0f856
--- /dev/null
+++ b/src/fetcher/connections/prefetch/ArrayViewIterator.ts
@@ -0,0 +1,47 @@
+import { AsyncIterator } from "asynciterator";
+
+export default class ArrayViewIterator extends AsyncIterator {
+
+ private readonly source: T[];
+ private readonly startIndex: number;
+ private readonly stopIndex: number;
+ private readonly step: number;
+
+ private currentIndex: number;
+
+ constructor(source: T[], startIndex: number, stopIndex: number, step: -1 | 1) {
+ super();
+
+ this.source = source;
+ this.startIndex = startIndex;
+ this.stopIndex = stopIndex;
+ this.step = step;
+
+ if (step > 0 ? stopIndex < startIndex : stopIndex > startIndex) {
+ this.close();
+ return;
+ }
+
+ this.currentIndex = startIndex;
+ this.readable = true;
+ }
+
+ public read(): T {
+ if (this.closed) {
+ return null;
+ }
+
+ const {source, step, currentIndex, stopIndex} = this;
+
+ if (step > 0 ? currentIndex > stopIndex : currentIndex < stopIndex) {
+ this.close();
+ return null;
+ }
+
+ const item = source[currentIndex];
+
+ this.currentIndex += step;
+
+ return item;
+ }
+}
diff --git a/src/fetcher/connections/prefetch/ConnectionsProviderPrefetch.ts b/src/fetcher/connections/prefetch/ConnectionsProviderPrefetch.ts
new file mode 100644
index 00000000..bb2b8bdb
--- /dev/null
+++ b/src/fetcher/connections/prefetch/ConnectionsProviderPrefetch.ts
@@ -0,0 +1,75 @@
+import { AsyncIterator } from "asynciterator";
+import { inject, injectable } from "inversify";
+import Catalog from "../../../Catalog";
+import Context from "../../../Context";
+import TYPES, { ConnectionsFetcherFactory } from "../../../types";
+import IConnection from "../IConnection";
+import IConnectionsFetcher from "../IConnectionsFetcher";
+import IConnectionsIteratorOptions from "../IConnectionsIteratorOptions";
+import IConnectionsProvider from "../IConnectionsProvider";
+import ConnectionsStore from "./ConnectionsStore";
+
+/**
+ * This connections provider implements the [[IConnectionsProvider.prefetchConnections]] method.
+ * When called, it asks an AsyncIterator from the instantiated [[IConnectionsFetcher]].
+ * All items from that iterator get appended to a [[ConnectionsStore]]
+ *
+ * When [[IConnectionsProvider.createIterator]] is called, it returns an iterator *view* from the [[ConnectionsStore]]
+ */
+@injectable()
+export default class ConnectionsProviderPrefetch implements IConnectionsProvider {
+
+ private static MAX_CONNECTIONS = 20000;
+
+ private readonly context: Context;
+ private readonly connectionsFetcher: IConnectionsFetcher;
+ private readonly connectionsStore: ConnectionsStore;
+
+ private startedPrefetching: boolean;
+ private connectionsIterator: AsyncIterator;
+ private connectionsIteratorOptions: IConnectionsIteratorOptions;
+
+ constructor(
+ @inject(TYPES.ConnectionsFetcherFactory) connectionsFetcherFactory: ConnectionsFetcherFactory,
+ @inject(TYPES.Catalog) catalog: Catalog,
+ @inject(TYPES.Context) context: Context,
+ ) {
+ const { accessUrl, travelMode } = catalog.connectionsSourceConfigs[0];
+
+ this.context = context;
+ this.connectionsFetcher = connectionsFetcherFactory(accessUrl, travelMode);
+ this.connectionsStore = new ConnectionsStore(context);
+ }
+
+ public prefetchConnections(): void {
+ if (!this.startedPrefetching) {
+ this.startedPrefetching = true;
+
+ setTimeout(() => {
+ const options: IConnectionsIteratorOptions = {
+ backward: false,
+ lowerBoundDate: new Date(),
+ };
+
+ this.connectionsFetcher.setIteratorOptions(options);
+ this.connectionsIterator = this.connectionsFetcher.createIterator();
+ this.connectionsStore.setSourceIterator(this.connectionsIterator);
+ this.connectionsStore.startPrimaryPush(ConnectionsProviderPrefetch.MAX_CONNECTIONS);
+ }, 0);
+ }
+ }
+
+ public createIterator(): AsyncIterator {
+ if (this.startedPrefetching) {
+ return this.connectionsStore
+ .getIterator(this.connectionsIteratorOptions);
+ }
+
+ throw new Error("TODO");
+ }
+
+ public setIteratorOptions(options: IConnectionsIteratorOptions): void {
+ this.connectionsIteratorOptions = options;
+ }
+
+}
diff --git a/src/fetcher/connections/prefetch/ConnectionsStore.test.ts b/src/fetcher/connections/prefetch/ConnectionsStore.test.ts
new file mode 100644
index 00000000..4348e148
--- /dev/null
+++ b/src/fetcher/connections/prefetch/ConnectionsStore.test.ts
@@ -0,0 +1,126 @@
+import { ArrayIterator, AsyncIterator } from "asynciterator";
+import "jest";
+import IConnection from "../IConnection";
+import IConnectionsIteratorOptions from "../IConnectionsIteratorOptions";
+import ConnectionsStore from "./ConnectionsStore";
+
+describe("[ConnectionsStore]", () => {
+
+ /**
+ * In this test, departureTime dates are substituted for numbers for simplicity
+ * Inside the ConnectionsStore, #valueOf() gets called on the connection.departureTime
+ * and both Dates and Numbers return a number
+ */
+
+ let connectionsStore;
+ let createIterator;
+
+ beforeEach(() => {
+ const fakeDepartureTimes = [1, 2, 3, 3, 5, 6, 6, 6, 6, 7, 7];
+ // @ts-ignore
+ const fakeConnections: IConnection[] = fakeDepartureTimes
+ .map((departureTime) => ({ departureTime }));
+
+ const fakeSourceIterator = new ArrayIterator(fakeConnections);
+
+ connectionsStore = new ConnectionsStore();
+
+ connectionsStore.setSourceIterator(fakeSourceIterator);
+ connectionsStore.startPrimaryPush(500);
+
+ createIterator = (backward, lowerBoundDate, upperBoundDate): Promise> => {
+ return new Promise((resolve) => {
+ // Primary push start async, so get iterator async
+ // Running a query is most often initiated by a user event, while prefetching start automatically
+ setTimeout(() => {
+ const iteratorOptions: IConnectionsIteratorOptions = {
+ backward,
+ };
+
+ if (lowerBoundDate) {
+ iteratorOptions.lowerBoundDate = (lowerBoundDate as unknown) as Date;
+ }
+
+ if (upperBoundDate) {
+ iteratorOptions.upperBoundDate = (upperBoundDate as unknown) as Date;
+ }
+
+ resolve(connectionsStore.getIterator(iteratorOptions));
+ }, 100);
+ });
+ };
+ });
+
+ describe("backward", () => {
+
+ it("upperBoundDate is loaded & exists in store", async (done) => {
+ const iteratorView = await createIterator(true, null, 6);
+
+ const expected = [1, 2, 3, 3, 5, 6, 6, 6, 6];
+ let current = expected.length - 1;
+
+ iteratorView.each((str: IConnection) => {
+ expect(expected[current--]).toBe(str.departureTime);
+ });
+
+ iteratorView.on("end", () => {
+ expect(current).toBe(-1);
+ done();
+ });
+ });
+
+ it("upperBoundDate is loaded but doesn\'t exist in store", async (done) => {
+ const iteratorView = await createIterator(true, null, 4);
+
+ const expected = [1, 2, 3, 3];
+ let current = expected.length - 1;
+
+ iteratorView.each((str: IConnection) => {
+ expect(expected[current--]).toBe(str.departureTime);
+ });
+
+ iteratorView.on("end", () => {
+ expect(current).toBe(-1);
+ done();
+ });
+ });
+
+ });
+
+ describe("forward", () => {
+
+ it("lowerBoundDate is loaded & exists in store", async (done) => {
+ const iteratorView = await createIterator(false, 3, 6);
+
+ const expected = [3, 3, 5, 6, 6, 6, 6];
+ let current = 0;
+
+ iteratorView.each((str: IConnection) => {
+ expect(expected[current++]).toBe(str.departureTime);
+ });
+
+ iteratorView.on("end", () => {
+ expect(current).toBe(expected.length);
+ done();
+ });
+ });
+
+ it("lowerBoundDate is loaded but doesn\'t exist in store", async (done) => {
+ const iteratorView = await createIterator(false, 4, 6);
+
+ const expected = [5, 6, 6, 6, 6];
+ let current = 0;
+
+ iteratorView.each((str: IConnection) => {
+ expect(expected[current++]).toBe(str.departureTime);
+ });
+
+ iteratorView.on("end", () => {
+ expect(current).toBe(expected.length);
+ done();
+ });
+ });
+
+ });
+
+});
diff --git a/src/fetcher/connections/prefetch/ConnectionsStore.ts b/src/fetcher/connections/prefetch/ConnectionsStore.ts
new file mode 100644
index 00000000..33612d79
--- /dev/null
+++ b/src/fetcher/connections/prefetch/ConnectionsStore.ts
@@ -0,0 +1,355 @@
+import { AsyncIterator, EmptyIterator } from "asynciterator";
+import { PromiseProxyIterator } from "asynciterator-promiseproxy";
+import Context from "../../../Context";
+import EventType from "../../../enums/EventType";
+import BinarySearch from "../../../util/BinarySearch";
+import ExpandingIterator from "../../../util/iterators/ExpandingIterator";
+import Units from "../../../util/Units";
+import IConnection from "../IConnection";
+import IConnectionsIteratorOptions from "../IConnectionsIteratorOptions";
+import ArrayViewIterator from "./ArrayViewIterator";
+import IDeferredBackwardView from "./IDeferredBackwardView";
+import IExpandingForwardView from "./IExpandingForwardView";
+
+/**
+ * Class used while prefetching [[IConnection]] instances. It allows appending connections
+ * and creating iterator *views*. Iterator *views* are AsyncIterators that emit references to connections in the store.
+ *
+ * It is assumed that all connections are appended in ascending order by `departureTime`.
+ *
+ * Consequently this connections store serves as an in-memory cache for connections
+ */
+export default class ConnectionsStore {
+
+ private static REPORTING_THRESHOLD = Units.fromHours(.25);
+
+ private readonly context: Context;
+ private readonly store: IConnection[];
+ private readonly binarySearch: BinarySearch;
+
+ private sourceIterator: AsyncIterator;
+ private deferredBackwardViews: IDeferredBackwardView[];
+ private expandingForwardViews: IExpandingForwardView[];
+
+ private hasFinishedPrimary: boolean;
+ private isContinuing: boolean;
+ private lastReportedDepartureTime: Date;
+
+ constructor(context?: Context) {
+ this.context = context;
+ this.store = [];
+ this.binarySearch = new BinarySearch(this.store, (connection) => connection.departureTime.valueOf());
+ this.deferredBackwardViews = [];
+ this.expandingForwardViews = [];
+ this.hasFinishedPrimary = false;
+ }
+
+ public setSourceIterator(iterator: AsyncIterator): void {
+ this.sourceIterator = iterator;
+ }
+
+ public startPrimaryPush(maxConnections: number): void {
+
+ this.sourceIterator
+ .transform({
+ limit: maxConnections,
+ destroySource: false,
+ })
+ .on("end", () => this.finishPrimaryPush())
+ .each((connection: IConnection) => {
+ if (this.context) {
+ this.maybeEmitPrefetchEvent(connection);
+ }
+
+ this.append(connection);
+ });
+ }
+
+ public getIterator(iteratorOptions: IConnectionsIteratorOptions): AsyncIterator {
+ const { backward } = iteratorOptions;
+ let { lowerBoundDate, upperBoundDate } = iteratorOptions;
+
+ if (this.hasFinishedPrimary && this.store.length === 0) {
+ return new EmptyIterator();
+ }
+
+ const firstConnection = this.store[0];
+ const firstDepartureTime = firstConnection && firstConnection.departureTime;
+
+ const lastConnection = this.store[this.store.length - 1];
+ const lastDepartureTime = lastConnection && lastConnection.departureTime;
+
+ if (lowerBoundDate && lowerBoundDate < firstDepartureTime) {
+ throw new Error("Must supply a lowerBoundDate after the first prefetched connection");
+ }
+
+ if (backward) {
+
+ if (!upperBoundDate) {
+ throw new Error("Must supply upperBoundDate when iterating backward");
+ }
+
+ if (!lowerBoundDate) {
+ lowerBoundDate = firstDepartureTime;
+ }
+
+ this.emitConnectionViewEvent(lowerBoundDate, upperBoundDate, false);
+
+ // If the store is still empty or the latest departure time isn't later than the upperBoundDate,
+ // then return a promise proxy iterator
+ const notFinishedScenario = !this.hasFinishedPrimary
+ && (!lastDepartureTime || lastDepartureTime <= upperBoundDate);
+
+ const finishedScenario = this.hasFinishedPrimary
+ && lastDepartureTime < upperBoundDate;
+
+ if (notFinishedScenario || finishedScenario) {
+ const { deferred, promise } = this.createDeferredBackwardView(lowerBoundDate, upperBoundDate);
+
+ this.deferredBackwardViews.push(deferred);
+
+ if (this.hasFinishedPrimary) {
+ this.continueAfterFinishing();
+ }
+
+ return new PromiseProxyIterator(() => promise);
+ }
+
+ } else {
+
+ if (!lowerBoundDate) {
+ throw new Error("Must supply lowerBoundDate when iterating forward");
+ }
+
+ if (!upperBoundDate) {
+ // Mock +infinity
+ upperBoundDate = new Date(lowerBoundDate.valueOf() + Units.fromHours(24));
+ }
+
+ this.emitConnectionViewEvent(lowerBoundDate, upperBoundDate, false);
+
+ // If the store is still empty or the latest departure time isn't later than the upperBoundDate,
+ // then return a an expanding iterator view
+ const notFinishedScenario = !this.hasFinishedPrimary
+ && (!lastDepartureTime || lastDepartureTime <= upperBoundDate);
+
+ const finishedScenario = this.hasFinishedPrimary
+ && lastDepartureTime < upperBoundDate;
+
+ if (notFinishedScenario || finishedScenario) {
+ const { view, iterator } = this.createExpandingForwardView(lowerBoundDate, upperBoundDate);
+
+ this.expandingForwardViews.push(view);
+
+ if (this.hasFinishedPrimary) {
+ this.continueAfterFinishing();
+ }
+
+ return iterator;
+ }
+ }
+
+ // If the whole interval fits inside the prefetched window, return an iterator view
+ if (lowerBoundDate >= firstDepartureTime && upperBoundDate < lastDepartureTime) {
+ const { iterator } = this.getIteratorView(backward, lowerBoundDate, upperBoundDate);
+
+ this.emitConnectionViewEvent(lowerBoundDate, upperBoundDate, true);
+
+ return iterator;
+ }
+
+ throw new Error("This shouldn\'t happen");
+ }
+
+ /**
+ * Add a new [[IConnection]] to the store.
+ *
+ * Additionally, this method checks if any forward iterator views can be expanded or if any backward iterator can be
+ * resolved
+ *
+ * @returns the number of unsatisfied views
+ */
+ private append(connection: IConnection): number {
+ this.store.push(connection);
+
+ // Check if any deferred backward views are satisfied
+ if (this.deferredBackwardViews.length) {
+ this.deferredBackwardViews = this.deferredBackwardViews
+ .filter(({ lowerBoundDate, upperBoundDate, resolve }) => {
+
+ if (connection.departureTime > upperBoundDate) {
+ const { iterator } = this.getIteratorView(true, lowerBoundDate, upperBoundDate);
+
+ this.emitConnectionViewEvent(lowerBoundDate, upperBoundDate, true);
+
+ resolve(iterator);
+ return false;
+ }
+
+ return true;
+ });
+ }
+
+ // Check if any forward views can be expanded
+ if (this.expandingForwardViews.length) {
+ this.expandingForwardViews = this.expandingForwardViews
+ .filter(({ tryExpand }) => tryExpand(connection, this.store.length - 1));
+ }
+
+ return this.deferredBackwardViews.length + this.expandingForwardViews.length;
+ }
+
+ /**
+ * Signals that the store will no longer be appended.
+ * [[getIterator]] never returns a deferred backward view after this, because those would never get resolved
+ */
+ private finishPrimaryPush(): void {
+ this.hasFinishedPrimary = true;
+
+ if (this.deferredBackwardViews.length || this.expandingForwardViews.length) {
+ this.continueAfterFinishing();
+ }
+ }
+
+ private finishSecondaryPush(): void {
+ this.isContinuing = false;
+ }
+
+ private continueAfterFinishing(): void {
+ if (!this.isContinuing) {
+ this.isContinuing = true;
+
+ setTimeout(() => this.startSecondaryPush(), 0);
+ }
+ }
+
+ private startSecondaryPush(): void {
+ const secondaryPushIterator = this.sourceIterator
+ .transform({})
+ .on("end", () => this.finishSecondaryPush());
+
+ secondaryPushIterator.each((connection: IConnection) => {
+ if (this.context) {
+ this.maybeEmitPrefetchEvent(connection);
+ }
+
+ const unsatisfiedViewCount = this.append(connection);
+
+ if (unsatisfiedViewCount === 0) {
+ secondaryPushIterator.close();
+ }
+ });
+ }
+
+ private createDeferredBackwardView(lowerBoundDate, upperBoundDate):
+ { deferred: IDeferredBackwardView, promise: Promise> } {
+
+ const deferred: Partial = {
+ lowerBoundDate,
+ upperBoundDate,
+ };
+
+ const promise = new Promise>((resolve) => {
+ deferred.resolve = resolve;
+ });
+
+ return {
+ deferred: deferred as IDeferredBackwardView,
+ promise,
+ };
+ }
+
+ private createExpandingForwardView(lowerBoundDate, upperBoundDate):
+ { view: IExpandingForwardView, iterator: AsyncIterator } {
+
+ const { iterator: existingIterator, upperBoundIndex } = this.getIteratorView(false, lowerBoundDate, upperBoundDate);
+ const expandingIterator = new ExpandingIterator();
+
+ const iterator = expandingIterator.prepend(existingIterator);
+
+ let lastStoreIndex = upperBoundIndex;
+
+ const view: IExpandingForwardView = {
+ lowerBoundDate,
+ upperBoundDate,
+ tryExpand: (connection: IConnection, storeIndex: number): boolean => {
+
+ if (storeIndex - lastStoreIndex > 1) {
+ // No idea if this can happen
+ console.warn("Skipped", storeIndex - lastStoreIndex);
+ }
+
+ lastStoreIndex = storeIndex;
+
+ // No need to keep trying to expand if the consumer has closed it
+ if (iterator.closed) {
+ expandingIterator.close();
+
+ return false; // Remove from expanding forward views
+ }
+
+ if (connection.departureTime <= upperBoundDate) {
+ expandingIterator.write(connection);
+
+ return true; // Keep in expanding forward views
+
+ } else {
+ expandingIterator.closeAfterFlush();
+ // iterator.close();
+
+ this.emitConnectionViewEvent(lowerBoundDate, upperBoundDate, true);
+
+ return false; // Remove from expanding forward views
+ }
+ },
+ };
+
+ return { view, iterator };
+ }
+
+ private getIteratorView(backward: boolean, lowerBoundDate: Date, upperBoundDate: Date):
+ { iterator: AsyncIterator, lowerBoundIndex: number, upperBoundIndex: number } {
+
+ const lowerBoundIndex = this.getLowerBoundIndex(lowerBoundDate);
+ const upperBoundIndex = this.getUpperBoundIndex(upperBoundDate);
+
+ const start = backward ? upperBoundIndex : lowerBoundIndex;
+ const stop = backward ? lowerBoundIndex : upperBoundIndex;
+ const step = backward ? -1 : 1;
+
+ const iterator = new ArrayViewIterator(this.store, start, stop, step);
+
+ return { iterator, lowerBoundIndex, upperBoundIndex };
+ }
+
+ private getLowerBoundIndex(date: Date): number {
+ return this.binarySearch.findFirstIndex(date.valueOf(), 0, this.store.length - 1);
+ }
+
+ private getUpperBoundIndex(date: Date): number {
+ return this.binarySearch.findLastIndex(date.valueOf(), 0, this.store.length - 1);
+ }
+
+ private emitConnectionViewEvent(lowerBoundDate: Date, upperBoundDate: Date, completed: boolean) {
+ if (this.context) {
+ this.context.emit(EventType.ConnectionIteratorView, lowerBoundDate, upperBoundDate, completed);
+ }
+ }
+
+ private maybeEmitPrefetchEvent(connection: IConnection): void {
+ if (!this.lastReportedDepartureTime) {
+ this.lastReportedDepartureTime = connection.departureTime;
+
+ this.context.emit(EventType.ConnectionPrefetch, this.lastReportedDepartureTime);
+ return;
+ }
+
+ const timeSinceLastEvent = connection.departureTime.valueOf() - this.lastReportedDepartureTime.valueOf();
+
+ if (timeSinceLastEvent > ConnectionsStore.REPORTING_THRESHOLD) {
+ this.lastReportedDepartureTime = connection.departureTime;
+
+ this.context.emit(EventType.ConnectionPrefetch, this.lastReportedDepartureTime);
+ }
+ }
+}
diff --git a/src/fetcher/connections/prefetch/IDeferredBackwardView.ts b/src/fetcher/connections/prefetch/IDeferredBackwardView.ts
new file mode 100644
index 00000000..781878ea
--- /dev/null
+++ b/src/fetcher/connections/prefetch/IDeferredBackwardView.ts
@@ -0,0 +1,8 @@
+import { AsyncIterator } from "asynciterator";
+import IConnection from "../IConnection";
+
+export default interface IDeferredBackwardView {
+ lowerBoundDate: Date;
+ upperBoundDate: Date;
+ resolve: (iterator: AsyncIterator) => void;
+}
diff --git a/src/fetcher/connections/prefetch/IExpandingForwardView.ts b/src/fetcher/connections/prefetch/IExpandingForwardView.ts
new file mode 100644
index 00000000..7e376e1a
--- /dev/null
+++ b/src/fetcher/connections/prefetch/IExpandingForwardView.ts
@@ -0,0 +1,7 @@
+import IConnection from "../IConnection";
+
+export default interface IExpandingForwardView {
+ lowerBoundDate: Date;
+ upperBoundDate: Date;
+ tryExpand: (connection: IConnection, index: number) => boolean;
+}
diff --git a/src/fetcher/connections/tests/ConnectionsFetcherNMBSTest.ts b/src/fetcher/connections/tests/ConnectionsFetcherNMBSTest.ts
index f512dc0f..5be1c39e 100644
--- a/src/fetcher/connections/tests/ConnectionsFetcherNMBSTest.ts
+++ b/src/fetcher/connections/tests/ConnectionsFetcherNMBSTest.ts
@@ -2,27 +2,31 @@ import { ArrayIterator, AsyncIterator } from "asynciterator";
import { injectable } from "inversify";
import IConnection from "../IConnection";
import IConnectionsFetcher from "../IConnectionsFetcher";
-import IConnectionsFetcherConfig from "../IConnectionsFetcherConfig";
+import IConnectionsIteratorOptions from "../IConnectionsIteratorOptions";
@injectable()
export default class ConnectionsFetcherNMBSTest implements IConnectionsFetcher {
private connections: Array> = [];
- private config: IConnectionsFetcherConfig = {};
+ private options: IConnectionsIteratorOptions = {};
constructor(connections: Array>) {
this.connections = connections;
}
- public setConfig(config: IConnectionsFetcherConfig): void {
- this.config = config;
+ public prefetchConnections(): void {
+ return;
+ }
+
+ public setIteratorOptions(options: IConnectionsIteratorOptions): void {
+ this.options = options;
}
public createIterator(): AsyncIterator {
let array = this.connections
.map((r) => r.value);
- if (this.config.backward) {
+ if (this.options.backward) {
array = array.reverse();
}
diff --git a/src/fetcher/connections/tests/data/ingelmunster-ghent.ts b/src/fetcher/connections/tests/data/ingelmunster-ghent.ts
index 23ff7413..d4c5f019 100644
--- a/src/fetcher/connections/tests/data/ingelmunster-ghent.ts
+++ b/src/fetcher/connections/tests/data/ingelmunster-ghent.ts
@@ -1,4 +1,4 @@
-import TravelMode from "../../../../TravelMode";
+import TravelMode from "../../../../enums/TravelMode";
const connections = [
{
diff --git a/src/fetcher/connections/tests/data/joining.ts b/src/fetcher/connections/tests/data/joining.ts
index 99a31faa..f237967b 100644
--- a/src/fetcher/connections/tests/data/joining.ts
+++ b/src/fetcher/connections/tests/data/joining.ts
@@ -1,6 +1,6 @@
-import TravelMode from "../../../../TravelMode";
-import DropOffType from "../../DropOffType";
-import PickupType from "../../PickupType";
+import DropOffType from "../../../../enums/DropOffType";
+import PickupType from "../../../../enums/PickupType";
+import TravelMode from "../../../../enums/TravelMode";
const connections = [
{
diff --git a/src/fetcher/connections/tests/data/splitting.ts b/src/fetcher/connections/tests/data/splitting.ts
index 56fae16a..d625329d 100644
--- a/src/fetcher/connections/tests/data/splitting.ts
+++ b/src/fetcher/connections/tests/data/splitting.ts
@@ -1,6 +1,6 @@
-import TravelMode from "../../../../TravelMode";
-import DropOffType from "../../DropOffType";
-import PickupType from "../../PickupType";
+import DropOffType from "../../../../enums/DropOffType";
+import PickupType from "../../../../enums/PickupType";
+import TravelMode from "../../../../enums/TravelMode";
const connections = [
{
diff --git a/src/fetcher/stops/IStopsProvider.ts b/src/fetcher/stops/IStopsProvider.ts
index 95ef130f..6f467568 100644
--- a/src/fetcher/stops/IStopsProvider.ts
+++ b/src/fetcher/stops/IStopsProvider.ts
@@ -7,6 +7,7 @@ import IStop from "./IStop";
* @method getAllStops Returns concatenated array of [[IStop]]s from all [[IStopsFetcher]]s it mediates
*/
export default interface IStopsProvider {
+ prefetchStops: () => void;
getStopById: (stopId: string) => Promise;
getAllStops: () => Promise;
}
diff --git a/src/fetcher/stops/StopsProviderDefault.ts b/src/fetcher/stops/StopsProviderDefault.ts
index ffe9b65a..a7fa1cea 100644
--- a/src/fetcher/stops/StopsProviderDefault.ts
+++ b/src/fetcher/stops/StopsProviderDefault.ts
@@ -9,18 +9,26 @@ import IStopsProvider from "./IStopsProvider";
export default class StopsProviderDefault implements IStopsProvider {
private readonly stopsFetchers: IStopsFetcher[];
+ private cachedStops: IStop[];
constructor(
@inject(TYPES.StopsFetcherFactory) stopsFetcherFactory: StopsFetcherFactory,
@inject(TYPES.Catalog) catalog: Catalog,
) {
this.stopsFetchers = [];
+ this.cachedStops = [];
- for (const { accessUrl } of catalog.stopsFetcherConfigs) {
+ for (const { accessUrl } of catalog.stopsSourceConfigs) {
this.stopsFetchers.push(stopsFetcherFactory(accessUrl));
}
}
+ public prefetchStops(): void {
+ for (const stopsFetcher of this.stopsFetchers) {
+ stopsFetcher.prefetchStops();
+ }
+ }
+
public async getStopById(stopId: string): Promise {
return Promise.all(this.stopsFetchers
.map((stopsFetcher: IStopsFetcher) => stopsFetcher.getStopById(stopId)),
@@ -28,8 +36,16 @@ export default class StopsProviderDefault implements IStopsProvider {
}
public async getAllStops(): Promise {
+ if (this.cachedStops.length > 0) {
+ return Promise.resolve(this.cachedStops);
+ }
+
return Promise.all(this.stopsFetchers
.map((stopsFetcher: IStopsFetcher) => stopsFetcher.getAllStops()),
- ).then((results: IStop[][]) => [].concat(...results));
+ ).then((results: IStop[][]) => {
+ this.cachedStops = [].concat(...results);
+
+ return this.cachedStops;
+ });
}
}
diff --git a/src/fetcher/stops/ld-fetch/StopsFetcherLDFetch.ts b/src/fetcher/stops/ld-fetch/StopsFetcherLDFetch.ts
index d7ef11b9..8812fc53 100644
--- a/src/fetcher/stops/ld-fetch/StopsFetcherLDFetch.ts
+++ b/src/fetcher/stops/ld-fetch/StopsFetcherLDFetch.ts
@@ -34,6 +34,10 @@ export default class StopsFetcherLDFetch implements IStopsFetcher {
this.accessUrl = accessUrl;
}
+ public prefetchStops(): void {
+ this.ensureStopsLoaded();
+ }
+
public async getStopById(stopId: string): Promise {
await this.ensureStopsLoaded();
diff --git a/src/interfaces/IQuery.ts b/src/interfaces/IQuery.ts
index 1142c149..78bbbc7e 100644
--- a/src/interfaces/IQuery.ts
+++ b/src/interfaces/IQuery.ts
@@ -1,5 +1,5 @@
import ILocation from "./ILocation";
-import { DistanceM, DurationMs, SpeedkmH } from "./units";
+import { DistanceM, DurationMs, SpeedKmH } from "./units";
export default interface IQuery {
from?: string | string[] | ILocation | ILocation[];
@@ -8,12 +8,13 @@ export default interface IQuery {
maximumArrivalTime?: Date;
roadOnly?: boolean;
publicTransportOnly?: boolean;
- walkingSpeed?: SpeedkmH;
- minimumWalkingSpeed?: SpeedkmH;
- maximumWalkingSpeed?: SpeedkmH;
+ walkingSpeed?: SpeedKmH;
+ minimumWalkingSpeed?: SpeedKmH;
+ maximumWalkingSpeed?: SpeedKmH;
maximumWalkingDuration?: DurationMs;
maximumWalkingDistance?: DistanceM;
minimumTransferDuration?: DurationMs;
maximumTransferDuration?: DurationMs;
+ maximumTransferDistance?: DistanceM;
maximumTransfers?: number;
}
diff --git a/src/interfaces/IStep.ts b/src/interfaces/IStep.ts
index bf475b41..db6bbaa8 100644
--- a/src/interfaces/IStep.ts
+++ b/src/interfaces/IStep.ts
@@ -1,4 +1,4 @@
-import TravelMode from "../TravelMode";
+import TravelMode from "../enums/TravelMode";
import ILocation from "./ILocation";
import IProbabilisticValue from "./IProbabilisticValue";
import { DistanceM, DurationMs } from "./units";
diff --git a/src/interfaces/units.ts b/src/interfaces/units.ts
index 23f52d27..9cb37ed0 100644
--- a/src/interfaces/units.ts
+++ b/src/interfaces/units.ts
@@ -1,6 +1,14 @@
-// duration ms
+/**
+ * Represents duration in ms (milliseconds)
+ */
export type DurationMs = number;
-// distance in m
+
+/**
+ * Represents distance in m (meters)
+ */
export type DistanceM = number;
-// speed in km/h
-export type SpeedkmH = number;
+
+/**
+ * Represents duration in km/h (kilometers per hour)
+ */
+export type SpeedKmH = number;
diff --git a/src/inversify.config.ts b/src/inversify.config.ts
index 7209beb3..a5ec3b50 100644
--- a/src/inversify.config.ts
+++ b/src/inversify.config.ts
@@ -3,59 +3,57 @@ import Catalog from "./Catalog";
import catalogDeLijn from "./catalog.delijn";
import catalogNmbs from "./catalog.nmbs";
import Context from "./Context";
+import ReachableStopsSearchPhase from "./enums/ReachableStopsSearchPhase";
+import TravelMode from "./enums/TravelMode";
+import ConnectionsProviderMerge from "./fetcher/connections/ConnectionsProviderMerge";
import IConnectionsFetcher from "./fetcher/connections/IConnectionsFetcher";
import IConnectionsProvider from "./fetcher/connections/IConnectionsProvider";
-import ConnectionsFetcherLazy from "./fetcher/connections/ld-fetch/ConnectionsFetcherLazy";
-import ConnectionsProviderMerge from "./fetcher/connections/merge/ConnectionsProviderMerge";
+import ConnectionsFetcherLazy from "./fetcher/connections/lazy/ConnectionsFetcherLazy";
+import ConnectionsProviderPrefetch from "./fetcher/connections/prefetch/ConnectionsProviderPrefetch";
import LDFetch from "./fetcher/LDFetch";
import IStopsFetcher from "./fetcher/stops/IStopsFetcher";
import IStopsProvider from "./fetcher/stops/IStopsProvider";
import StopsFetcherLDFetch from "./fetcher/stops/ld-fetch/StopsFetcherLDFetch";
import StopsProviderDefault from "./fetcher/stops/StopsProviderDefault";
+import CSAProfile from "./planner/public-transport/CSAProfile";
import IJourneyExtractor from "./planner/public-transport/IJourneyExtractor";
import IPublicTransportPlanner from "./planner/public-transport/IPublicTransportPlanner";
-import JourneyExtractionPhase from "./planner/public-transport/JourneyExtractionPhase";
-import JourneyExtractorDefault from "./planner/public-transport/JourneyExtractorDefault";
-import PublicTransportPlannerCSAProfile from "./planner/public-transport/PublicTransportPlannerCSAProfile";
+import JourneyExtractorProfile from "./planner/public-transport/JourneyExtractorProfile";
import IRoadPlanner from "./planner/road/IRoadPlanner";
import RoadPlannerBirdsEye from "./planner/road/RoadPlannerBirdsEye";
import IReachableStopsFinder from "./planner/stops/IReachableStopsFinder";
-import ReachableStopsFinderBirdsEyeCached from "./planner/stops/ReachableStopsFinderBirdsEyeCached";
-import ReachableStopsSearchPhase from "./planner/stops/ReachableStopsSearchPhase";
+import ReachableStopsFinderOnlySelf from "./planner/stops/ReachableStopsFinderOnlySelf";
+import ReachableStopsFinderRoadPlannerCached from "./planner/stops/ReachableStopsFinderRoadPlannerCached";
import QueryRunnerExponential from "./query-runner/exponential/QueryRunnerExponential";
import ILocationResolver from "./query-runner/ILocationResolver";
import IQueryRunner from "./query-runner/IQueryRunner";
-import LocationResolverDefault from "./query-runner/LocationResolverDefault";
-import TravelMode from "./TravelMode";
+import LocationResolverConvenience from "./query-runner/LocationResolverConvenience";
import TYPES from "./types";
const container = new Container();
container.bind(TYPES.Context).to(Context).inSingletonScope();
container.bind(TYPES.QueryRunner).to(QueryRunnerExponential);
-container.bind(TYPES.LocationResolver).to(LocationResolverDefault);
+container.bind(TYPES.LocationResolver).to(LocationResolverConvenience);
container.bind(TYPES.PublicTransportPlanner)
- .to(PublicTransportPlannerCSAProfile);
+ .to(CSAProfile);
container.bind>(TYPES.PublicTransportPlannerFactory)
.toAutoFactory(TYPES.PublicTransportPlanner);
-container.bind(TYPES.JourneyExtractor)
- .to(JourneyExtractorDefault);
-container.bind(TYPES.RoadPlanner)
- .to(RoadPlannerBirdsEye).whenTargetTagged("phase", JourneyExtractionPhase.Initial);
-container.bind(TYPES.RoadPlanner)
- .to(RoadPlannerBirdsEye).whenTargetTagged("phase", JourneyExtractionPhase.Transfer);
container.bind(TYPES.RoadPlanner)
- .to(RoadPlannerBirdsEye).whenTargetTagged("phase", JourneyExtractionPhase.Final);
+ .to(RoadPlannerBirdsEye);
+
+container.bind(TYPES.JourneyExtractor)
+ .to(JourneyExtractorProfile);
container.bind(TYPES.ReachableStopsFinder)
- .to(ReachableStopsFinderBirdsEyeCached).whenTargetTagged("phase", ReachableStopsSearchPhase.Initial);
+ .to(ReachableStopsFinderRoadPlannerCached).whenTargetTagged("phase", ReachableStopsSearchPhase.Initial);
container.bind(TYPES.ReachableStopsFinder)
- .to(ReachableStopsFinderBirdsEyeCached).whenTargetTagged("phase", ReachableStopsSearchPhase.Transfer);
+ .to(ReachableStopsFinderOnlySelf).whenTargetTagged("phase", ReachableStopsSearchPhase.Transfer);
container.bind(TYPES.ReachableStopsFinder)
- .to(ReachableStopsFinderBirdsEyeCached).whenTargetTagged("phase", ReachableStopsSearchPhase.Final);
+ .to(ReachableStopsFinderRoadPlannerCached).whenTargetTagged("phase", ReachableStopsSearchPhase.Final);
-container.bind(TYPES.ConnectionsProvider).to(ConnectionsProviderMerge).inSingletonScope();
+container.bind(TYPES.ConnectionsProvider).to(ConnectionsProviderPrefetch).inSingletonScope();
container.bind(TYPES.ConnectionsFetcher).to(ConnectionsFetcherLazy);
container.bind>(TYPES.ConnectionsFetcherFactory)
.toFactory(
diff --git a/src/planner/IPlanner.ts b/src/planner/IPlanner.ts
index 28a85a84..d670860f 100644
--- a/src/planner/IPlanner.ts
+++ b/src/planner/IPlanner.ts
@@ -2,6 +2,10 @@ import { AsyncIterator } from "asynciterator";
import IPath from "../interfaces/IPath";
import IResolvedQuery from "../query-runner/IResolvedQuery";
+/**
+ * This interface functions as base interface for both the [[IPublicTransportPlanner]]
+ * and the [[IRoadPlanner]] interface
+ */
export default interface IPlanner {
plan: (query: IResolvedQuery) => Promise>;
}
diff --git a/src/planner/Path.ts b/src/planner/Path.ts
index ac79d24c..3174b223 100644
--- a/src/planner/Path.ts
+++ b/src/planner/Path.ts
@@ -1,7 +1,12 @@
import IPath from "../interfaces/IPath";
import IStep from "../interfaces/IStep";
+import { DurationMs } from "../interfaces/units";
import Step from "./Step";
+/**
+ * This Path class serves as an implementation of the [[IPath]] interface and as a home for some helper functions
+ * related to [[IPath]] instances
+ */
export default class Path implements IPath {
public static create(): Path {
@@ -10,6 +15,22 @@ export default class Path implements IPath {
);
}
+ /**
+ * Compare two [[IPath]] instances
+ * @returns true if the two paths are the same
+ */
+ public static compareEquals(path: IPath, otherPath: IPath): boolean {
+ if (path.steps.length !== otherPath.steps.length) {
+ return false;
+ }
+
+ return path.steps.every((step, stepIndex) => {
+ const otherStep = otherPath.steps[stepIndex];
+
+ return Step.compareEquals(step, otherStep);
+ });
+ }
+
public steps: IStep[];
constructor(steps: IStep[]) {
@@ -20,16 +41,23 @@ export default class Path implements IPath {
this.steps.push(step);
}
- public equals(path: IPath): boolean {
+ public addPath(path: IPath): void {
+ this.steps.push(...path.steps);
+ }
- if (this.steps.length !== path.steps.length) {
- return false;
- }
+ public reverse(): void {
+ this.steps.reverse();
+ }
- return this.steps.every((step, stepIndex) => {
- const otherStep = path.steps[stepIndex];
+ public getStartLocationId(): string {
+ return (" " + this.steps[0].startLocation.id).slice(1);
+ }
- return Step.compareEquals(step, otherStep);
- });
+ public addTime(duration: DurationMs): void {
+ this.steps = this.steps.map((step: IStep) => ({
+ ...step,
+ startTime: new Date(step.startTime.getTime() + duration),
+ stopTime: new Date(step.stopTime.getTime() + duration),
+ }));
}
}
diff --git a/src/planner/PlannerPhase.ts b/src/planner/PlannerPhase.ts
deleted file mode 100644
index f4b23ef5..00000000
--- a/src/planner/PlannerPhase.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import JourneyExtractionPhase from "./public-transport/JourneyExtractionPhase";
-import ReachableStopsSearchPhase from "./stops/ReachableStopsSearchPhase";
-
-type PlannerPhase = JourneyExtractionPhase | ReachableStopsSearchPhase;
-
-export default PlannerPhase;
diff --git a/src/planner/Step.ts b/src/planner/Step.ts
index d9a9b770..bfd7af1b 100644
--- a/src/planner/Step.ts
+++ b/src/planner/Step.ts
@@ -1,10 +1,14 @@
+import TravelMode from "../enums/TravelMode";
import IConnection from "../fetcher/connections/IConnection";
import ILocation from "../interfaces/ILocation";
import IProbabilisticValue from "../interfaces/IProbabilisticValue";
import IStep from "../interfaces/IStep";
import { DistanceM, DurationMs } from "../interfaces/units";
-import TravelMode from "../TravelMode";
+/**
+ * This Step class serves as an implementation of the [[IStep]] interface and as a home for some helper functions
+ * related to [[IStep]] instances
+ */
export default class Step implements IStep {
public static create(
@@ -46,6 +50,10 @@ export default class Step implements IStep {
);
}
+ /**
+ * Compare two [[IStep]] instances
+ * @returns true if the two steps are the same
+ */
public static compareEquals(step: IStep, otherStep: IStep): boolean {
if (otherStep.travelMode !== step.travelMode) {
return false;
diff --git a/src/planner/public-transport/CSA/data-structure/stops/IProfileByStop.ts b/src/planner/public-transport/CSA/data-structure/stops/IProfileByStop.ts
new file mode 100644
index 00000000..75da4cf8
--- /dev/null
+++ b/src/planner/public-transport/CSA/data-structure/stops/IProfileByStop.ts
@@ -0,0 +1,8 @@
+import ITransferProfile from "./ITransferProfile";
+
+/**
+ * Stores multiple [[IProfile]]'s ordered by departure time for an [[IStop]].
+ */
+export default interface IProfileByStop {
+ [stop: string]: ITransferProfile;
+}
diff --git a/src/planner/public-transport/CSA/data-structure/stops/ITransferProfile.ts b/src/planner/public-transport/CSA/data-structure/stops/ITransferProfile.ts
index 9d8b65f9..06b021cf 100644
--- a/src/planner/public-transport/CSA/data-structure/stops/ITransferProfile.ts
+++ b/src/planner/public-transport/CSA/data-structure/stops/ITransferProfile.ts
@@ -1,4 +1,5 @@
import IConnection from "../../../../../fetcher/connections/IConnection";
+import Path from "../../../../Path";
/**
* Interface for the CSA profile for a specific amount of transfers that can be made.
@@ -11,6 +12,7 @@ import IConnection from "../../../../../fetcher/connections/IConnection";
export default interface ITransferProfile {
departureTime: number;
arrivalTime: number;
- exitConnection: IConnection;
- enterConnection: IConnection;
+ exitConnection?: IConnection;
+ enterConnection?: IConnection;
+ path?: Path;
}
diff --git a/src/planner/public-transport/CSA/data-structure/trips/IEarliestArrivalByTrip.ts b/src/planner/public-transport/CSA/data-structure/trips/IEarliestArrivalByTrip.ts
index ac2c4bf7..31cd4627 100644
--- a/src/planner/public-transport/CSA/data-structure/trips/IEarliestArrivalByTrip.ts
+++ b/src/planner/public-transport/CSA/data-structure/trips/IEarliestArrivalByTrip.ts
@@ -1,8 +1,8 @@
-import IEarliestArrivalByTransfers from "./IEarliestArrivalByTransfers";
-
/**
* Stores for each gtfs:trip the earliest arrival [[IEarliestArrivalByTransfers]] to the target [[IStop]].
*/
+import IEarliestArrivalByTransfers from "./IEarliestArrivalByTransfers";
+
export default interface IEarliestArrivalByTrip {
[trip: string]: IEarliestArrivalByTransfers;
}
diff --git a/src/planner/public-transport/CSA/data-structure/trips/IEnterConnectionByTrip.ts b/src/planner/public-transport/CSA/data-structure/trips/IEnterConnectionByTrip.ts
new file mode 100644
index 00000000..78a3c402
--- /dev/null
+++ b/src/planner/public-transport/CSA/data-structure/trips/IEnterConnectionByTrip.ts
@@ -0,0 +1,10 @@
+import IConnection from "../../../../../fetcher/connections/IConnection";
+
+/**
+ * @property arrivalTime Describes the earliest arrival time in milliseconds to the target [[IStop]].
+ * @property connection Describes the [[IConnection]] that should be taken to arrive
+ * at the arrivalTime in the target location.
+ */
+export default interface IEnterConnectionByTrip {
+ [trip: string]: IConnection;
+}
diff --git a/src/planner/public-transport/CSA/util/ProfileUtil.ts b/src/planner/public-transport/CSA/util/ProfileUtil.ts
index f8317df9..a8b38705 100644
--- a/src/planner/public-transport/CSA/util/ProfileUtil.ts
+++ b/src/planner/public-transport/CSA/util/ProfileUtil.ts
@@ -1,4 +1,4 @@
-import DropOffType from "../../../../fetcher/connections/DropOffType";
+import DropOffType from "../../../../enums/DropOffType";
import IConnection from "../../../../fetcher/connections/IConnection";
import { DurationMs } from "../../../../interfaces/units";
import IArrivalTimeByTransfers from "../data-structure/IArrivalTimeByTransfers";
@@ -17,6 +17,10 @@ export default class ProfileUtil {
result[stop] = profilesByStop[stop].filter((profile) =>
profile.departureTime !== Infinity,
);
+
+ if (result[stop].length === 0) {
+ delete result[stop];
+ }
}
}
return result;
diff --git a/src/planner/public-transport/CSAEarliestArrival.test.ts b/src/planner/public-transport/CSAEarliestArrival.test.ts
new file mode 100644
index 00000000..ac04174a
--- /dev/null
+++ b/src/planner/public-transport/CSAEarliestArrival.test.ts
@@ -0,0 +1,282 @@
+import "jest";
+import LDFetch from "ldfetch";
+import Defaults from "../../Defaults";
+import TravelMode from "../../enums/TravelMode";
+import ConnectionsFetcherLazy from "../../fetcher/connections/lazy/ConnectionsFetcherLazy";
+import ConnectionsFetcherNMBSTest from "../../fetcher/connections/tests/ConnectionsFetcherNMBSTest";
+import connectionsIngelmunsterGhent from "../../fetcher/connections/tests/data/ingelmunster-ghent";
+import connectionsJoining from "../../fetcher/connections/tests/data/joining";
+import connectionsSplitting from "../../fetcher/connections/tests/data/splitting";
+import StopsFetcherLDFetch from "../../fetcher/stops/ld-fetch/StopsFetcherLDFetch";
+import IPath from "../../interfaces/IPath";
+import IQuery from "../../interfaces/IQuery";
+import IStep from "../../interfaces/IStep";
+import IResolvedQuery from "../../query-runner/IResolvedQuery";
+import LocationResolverDefault from "../../query-runner/LocationResolverDefault";
+import QueryRunnerDefault from "../../query-runner/QueryRunnerDefault";
+import Iterators from "../../util/Iterators";
+import ReachableStopsFinderBirdsEyeCached from "../stops/ReachableStopsFinderBirdsEyeCached";
+import CSAEarliestArrival from "./CSAEarliestArrival";
+import JourneyExtractorEarliestArrival from "./JourneyExtractorEarliestArrival";
+
+describe("[PublicTransportPlannerCSAEarliestArrival]", () => {
+ describe("mock data", () => {
+ jest.setTimeout(100000);
+
+ const createCSA = (connections) => {
+ const ldFetch = new LDFetch({ headers: { Accept: "application/ld+json" } });
+
+ const connectionFetcher = new ConnectionsFetcherNMBSTest(connections);
+ connectionFetcher.setIteratorOptions({ backward: false });
+
+ const stopsFetcher = new StopsFetcherLDFetch(ldFetch);
+ stopsFetcher.setAccessUrl("https://irail.be/stations/NMBS");
+
+ const locationResolver = new LocationResolverDefault(stopsFetcher);
+ const reachableStopsFinder = new ReachableStopsFinderBirdsEyeCached(stopsFetcher);
+ const journeyExtractor = new JourneyExtractorEarliestArrival(
+ locationResolver,
+ );
+
+ return new CSAEarliestArrival(
+ connectionFetcher,
+ locationResolver,
+ reachableStopsFinder,
+ reachableStopsFinder,
+ reachableStopsFinder,
+ journeyExtractor,
+ );
+ };
+
+ describe("basic test", () => {
+ let result: IPath[];
+
+ const query: IResolvedQuery = {
+ publicTransportOnly: true,
+ from: [{latitude: 50.914326, longitude: 3.255415, id: "http://irail.be/stations/NMBS/008896925" }],
+ to: [{ latitude: 51.035896, longitude: 3.710875, id: "http://irail.be/stations/NMBS/008892007" }],
+ minimumDepartureTime: new Date("2018-11-06T09:00:00.000Z"),
+ maximumArrivalTime: new Date("2018-11-06T19:00:00.000Z"),
+ maximumTransfers: 8,
+ minimumWalkingSpeed: Defaults.defaultMinimumWalkingSpeed,
+ maximumWalkingSpeed: Defaults.defaultMaximumWalkingSpeed,
+ maximumTransferDuration: Defaults.defaultMaximumTransferDuration,
+ minimumTransferDuration: Defaults.defaultMinimumTransferDuration,
+ };
+
+ beforeAll(async () => {
+ const CSA = createCSA(connectionsIngelmunsterGhent);
+ const iterator = await CSA.plan(query);
+ result = await Iterators.toArray(iterator);
+ });
+
+ it("Correct departure and arrival stop", () => {
+ expect(result).toBeDefined();
+
+ for (const path of result) {
+ expect(path.steps).toBeDefined();
+ expect(path.steps[0]).toBeDefined();
+ expect(query.from.map((from) => from.id)).toContain(path.steps[0].startLocation.id);
+ expect(query.to.map((to) => to.id)).toContain(path.steps[path.steps.length - 1].stopLocation.id);
+ }
+ });
+ });
+
+ describe("splitting", () => {
+ let result: IPath[];
+
+ const query: IResolvedQuery = {
+ publicTransportOnly: true,
+ from: [{
+ id: "http://irail.be/stations/NMBS/008821006",
+ latitude: 51.2172,
+ longitude: 4.421101,
+ }],
+ to: [{
+ id: "http://irail.be/stations/NMBS/008812005",
+ latitude: 50.859663,
+ longitude: 4.360846,
+ }],
+ minimumDepartureTime: new Date("2017-12-19T15:50:00.000Z"),
+ maximumArrivalTime: new Date("2017-12-19T16:50:00.000Z"),
+ maximumTransfers: 1,
+ minimumWalkingSpeed: Defaults.defaultMinimumWalkingSpeed,
+ maximumWalkingSpeed: Defaults.defaultMaximumWalkingSpeed,
+ maximumTransferDuration: Defaults.defaultMaximumTransferDuration,
+ minimumTransferDuration: Defaults.defaultMinimumTransferDuration,
+ };
+
+ beforeAll(async () => {
+ const CSA = createCSA(connectionsSplitting);
+ const iterator = await CSA.plan(query);
+ result = await Iterators.toArray(iterator);
+ });
+
+ it("Correct departure and arrival stop", () => {
+ expect(result).toBeDefined();
+ expect(result.length).toBeGreaterThanOrEqual(1);
+
+ for (const path of result) {
+ expect(path.steps).toBeDefined();
+
+ expect(path.steps.length).toEqual(1);
+ expect(path.steps[0]).toBeDefined();
+
+ expect(query.from.map((from) => from.id)).toContain(path.steps[0].startLocation.id);
+ expect(query.to.map((to) => to.id)).toContain(path.steps[0].stopLocation.id);
+ }
+ });
+ });
+
+ describe("joining", () => {
+ let result: IPath[];
+
+ const query: IResolvedQuery = {
+ publicTransportOnly: true,
+ from: [{
+ id: "http://irail.be/stations/NMBS/008812005",
+ latitude: 50.859663,
+ longitude: 4.360846,
+ }],
+ to: [{
+ id: "http://irail.be/stations/NMBS/008821006",
+ latitude: 51.2172,
+ longitude: 4.421101,
+ }],
+ minimumDepartureTime: new Date("2017-12-19T16:20:00.000Z"),
+ maximumArrivalTime: new Date("2017-12-19T16:50:00.000Z"),
+ maximumTransfers: 1,
+ minimumWalkingSpeed: Defaults.defaultMinimumWalkingSpeed,
+ maximumWalkingSpeed: Defaults.defaultMaximumWalkingSpeed,
+ maximumTransferDuration: Defaults.defaultMaximumTransferDuration,
+ minimumTransferDuration: Defaults.defaultMinimumTransferDuration,
+ };
+
+ beforeAll(async () => {
+ const CSA = createCSA(connectionsJoining);
+ const iterator = await CSA.plan(query);
+ result = await Iterators.toArray(iterator);
+ });
+
+ it("Correct departure and arrival stop", () => {
+ expect(result).toBeDefined();
+ expect(result.length).toBeGreaterThanOrEqual(1);
+
+ for (const path of result) {
+ expect(path.steps).toBeDefined();
+
+ expect(path.steps.length).toEqual(1);
+ expect(path.steps[0]).toBeDefined();
+
+ expect(query.from.map((from) => from.id)).toContain(path.steps[0].startLocation.id);
+ expect(query.to.map((to) => to.id)).toContain(path.steps[0].stopLocation.id);
+ }
+ });
+ });
+ });
+
+ describe("real-time data", () => {
+ const createQueryRunner = () => {
+ const ldFetch = new LDFetch({ headers: { Accept: "application/ld+json" } });
+
+ const connectionFetcher = new ConnectionsFetcherLazy(ldFetch);
+ connectionFetcher.setTravelMode(TravelMode.Train);
+ connectionFetcher.setAccessUrl("https://graph.irail.be/sncb/connections");
+
+ const stopsFetcher = new StopsFetcherLDFetch(ldFetch);
+ stopsFetcher.setAccessUrl("https://irail.be/stations/NMBS");
+
+ const locationResolver = new LocationResolverDefault(stopsFetcher);
+ const reachableStopsFinder = new ReachableStopsFinderBirdsEyeCached(stopsFetcher);
+ const journeyExtractor = new JourneyExtractorEarliestArrival(
+ locationResolver,
+ );
+
+ const CSA = new CSAEarliestArrival(
+ connectionFetcher,
+ locationResolver,
+ reachableStopsFinder,
+ reachableStopsFinder,
+ reachableStopsFinder,
+ journeyExtractor,
+ );
+
+ return new QueryRunnerDefault(locationResolver, CSA);
+ };
+
+ const checkStops = (result, query) => {
+ expect(result).toBeDefined();
+ expect(result.length).toBeGreaterThanOrEqual(1);
+
+ for (const path of result) {
+ expect(path.steps).toBeDefined();
+
+ expect(path.steps.length).toBeGreaterThanOrEqual(1);
+
+ let currentLocation = query.from;
+ path.steps.forEach((step: IStep) => {
+ expect(step).toBeDefined();
+ expect(currentLocation).toEqual(step.startLocation.id);
+ currentLocation = step.stopLocation.id;
+ });
+
+ expect(query.to).toEqual(currentLocation);
+ }
+ };
+
+ const checkTimes = (result, minimumDepartureTime) => {
+ expect(result).toBeDefined();
+ expect(result.length).toBeGreaterThanOrEqual(1);
+
+ for (const path of result) {
+ expect(path.steps).toBeDefined();
+
+ expect(path.steps.length).toBeGreaterThanOrEqual(1);
+ expect(path.steps[0]).toBeDefined();
+ expect(path.steps[path.steps.length - 1]).toBeDefined();
+
+ let currentTime = minimumDepartureTime.getTime();
+ path.steps.forEach((step: IStep) => {
+ expect(step).toBeDefined();
+ if (step.travelMode === TravelMode.Walking) {
+ currentTime += step.duration.minimum;
+ } else {
+ expect(currentTime).toBeLessThanOrEqual(step.startTime.getTime());
+ currentTime = step.stopTime.getTime();
+ }
+ });
+
+ expect(path.steps[0].startTime.getTime()).toBeGreaterThanOrEqual(minimumDepartureTime.getTime());
+ }
+ };
+
+ describe("Departure Time now - Arrival Time now+2h", () => {
+ jest.setTimeout(100000);
+
+ const minimumDepartureTime = new Date();
+
+ const query: IQuery = {
+ publicTransportOnly: true,
+ from: "http://irail.be/stations/NMBS/008896925", // Ingelmunster
+ to: "http://irail.be/stations/NMBS/008892007", // Ghent-Sint-Pieters
+ minimumDepartureTime,
+ };
+ let result: IPath[];
+
+ beforeAll(async () => {
+ const queryRunner = createQueryRunner();
+ const iterator = await queryRunner.run(query);
+
+ result = await Iterators.toArray(iterator);
+ });
+
+ it("Correct departure and arrival stop", () => {
+ checkStops(result, query);
+ });
+
+ it("Correct departure and arrival time", () => {
+ checkTimes(result, minimumDepartureTime);
+ });
+ });
+ });
+});
diff --git a/src/planner/public-transport/CSAEarliestArrival.ts b/src/planner/public-transport/CSAEarliestArrival.ts
new file mode 100644
index 00000000..ff997909
--- /dev/null
+++ b/src/planner/public-transport/CSAEarliestArrival.ts
@@ -0,0 +1,514 @@
+import { ArrayIterator, AsyncIterator } from "asynciterator";
+import { inject, injectable, tagged } from "inversify";
+import Context from "../../Context";
+import DropOffType from "../../enums/DropOffType";
+import EventType from "../../enums/EventType";
+import PickupType from "../../enums/PickupType";
+import ReachableStopsFinderMode from "../../enums/ReachableStopsFinderMode";
+import ReachableStopsSearchPhase from "../../enums/ReachableStopsSearchPhase";
+import TravelMode from "../../enums/TravelMode";
+import IConnection from "../../fetcher/connections/IConnection";
+import IConnectionsProvider from "../../fetcher/connections/IConnectionsProvider";
+import IStop from "../../fetcher/stops/IStop";
+import ILocation from "../../interfaces/ILocation";
+import IPath from "../../interfaces/IPath";
+import IStep from "../../interfaces/IStep";
+import ILocationResolver from "../../query-runner/ILocationResolver";
+import IResolvedQuery from "../../query-runner/IResolvedQuery";
+import TYPES from "../../types";
+import Geo from "../../util/Geo";
+import Path from "../Path";
+import Step from "../Step";
+import IReachableStopsFinder, { IReachableStop } from "../stops/IReachableStopsFinder";
+import IProfileByStop from "./CSA/data-structure/stops/IProfileByStop";
+import ITransferProfile from "./CSA/data-structure/stops/ITransferProfile";
+import IEnterConnectionByTrip from "./CSA/data-structure/trips/IEnterConnectionByTrip";
+import IJourneyExtractor from "./IJourneyExtractor";
+import IPublicTransportPlanner from "./IPublicTransportPlanner";
+
+/**
+ * An implementation of the Connection Scan Algorithm (CSA).
+ *
+ * @implements [[IPublicTransportPlanner]]
+ * @property profilesByStop Describes the CSA profiles for each scanned stop.
+ * @property enterConnectionByTrip Describes the connection you should enter at a departure location for each trip.
+ *
+ * @returns multiple [[IPath]]s that consist of several [[IStep]]s.
+ */
+@injectable()
+export default class CSAEarliestArrival implements IPublicTransportPlanner {
+ private readonly connectionsProvider: IConnectionsProvider;
+ private readonly locationResolver: ILocationResolver;
+ private readonly initialReachableStopsFinder: IReachableStopsFinder;
+ private readonly finalReachableStopsFinder: IReachableStopsFinder;
+ private readonly transferReachableStopsFinder: IReachableStopsFinder;
+ private readonly journeyExtractor: IJourneyExtractor;
+ private readonly context: Context;
+
+ private profilesByStop: IProfileByStop = {}; // S
+ private enterConnectionByTrip: IEnterConnectionByTrip = {}; // T
+ private gtfsTripsByConnection = {};
+
+ private initialReachableStops: IReachableStop[] = [];
+ private finalReachableStops: IReachableStop[] = [];
+
+ private query: IResolvedQuery;
+ private connectionsIterator: AsyncIterator;
+
+ constructor(
+ @inject(TYPES.ConnectionsProvider)
+ connectionsProvider: IConnectionsProvider,
+ @inject(TYPES.LocationResolver)
+ locationResolver: ILocationResolver,
+ @inject(TYPES.ReachableStopsFinder)
+ @tagged("phase", ReachableStopsSearchPhase.Initial)
+ initialReachableStopsFinder: IReachableStopsFinder,
+ @inject(TYPES.ReachableStopsFinder)
+ @tagged("phase", ReachableStopsSearchPhase.Transfer)
+ transferReachableStopsFinder: IReachableStopsFinder,
+ @inject(TYPES.ReachableStopsFinder)
+ @tagged("phase", ReachableStopsSearchPhase.Final)
+ finalReachableStopsFinder: IReachableStopsFinder,
+ @inject(TYPES.JourneyExtractor)
+ journeyExtractor: IJourneyExtractor,
+ @inject(TYPES.Context)
+ context?: Context,
+ ) {
+ this.connectionsProvider = connectionsProvider;
+ this.locationResolver = locationResolver;
+ this.initialReachableStopsFinder = initialReachableStopsFinder;
+ this.transferReachableStopsFinder = transferReachableStopsFinder;
+ this.finalReachableStopsFinder = finalReachableStopsFinder;
+ this.journeyExtractor = journeyExtractor;
+ this.context = context;
+ }
+
+ public async plan(query: IResolvedQuery): Promise> {
+ this.query = query;
+
+ this.setBounds();
+
+ return this.calculateJourneys();
+ }
+
+ private setBounds() {
+ const {
+ minimumDepartureTime: lowerBoundDate,
+ maximumArrivalTime: upperBoundDate,
+ } = this.query;
+
+ this.connectionsProvider.setIteratorOptions({
+ upperBoundDate,
+ lowerBoundDate,
+ });
+ }
+
+ private async calculateJourneys(): Promise> {
+ const hasInitialReachableStops: boolean = await this.initInitialReachableStops();
+ const hasFinalReachableStops: boolean = await this.initFinalReachableStops();
+
+ if (!hasInitialReachableStops || !hasFinalReachableStops) {
+ return Promise.resolve(new ArrayIterator([]));
+ }
+
+ this.connectionsIterator = this.connectionsProvider.createIterator();
+
+ const self = this;
+
+ return new Promise((resolve, reject) => {
+ let isDone = false;
+
+ const done = () => {
+ if (!isDone) {
+ self.connectionsIterator.close();
+
+ self.journeyExtractor.extractJourneys(self.profilesByStop, self.query)
+ .then((resultIterator) => {
+ resolve(resultIterator);
+ });
+
+ isDone = true;
+ }
+ };
+
+ this.connectionsIterator.on("readable", () =>
+ self.processNextConnection(done),
+ );
+
+ this.connectionsIterator.on("end", () => done());
+
+ }) as Promise>;
+ }
+
+ private async processNextConnection(done: () => void) {
+ let connection = this.connectionsIterator.read();
+
+ while (connection) {
+ this.discoverConnection(connection);
+
+ const {
+ to,
+ minimumDepartureTime,
+ maximumWalkingDuration,
+ minimumTransferDuration,
+ maximumTransferDuration,
+ } = this.query;
+
+ const arrivalStopId: string = to[0].id;
+ if (this.profilesByStop[arrivalStopId].arrivalTime <= connection.departureTime.getTime()) {
+ this.connectionsIterator.close();
+ done();
+ break;
+ }
+
+ if (connection.departureTime < minimumDepartureTime && !this.connectionsIterator.closed) {
+ connection = this.connectionsIterator.read();
+ continue;
+ }
+
+ const tripIds = this.getTripIdsFromConnection(connection);
+ for (const tripId of tripIds) {
+
+ const canRemainSeated = this.enterConnectionByTrip[tripId];
+
+ const initialStop = this.initialReachableStops.find(({ stop }) =>
+ stop.id === connection.departureStop,
+ );
+
+ const departure = connection.departureTime.getTime();
+ const arrival = this.profilesByStop[connection.departureStop].arrivalTime;
+
+ const transferDuration = departure - arrival;
+
+ const canTakeTransfer = (
+ transferDuration > -Infinity &&
+ (transferDuration >= minimumTransferDuration || initialStop && transferDuration >= 0) &&
+ (transferDuration <= maximumTransferDuration || initialStop && transferDuration <= maximumWalkingDuration) &&
+ connection["gtfs:pickupType"] !== PickupType.NotAvailable
+ );
+
+ if (canRemainSeated || canTakeTransfer) {
+ this.updateTrips(connection, tripId);
+
+ if (connection["gtfs:dropOffType"] !== DropOffType.NotAvailable) {
+ await this.updateProfiles(connection, tripId);
+ }
+ }
+
+ }
+
+ if (!this.connectionsIterator.closed) {
+ connection = this.connectionsIterator.read();
+ continue;
+ }
+
+ connection = undefined;
+ }
+ }
+
+ private async initInitialReachableStops(): Promise {
+ const fromLocation: IStop = this.query.from[0] as IStop;
+
+ // Making sure the departure location has an id
+ const geoId = Geo.getId(this.query.from[0]);
+ if (!fromLocation.id) {
+ this.query.from[0].id = geoId;
+ this.query.from[0].name = "Departure location";
+ }
+
+ this.initialReachableStops = await this.initialReachableStopsFinder.findReachableStops(
+ fromLocation,
+ ReachableStopsFinderMode.Source,
+ this.query.maximumWalkingDuration,
+ this.query.minimumWalkingSpeed,
+ );
+
+ // Abort when we can't reach a single stop.
+ if (this.initialReachableStops.length <= 1 && this.initialReachableStops[0].stop.id === geoId && this.context) {
+ this.context.emit(EventType.AbortQuery, "No reachable stops at departure location");
+
+ return false;
+ }
+
+ // Check if departure location is a stop.
+ for (const reachableStop of this.initialReachableStops) {
+ if (reachableStop.duration === 0) {
+ this.query.from[0] = reachableStop.stop;
+ }
+ }
+
+ if (this.context) {
+ this.context.emit(EventType.InitialReachableStops, this.initialReachableStops);
+ }
+
+ this.initialReachableStops.forEach(({ stop, duration }: IReachableStop) => {
+ const departureTime = this.query.minimumDepartureTime.getTime();
+ const arrivalTime = this.query.minimumDepartureTime.getTime() + duration;
+
+ this.profilesByStop[stop.id] = {
+ departureTime,
+ arrivalTime,
+ };
+
+ if (duration > 0) {
+ const path: Path = Path.create();
+
+ const footpath: IStep = Step.create(
+ this.query.from[0],
+ stop,
+ TravelMode.Walking,
+ {
+ minimum: arrivalTime - departureTime,
+ },
+ new Date(departureTime),
+ new Date(arrivalTime),
+ );
+
+ path.addStep(footpath);
+
+ this.profilesByStop[stop.id].path = path;
+ }
+
+ });
+
+ return true;
+ }
+
+ private async initFinalReachableStops(): Promise {
+ const arrivalStop: IStop = this.query.to[0] as IStop;
+
+ const geoId = Geo.getId(this.query.to[0]);
+ if (!this.query.to[0].id) {
+ this.query.to[0].id = geoId;
+ this.query.to[0].name = "Arrival location";
+ }
+
+ this.finalReachableStops = await this.finalReachableStopsFinder
+ .findReachableStops(
+ arrivalStop,
+ ReachableStopsFinderMode.Target,
+ this.query.maximumWalkingDuration,
+ this.query.minimumWalkingSpeed,
+ );
+
+ if (this.finalReachableStops.length <= 1 && this.finalReachableStops[0].stop.id === geoId && this.context) {
+ this.context.emit(EventType.AbortQuery, "No reachable stops at arrival location");
+
+ return false;
+ }
+
+ if (this.context) {
+ this.context.emit(EventType.FinalReachableStops, this.finalReachableStops);
+ }
+
+ // Check if arrival location is a stop.
+ for (const reachableStop of this.finalReachableStops) {
+ if (reachableStop.duration === 0) {
+ this.query.to[0] = reachableStop.stop;
+ }
+ }
+
+ this.profilesByStop[this.query.to[0].id] = {
+ departureTime: Infinity,
+ arrivalTime: Infinity,
+ };
+
+ return true;
+ }
+
+ private discoverConnection(connection: IConnection) {
+ this.setTripIdsByConnectionId(connection);
+
+ if (!this.profilesByStop[connection.departureStop]) {
+ this.profilesByStop[connection.departureStop] = {
+ departureTime: Infinity,
+ arrivalTime: Infinity,
+ };
+ }
+
+ if (!this.profilesByStop[connection.arrivalStop]) {
+ this.profilesByStop[connection.arrivalStop] = {
+ departureTime: Infinity,
+ arrivalTime: Infinity,
+ };
+ }
+ }
+
+ private getTripIdsFromConnection(connection: IConnection): string[] {
+ return this.gtfsTripsByConnection[connection.id];
+ }
+
+ private setTripIdsByConnectionId(connection: IConnection): void {
+ if (!this.gtfsTripsByConnection.hasOwnProperty(connection.id)) {
+ this.gtfsTripsByConnection[connection.id] = [];
+ }
+
+ this.gtfsTripsByConnection[connection.id].push(connection["gtfs:trip"]);
+
+ let nextConnectionIndex = 0;
+ while (connection.nextConnection && nextConnectionIndex < connection.nextConnection.length) {
+ const connectionId = connection.nextConnection[nextConnectionIndex];
+
+ if (!this.gtfsTripsByConnection.hasOwnProperty(connectionId)) {
+ this.gtfsTripsByConnection[connectionId] = [];
+
+ }
+
+ this.gtfsTripsByConnection[connectionId].push(connection["gtfs:trip"]);
+ nextConnectionIndex++;
+ }
+
+ }
+
+ private updateTrips(connection: IConnection, tripId: string): void {
+ const isInitialReachableStop = this.initialReachableStops.find(({ stop }: IReachableStop) =>
+ stop.id === connection.departureStop,
+ );
+
+ if (!this.enterConnectionByTrip[tripId] || isInitialReachableStop) {
+ this.enterConnectionByTrip[tripId] = connection;
+ }
+ }
+
+ private async updateProfiles(connection: IConnection, tripId: string): Promise {
+ try {
+ const arrivalStop: ILocation = await this.locationResolver.resolve(connection.arrivalStop);
+ const reachableStops: IReachableStop[] = await this.transferReachableStopsFinder.findReachableStops(
+ arrivalStop as IStop,
+ ReachableStopsFinderMode.Source,
+ this.query.maximumTransferDuration,
+ this.query.minimumWalkingSpeed,
+ );
+
+ reachableStops.forEach((reachableStop: IReachableStop) => {
+ const { stop, duration } = reachableStop;
+
+ if (!this.profilesByStop[stop.id]) {
+ this.profilesByStop[stop.id] = {
+ departureTime: Infinity,
+ arrivalTime: Infinity,
+ };
+ }
+
+ if (stop.id === this.query.to[0].id) {
+ return;
+ }
+
+ const reachableStopArrival = this.profilesByStop[stop.id].arrivalTime;
+
+ const departureTime = connection.departureTime.getTime();
+ const arrivalTime = connection.arrivalTime.getTime() + duration;
+
+ if (reachableStopArrival > arrivalTime) {
+ const transferProfile: ITransferProfile = {
+ departureTime,
+ arrivalTime,
+ exitConnection: connection,
+ enterConnection: this.enterConnectionByTrip[tripId],
+ };
+
+ if (duration > 0) {
+ const path: Path = Path.create();
+
+ const footpath: IStep = Step.create(
+ arrivalStop,
+ stop,
+ TravelMode.Walking,
+ {
+ minimum: duration,
+ },
+ new Date(arrivalTime - duration),
+ new Date(arrivalTime),
+ );
+
+ path.addStep(footpath);
+
+ transferProfile.path = path;
+ }
+
+ if (this.context && this.context.listenerCount(EventType.AddedNewTransferProfile) > 0) {
+ this.emitTransferProfile(transferProfile);
+ }
+
+ this.profilesByStop[stop.id] = transferProfile;
+ }
+ });
+
+ this.checkIfArrivalStopIsReachable(connection, tripId, arrivalStop);
+
+ } catch (e) {
+ if (this.context) {
+ this.context.emitWarning(e);
+ }
+ }
+ }
+
+ private checkIfArrivalStopIsReachable(connection: IConnection, tripId: string, arrivalStop: ILocation): void {
+ const canReachArrivalStop = this.finalReachableStops.find((reachableStop: IReachableStop) =>
+ reachableStop.stop.id === arrivalStop.id,
+ );
+
+ if (canReachArrivalStop) {
+ const finalLocationId = this.query.to[0].id;
+
+ if (canReachArrivalStop.stop.id === finalLocationId) {
+ const departureTime = connection.departureTime.getTime();
+ const arrivalTime = connection.arrivalTime.getTime();
+ const reachableStopArrival = this.profilesByStop[connection.arrivalStop].arrivalTime;
+
+ if (reachableStopArrival > arrivalTime) {
+ this.profilesByStop[finalLocationId] = {
+ departureTime,
+ arrivalTime,
+ exitConnection: connection,
+ enterConnection: this.enterConnectionByTrip[tripId],
+ };
+ }
+
+ }
+
+ const finalProfile = this.profilesByStop[finalLocationId];
+
+ const departureTime = connection.arrivalTime.getTime();
+ const arrivalTime = connection.arrivalTime.getTime() + canReachArrivalStop.duration;
+
+ if ((!finalProfile || finalProfile.arrivalTime > arrivalTime) && canReachArrivalStop.duration > 0) {
+ const path: Path = Path.create();
+
+ const footpath: IStep = Step.create(
+ arrivalStop,
+ this.query.to[0],
+ TravelMode.Walking,
+ {
+ minimum: arrivalTime - departureTime,
+ },
+ new Date(departureTime),
+ new Date(arrivalTime),
+ );
+
+ path.addStep(footpath);
+
+ this.profilesByStop[finalLocationId] = {
+ departureTime,
+ arrivalTime,
+ path,
+ };
+ }
+ }
+ }
+
+ private async emitTransferProfile(transferProfile: ITransferProfile): Promise {
+ try {
+ const departureStop = await this.locationResolver.resolve(transferProfile.enterConnection.departureStop);
+ const arrivalStop = await this.locationResolver.resolve(transferProfile.exitConnection.arrivalStop);
+
+ this.context.emit(EventType.AddedNewTransferProfile, {
+ departureStop,
+ arrivalStop,
+ });
+
+ } catch (e) {
+ this.context.emitWarning(e);
+ }
+ }
+}
diff --git a/src/planner/public-transport/PublicTransportPlannerCSAProfile.test.ts b/src/planner/public-transport/CSAProfile.test.ts
similarity index 94%
rename from src/planner/public-transport/PublicTransportPlannerCSAProfile.test.ts
rename to src/planner/public-transport/CSAProfile.test.ts
index 85927f8b..8bcba17e 100644
--- a/src/planner/public-transport/PublicTransportPlannerCSAProfile.test.ts
+++ b/src/planner/public-transport/CSAProfile.test.ts
@@ -1,7 +1,8 @@
import "jest";
import LDFetch from "ldfetch";
import Defaults from "../../Defaults";
-import ConnectionsFetcherLazy from "../../fetcher/connections/ld-fetch/ConnectionsFetcherLazy";
+import TravelMode from "../../enums/TravelMode";
+import ConnectionsFetcherLazy from "../../fetcher/connections/lazy/ConnectionsFetcherLazy";
import ConnectionsFetcherNMBSTest from "../../fetcher/connections/tests/ConnectionsFetcherNMBSTest";
import connectionsIngelmunsterGhent from "../../fetcher/connections/tests/data/ingelmunster-ghent";
import connectionsJoining from "../../fetcher/connections/tests/data/joining";
@@ -13,11 +14,10 @@ import IStep from "../../interfaces/IStep";
import IResolvedQuery from "../../query-runner/IResolvedQuery";
import LocationResolverDefault from "../../query-runner/LocationResolverDefault";
import QueryRunnerDefault from "../../query-runner/QueryRunnerDefault";
-import TravelMode from "../../TravelMode";
import Iterators from "../../util/Iterators";
import ReachableStopsFinderBirdsEyeCached from "../stops/ReachableStopsFinderBirdsEyeCached";
-import JourneyExtractorDefault from "./JourneyExtractorDefault";
-import PublicTransportPlannerCSAProfile from "./PublicTransportPlannerCSAProfile";
+import CSAProfile from "./CSAProfile";
+import JourneyExtractorProfile from "./JourneyExtractorProfile";
describe("[PublicTransportPlannerCSAProfile]", () => {
describe("mock data", () => {
@@ -27,18 +27,18 @@ describe("[PublicTransportPlannerCSAProfile]", () => {
const ldFetch = new LDFetch({ headers: { Accept: "application/ld+json" } });
const connectionFetcher = new ConnectionsFetcherNMBSTest(connections);
- connectionFetcher.setConfig({ backward: true });
+ connectionFetcher.setIteratorOptions({ backward: true });
const stopsFetcher = new StopsFetcherLDFetch(ldFetch);
stopsFetcher.setAccessUrl("https://irail.be/stations/NMBS");
const locationResolver = new LocationResolverDefault(stopsFetcher);
const reachableStopsFinder = new ReachableStopsFinderBirdsEyeCached(stopsFetcher);
- const journeyExtractor = new JourneyExtractorDefault(
+ const journeyExtractor = new JourneyExtractorProfile(
locationResolver,
);
- return new PublicTransportPlannerCSAProfile(
+ return new CSAProfile(
connectionFetcher,
locationResolver,
reachableStopsFinder,
@@ -185,11 +185,11 @@ describe("[PublicTransportPlannerCSAProfile]", () => {
const locationResolver = new LocationResolverDefault(stopsFetcher);
const reachableStopsFinder = new ReachableStopsFinderBirdsEyeCached(stopsFetcher);
- const journeyExtractor = new JourneyExtractorDefault(
+ const journeyExtractor = new JourneyExtractorProfile(
locationResolver,
);
- const CSA = new PublicTransportPlannerCSAProfile(
+ const CSA = new CSAProfile(
connectionFetcher,
locationResolver,
reachableStopsFinder,
@@ -259,7 +259,7 @@ describe("[PublicTransportPlannerCSAProfile]", () => {
const query: IQuery = {
publicTransportOnly: true,
from: "http://irail.be/stations/NMBS/008896925", // Ingelmunster
- to: "http://irail.be/stations/NMBS/008892007", // Antwerpen
+ to: "http://irail.be/stations/NMBS/008892007", // Ghent-Sint-Pieters
maximumArrivalTime,
minimumDepartureTime,
};
@@ -291,7 +291,7 @@ describe("[PublicTransportPlannerCSAProfile]", () => {
const query: IQuery = {
publicTransportOnly: true,
from: "http://irail.be/stations/NMBS/008896925", // Ingelmunster
- to: "http://irail.be/stations/NMBS/008892007", // Antwerpen
+ to: "http://irail.be/stations/NMBS/008892007", // Ghent-Sint-Pieters
maximumArrivalTime,
minimumDepartureTime,
};
diff --git a/src/planner/public-transport/PublicTransportPlannerCSAProfile.ts b/src/planner/public-transport/CSAProfile.ts
similarity index 80%
rename from src/planner/public-transport/PublicTransportPlannerCSAProfile.ts
rename to src/planner/public-transport/CSAProfile.ts
index e8c7db71..25eb5d64 100644
--- a/src/planner/public-transport/PublicTransportPlannerCSAProfile.ts
+++ b/src/planner/public-transport/CSAProfile.ts
@@ -1,11 +1,13 @@
-import { AsyncIterator } from "asynciterator";
+import { ArrayIterator, AsyncIterator } from "asynciterator";
import { inject, injectable, tagged } from "inversify";
import Context from "../../Context";
-import EventType from "../../EventType";
-import DropOffType from "../../fetcher/connections/DropOffType";
+import DropOffType from "../../enums/DropOffType";
+import EventType from "../../enums/EventType";
+import PickupType from "../../enums/PickupType";
+import ReachableStopsFinderMode from "../../enums/ReachableStopsFinderMode";
+import ReachableStopsSearchPhase from "../../enums/ReachableStopsSearchPhase";
import IConnection from "../../fetcher/connections/IConnection";
import IConnectionsProvider from "../../fetcher/connections/IConnectionsProvider";
-import PickupType from "../../fetcher/connections/PickupType";
import IStop from "../../fetcher/stops/IStop";
import ILocation from "../../interfaces/ILocation";
import IPath from "../../interfaces/IPath";
@@ -13,10 +15,9 @@ import { DurationMs } from "../../interfaces/units";
import ILocationResolver from "../../query-runner/ILocationResolver";
import IResolvedQuery from "../../query-runner/IResolvedQuery";
import TYPES from "../../types";
+import Geo from "../../util/Geo";
import Vectors from "../../util/Vectors";
import IReachableStopsFinder, { IReachableStop } from "../stops/IReachableStopsFinder";
-import ReachableStopsFinderMode from "../stops/ReachableStopsFinderMode";
-import ReachableStopsSearchPhase from "../stops/ReachableStopsSearchPhase";
import IArrivalTimeByTransfers from "./CSA/data-structure/IArrivalTimeByTransfers";
import IProfilesByStop from "./CSA/data-structure/stops/IProfilesByStop";
import ITransferProfile from "./CSA/data-structure/stops/ITransferProfile";
@@ -39,7 +40,7 @@ import IPublicTransportPlanner from "./IPublicTransportPlanner";
* @returns multiple [[IPath]]s that consist of several [[IStep]]s.
*/
@injectable()
-export default class PublicTransportPlannerCSAProfile implements IPublicTransportPlanner {
+export default class CSAProfile implements IPublicTransportPlanner {
private readonly connectionsProvider: IConnectionsProvider;
private readonly locationResolver: ILocationResolver;
private readonly initialReachableStopsFinder: IReachableStopsFinder;
@@ -88,18 +89,6 @@ export default class PublicTransportPlannerCSAProfile implements IPublicTranspor
public async plan(query: IResolvedQuery): Promise> {
this.query = query;
- const departureLocation = this.query.from[0];
- if (!departureLocation.id) {
- departureLocation.id = "geo:" + departureLocation.latitude + "," + departureLocation.longitude;
- departureLocation.name = "Departure location";
- }
-
- const arrivalLocation = this.query.to[0];
- if (!arrivalLocation.id) {
- arrivalLocation.id = "geo:" + arrivalLocation.latitude + "," + arrivalLocation.longitude;
- arrivalLocation.name = "Arrival location";
- }
-
this.setBounds();
return this.calculateJourneys();
@@ -111,7 +100,7 @@ export default class PublicTransportPlannerCSAProfile implements IPublicTranspor
maximumArrivalTime: upperBoundDate,
} = this.query;
- this.connectionsProvider.setConfig({
+ this.connectionsProvider.setIteratorOptions({
backward: true,
upperBoundDate,
lowerBoundDate,
@@ -119,8 +108,12 @@ export default class PublicTransportPlannerCSAProfile implements IPublicTranspor
}
private async calculateJourneys(): Promise> {
- await this.initDurationToTargetByStop();
- await this.initInitialReachableStops();
+ const hasInitialReachableStops: boolean = await this.initDurationToTargetByStop();
+ const hasFinalReachableStops: boolean = await this.initInitialReachableStops();
+
+ if (!hasInitialReachableStops || !hasFinalReachableStops) {
+ return Promise.resolve(new ArrayIterator([]));
+ }
this.connectionsIterator = this.connectionsProvider.createIterator();
@@ -128,6 +121,7 @@ export default class PublicTransportPlannerCSAProfile implements IPublicTranspor
return new Promise((resolve, reject) => {
let isDone = false;
+
const done = () => {
if (!isDone) {
self.connectionsIterator.close();
@@ -136,6 +130,7 @@ export default class PublicTransportPlannerCSAProfile implements IPublicTranspor
.then((resultIterator) => {
resolve(resultIterator);
});
+
isDone = true;
}
};
@@ -149,19 +144,19 @@ export default class PublicTransportPlannerCSAProfile implements IPublicTranspor
}) as Promise>;
}
- private processNextConnection(done: () => void) {
- const connection = this.connectionsIterator.read();
+ private async processNextConnection(done: () => void) {
+ let connection = this.connectionsIterator.read();
- if (connection) {
- if (connection.arrivalTime > this.query.maximumArrivalTime) {
- this.maybeProcessNextConnection(done);
- return;
+ while (connection) {
+ if (connection.arrivalTime > this.query.maximumArrivalTime && !this.connectionsIterator.closed) {
+ connection = this.connectionsIterator.read();
+ continue;
}
if (connection.departureTime < this.query.minimumDepartureTime) {
this.connectionsIterator.close();
done();
- return;
+ break;
}
if (this.context) {
@@ -174,18 +169,15 @@ export default class PublicTransportPlannerCSAProfile implements IPublicTranspor
this.updateEarliestArrivalByTrip(connection, earliestArrivalTime);
if (!this.isDominated(connection, earliestArrivalTime)) {
- this.getFootpathsForDepartureStop(connection, earliestArrivalTime)
- .then(() => this.maybeProcessNextConnection(done));
+ await this.getFootpathsForDepartureStop(connection, earliestArrivalTime);
+ }
- } else {
- this.maybeProcessNextConnection(done);
+ if (!this.connectionsIterator.closed) {
+ connection = this.connectionsIterator.read();
+ continue;
}
- }
- }
- private maybeProcessNextConnection(done: () => void) {
- if (!this.connectionsIterator.closed) {
- this.processNextConnection(done);
+ connection = undefined;
}
}
@@ -224,7 +216,7 @@ export default class PublicTransportPlannerCSAProfile implements IPublicTranspor
private calculateEarliestArrivalTime(connection: IConnection): IArrivalTimeByTransfers {
const remainSeatedTime = this.remainSeated(connection);
- if (connection["gtfs:dropOffType"] === "gtfs:NotAvailable") {
+ if (connection["gtfs:dropOffType"] === DropOffType.NotAvailable) {
return remainSeatedTime;
}
@@ -237,6 +229,12 @@ export default class PublicTransportPlannerCSAProfile implements IPublicTranspor
private async initDurationToTargetByStop(): Promise {
const arrivalStop: IStop = this.query.to[0] as IStop;
+ const geoId = Geo.getId(this.query.to[0]);
+ if (!this.query.to[0].id) {
+ this.query.to[0].id = geoId;
+ this.query.to[0].name = "Arrival location";
+ }
+
const reachableStops = await this.finalReachableStopsFinder
.findReachableStops(
arrivalStop,
@@ -245,8 +243,10 @@ export default class PublicTransportPlannerCSAProfile implements IPublicTranspor
this.query.minimumWalkingSpeed,
);
- if (reachableStops.length === 0 && this.context) {
- this.context.emit(EventType.QueryAbort);
+ if (reachableStops.length <= 1 && reachableStops[0].stop.id === geoId) {
+ if (this.context) {
+ this.context.emit(EventType.AbortQuery, "No reachable stops at arrival location");
+ }
return false;
}
@@ -256,6 +256,10 @@ export default class PublicTransportPlannerCSAProfile implements IPublicTranspor
}
for (const reachableStop of reachableStops) {
+ if (reachableStop.duration === 0) {
+ this.query.to[0] = reachableStop.stop;
+ }
+
this.durationToTargetByStop[reachableStop.stop.id] = reachableStop.duration;
}
@@ -265,6 +269,12 @@ export default class PublicTransportPlannerCSAProfile implements IPublicTranspor
private async initInitialReachableStops(): Promise {
const fromLocation: IStop = this.query.from[0] as IStop;
+ const geoId = Geo.getId(this.query.from[0]);
+ if (!this.query.from[0].id) {
+ this.query.from[0].id = geoId;
+ this.query.from[0].name = "Departure location";
+ }
+
this.initialReachableStops = await this.initialReachableStopsFinder.findReachableStops(
fromLocation,
ReachableStopsFinderMode.Source,
@@ -272,8 +282,16 @@ export default class PublicTransportPlannerCSAProfile implements IPublicTranspor
this.query.minimumWalkingSpeed,
);
- if (this.initialReachableStops.length === 0 && this.context) {
- this.context.emit(EventType.QueryAbort);
+ for (const reachableStop of this.initialReachableStops) {
+ if (reachableStop.duration === 0) {
+ this.query.from[0] = reachableStop.stop;
+ }
+ }
+
+ if (this.initialReachableStops.length <= 1 && this.initialReachableStops[0].stop.id === geoId) {
+ if (this.context) {
+ this.context.emit(EventType.AbortQuery, "No reachable stops at departure location");
+ }
return false;
}
@@ -402,35 +420,47 @@ export default class PublicTransportPlannerCSAProfile implements IPublicTranspor
);
}
- const departureStop: ILocation = await this.locationResolver.resolve(connection.departureStop);
- const reachableStops: IReachableStop[] = await this.transferReachableStopsFinder.findReachableStops(
- departureStop as IStop,
- ReachableStopsFinderMode.Source,
- this.query.maximumWalkingDuration,
- this.query.minimumWalkingSpeed,
- );
+ try {
+ const departureStop: ILocation = await this.locationResolver.resolve(connection.departureStop);
+ const reachableStops: IReachableStop[] = await this.transferReachableStopsFinder.findReachableStops(
+ departureStop as IStop,
+ ReachableStopsFinderMode.Source,
+ this.query.maximumTransferDuration,
+ this.query.minimumWalkingSpeed,
+ );
- reachableStops.forEach((reachableStop: IReachableStop) => {
- if (reachableStop.stop.id !== departureLocation.id) {
- this.incorporateInProfile(
- connection,
- reachableStop.duration,
- reachableStop.stop,
- earliestArrivalTimeByTransfers,
- );
+ reachableStops.forEach((reachableStop: IReachableStop) => {
+ if (reachableStop.stop.id !== departureLocation.id) {
+ this.incorporateInProfile(
+ connection,
+ reachableStop.duration,
+ reachableStop.stop,
+ earliestArrivalTimeByTransfers,
+ );
+ }
+ });
+
+ } catch (e) {
+ if (this.context) {
+ this.context.emitWarning(e);
}
- });
+ }
}
private async emitTransferProfile(transferProfile: ITransferProfile, amountOfTransfers: number): Promise {
- const departureStop = await this.locationResolver.resolve(transferProfile.enterConnection.departureStop);
- const arrivalStop = await this.locationResolver.resolve(transferProfile.exitConnection.arrivalStop);
+ try {
+ const departureStop = await this.locationResolver.resolve(transferProfile.enterConnection.departureStop);
+ const arrivalStop = await this.locationResolver.resolve(transferProfile.exitConnection.arrivalStop);
- this.context.emit(EventType.AddedNewTransferProfile, {
- departureStop,
- arrivalStop,
- amountOfTransfers,
- });
+ this.context.emit(EventType.AddedNewTransferProfile, {
+ departureStop,
+ arrivalStop,
+ amountOfTransfers,
+ });
+
+ } catch (e) {
+ this.context.emitWarning(e);
+ }
}
private incorporateInProfile(
@@ -472,7 +502,11 @@ export default class PublicTransportPlannerCSAProfile implements IPublicTranspor
const possibleExitConnection = this.earliestArrivalByTrip[connection["gtfs:trip"]]
[amountOfTransfers].connection || connection;
+ const isValidRoute = !this.query.maximumTravelDuration ||
+ (arrivalTimeByTransfers[amountOfTransfers].arrivalTime - departureTime <= this.query.maximumTravelDuration);
+
if (
+ isValidRoute &&
arrivalTimeByTransfers[amountOfTransfers].arrivalTime < transferProfile.arrivalTime &&
connection["gtfs:pickupType"] !== PickupType.NotAvailable &&
possibleExitConnection["gtfs:dropOfType"] !== DropOffType.NotAvailable
diff --git a/src/planner/public-transport/IJourneyExtractor.ts b/src/planner/public-transport/IJourneyExtractor.ts
index 5febd168..67a50362 100644
--- a/src/planner/public-transport/IJourneyExtractor.ts
+++ b/src/planner/public-transport/IJourneyExtractor.ts
@@ -1,8 +1,12 @@
import { AsyncIterator } from "asynciterator";
import IPath from "../../interfaces/IPath";
import IResolvedQuery from "../../query-runner/IResolvedQuery";
+import IProfileByStop from "./CSA/data-structure/stops/IProfileByStop";
import IProfilesByStop from "./CSA/data-structure/stops/IProfilesByStop";
export default interface IJourneyExtractor {
- extractJourneys: (profilesByStop: IProfilesByStop, query: IResolvedQuery) => Promise>;
+ extractJourneys: (
+ profilesByStop: IProfilesByStop | IProfileByStop,
+ query: IResolvedQuery,
+ ) => Promise>;
}
diff --git a/src/planner/public-transport/JourneyExtractionPhase.ts b/src/planner/public-transport/JourneyExtractionPhase.ts
deleted file mode 100644
index 603882da..00000000
--- a/src/planner/public-transport/JourneyExtractionPhase.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * Signifies different phases of the journey extraction algorithm
- * Dependencies of [[IJourneyExtractor]] implementations can be tagged with one of these phases,
- * so the algorithm knows which dependency to use at which time.
- * This is especially useful when the algorithm could potentially use different implementations of
- * the same interface at each phase
- */
-enum JourneyExtractionPhase {
- Initial = "journeyExtractionInitial",
- Transfer = "journeyExtractionTransfer",
- Final = "journeyExtractionFinal",
-}
-
-export default JourneyExtractionPhase;
diff --git a/src/planner/public-transport/JourneyExtractorEarliestArrival.ts b/src/planner/public-transport/JourneyExtractorEarliestArrival.ts
new file mode 100644
index 00000000..30f9bc89
--- /dev/null
+++ b/src/planner/public-transport/JourneyExtractorEarliestArrival.ts
@@ -0,0 +1,92 @@
+import { AsyncIterator, EmptyIterator, SingletonIterator } from "asynciterator";
+import { inject, injectable } from "inversify";
+import Context from "../../Context";
+import ILocation from "../../interfaces/ILocation";
+import IPath from "../../interfaces/IPath";
+import IStep from "../../interfaces/IStep";
+import ILocationResolver from "../../query-runner/ILocationResolver";
+import IResolvedQuery from "../../query-runner/IResolvedQuery";
+import TYPES from "../../types";
+import Path from "../Path";
+import Step from "../Step";
+import IProfileByStop from "./CSA/data-structure/stops/IProfileByStop";
+import ITransferProfile from "./CSA/data-structure/stops/ITransferProfile";
+import IJourneyExtractor from "./IJourneyExtractor";
+
+/**
+ * Creates journeys based on the profiles and query from [[PublicTransportPlannerCSAProfile]].
+ * A journey is an [[IPath]] that consist of several [[IStep]]s.
+ * The [[JourneyExtractor]] takes care of initial, intermediate and final footpaths.
+ *
+ * @property bestArrivalTime Stores the best arrival time for each pair of departure-arrival stops.
+ */
+@injectable()
+export default class JourneyExtractorEarliestArrival implements IJourneyExtractor {
+ private readonly locationResolver: ILocationResolver;
+
+ private context: Context;
+
+ constructor(
+ @inject(TYPES.LocationResolver) locationResolver: ILocationResolver,
+ @inject(TYPES.Context)context?: Context,
+ ) {
+ this.locationResolver = locationResolver;
+ this.context = context;
+ }
+
+ public async extractJourneys(
+ profilesByStop: IProfileByStop,
+ query: IResolvedQuery,
+ ): Promise> {
+ const path: Path = Path.create();
+
+ const departureStopId: string = query.from[0].id;
+
+ let currentStopId: string = query.to[0].id;
+ let currentProfile: ITransferProfile = profilesByStop[currentStopId];
+
+ while (currentStopId !== departureStopId && (currentProfile.enterConnection || currentProfile.path)) {
+ const { enterConnection, exitConnection, path: profilePath } = currentProfile;
+
+ if (profilePath) {
+ currentStopId = profilePath.getStartLocationId();
+
+ if (currentStopId === departureStopId) {
+ const lastStep = path.steps[path.steps.length - 1];
+ const timeToAdd = lastStep.startTime.getTime() - profilePath.steps[0].stopTime.getTime();
+
+ profilePath.addTime(timeToAdd);
+ }
+
+ path.addPath(profilePath);
+ }
+
+ if (currentProfile.enterConnection && currentProfile.exitConnection) {
+ const enterLocation: ILocation = await this.locationResolver.resolve(enterConnection.departureStop);
+ const exitLocation: ILocation = await this.locationResolver.resolve(exitConnection.arrivalStop);
+
+ const step: IStep = Step.createFromConnections(
+ enterConnection,
+ exitConnection,
+ );
+
+ step.startLocation = enterLocation;
+ step.stopLocation = exitLocation;
+ path.addStep(step);
+
+ currentStopId = enterConnection.departureStop;
+ }
+
+ currentProfile = profilesByStop[currentStopId];
+ }
+
+ if (!path.steps.length) {
+ return new EmptyIterator();
+ }
+
+ path.reverse();
+
+ return new SingletonIterator(path);
+ }
+
+}
diff --git a/src/planner/public-transport/JourneyExtractorDefault.ts b/src/planner/public-transport/JourneyExtractorProfile.ts
similarity index 81%
rename from src/planner/public-transport/JourneyExtractorDefault.ts
rename to src/planner/public-transport/JourneyExtractorProfile.ts
index e987b17a..37eebe48 100644
--- a/src/planner/public-transport/JourneyExtractorDefault.ts
+++ b/src/planner/public-transport/JourneyExtractorProfile.ts
@@ -1,12 +1,13 @@
import { ArrayIterator, AsyncIterator } from "asynciterator";
import { inject, injectable } from "inversify";
+import Context from "../../Context";
+import TravelMode from "../../enums/TravelMode";
import IConnection from "../../fetcher/connections/IConnection";
import ILocation from "../../interfaces/ILocation";
import IPath from "../../interfaces/IPath";
import IStep from "../../interfaces/IStep";
import ILocationResolver from "../../query-runner/ILocationResolver";
import IResolvedQuery from "../../query-runner/IResolvedQuery";
-import TravelMode from "../../TravelMode";
import TYPES from "../../types";
import Path from "../Path";
import Step from "../Step";
@@ -24,13 +25,18 @@ import IJourneyExtractor from "./IJourneyExtractor";
* @property bestArrivalTime Stores the best arrival time for each pair of departure-arrival stops.
*/
@injectable()
-export default class JourneyExtractorDefault implements IJourneyExtractor {
+export default class JourneyExtractorProfile implements IJourneyExtractor {
private readonly locationResolver: ILocationResolver;
private bestArrivalTime: number[][] = [];
+ private context: Context;
- constructor(@inject(TYPES.LocationResolver) locationResolver: ILocationResolver) {
+ constructor(
+ @inject(TYPES.LocationResolver) locationResolver: ILocationResolver,
+ @inject(TYPES.Context)context?: Context,
+ ) {
this.locationResolver = locationResolver;
+ this.context = context;
}
public async extractJourneys(
@@ -71,13 +77,14 @@ export default class JourneyExtractorDefault implements IJourneyExtractor {
arrivalLocation,
transferProfile.arrivalTime,
);
+
} catch (e) {
- console.warn(e);
+ this.context.emitWarning(e);
}
}
}
-
}
+
return new ArrayIterator(paths.reverse());
}
@@ -125,7 +132,7 @@ export default class JourneyExtractorDefault implements IJourneyExtractor {
const path: Path = Path.create();
let currentTransferProfile: ITransferProfile = transferProfile;
- let departureTime: Date = new Date(transferProfile.departureTime);
+ let departureTime: number = transferProfile.departureTime;
let remainingTransfers: number = transfers;
@@ -139,18 +146,24 @@ export default class JourneyExtractorDefault implements IJourneyExtractor {
const exitLocation: ILocation = await this.locationResolver.resolve(exitConnection.arrivalStop);
// Initial or transfer footpath.
- const transferDepartureTime: Date = enterConnection.departureTime;
+ const transferDepartureTime: number = enterConnection.departureTime.getTime();
+
+ if (departureTime !== transferDepartureTime) {
+
+ let timeToSubtract = 0;
+ if (path.steps.length > 0) {
+ timeToSubtract = departureTime - path.steps[path.steps.length - 1].stopTime.getTime();
+ }
- if (departureTime.getTime() !== transferDepartureTime.getTime()) {
const footpath: IStep = Step.create(
currentLocation,
enterLocation,
TravelMode.Walking,
{
- minimum: transferDepartureTime.getTime() - departureTime.getTime(),
+ minimum: transferDepartureTime - departureTime,
},
- departureTime,
- transferDepartureTime,
+ new Date(departureTime - timeToSubtract),
+ new Date(transferDepartureTime - timeToSubtract),
);
path.addStep(footpath);
@@ -173,17 +186,17 @@ export default class JourneyExtractorDefault implements IJourneyExtractor {
// Get next profile based on the arrival time at the current location.
if (remainingTransfers >= 0) {
const currentProfiles: IProfile[] = profilesByStop[currentLocation.id];
- let i: number = currentProfiles.length - 1;
+ let profileIndex: number = currentProfiles.length - 1;
- currentTransferProfile = currentProfiles[i].transferProfiles[remainingTransfers];
- departureTime = new Date(currentTransferProfile.departureTime);
+ currentTransferProfile = currentProfiles[profileIndex].transferProfiles[remainingTransfers];
+ departureTime = currentTransferProfile.departureTime;
- while (i >= 0 && departureTime < exitConnection.arrivalTime) {
- currentTransferProfile = currentProfiles[--i].transferProfiles[remainingTransfers];
- departureTime = new Date(currentTransferProfile.departureTime);
+ while (profileIndex >= 0 && departureTime < exitConnection.arrivalTime.getTime()) {
+ currentTransferProfile = currentProfiles[--profileIndex].transferProfiles[remainingTransfers];
+ departureTime = currentTransferProfile.departureTime;
}
- if (i === -1) {
+ if (profileIndex === -1) {
// This should never happen.
return Promise.reject("Can't find next connection");
}
diff --git a/src/planner/road/RoadPlannerBirdsEye.ts b/src/planner/road/RoadPlannerBirdsEye.ts
index 06cc4f09..fdf23eed 100644
--- a/src/planner/road/RoadPlannerBirdsEye.ts
+++ b/src/planner/road/RoadPlannerBirdsEye.ts
@@ -1,20 +1,27 @@
import { ArrayIterator, AsyncIterator } from "asynciterator";
import { injectable } from "inversify";
+import TravelMode from "../../enums/TravelMode";
import ILocation from "../../interfaces/ILocation";
import IPath from "../../interfaces/IPath";
import IProbabilisticValue from "../../interfaces/IProbabilisticValue";
-import { DurationMs, SpeedkmH } from "../../interfaces/units";
+import { DurationMs, SpeedKmH } from "../../interfaces/units";
import IResolvedQuery from "../../query-runner/IResolvedQuery";
-import TravelMode from "../../TravelMode";
import Geo from "../../util/Geo";
import Units from "../../util/Units";
+import Path from "../Path";
import IRoadPlanner from "./IRoadPlanner";
@injectable()
export default class RoadPlannerBirdsEye implements IRoadPlanner {
public async plan(query: IResolvedQuery): Promise> {
- const { from: fromLocations, to: toLocations, minimumWalkingSpeed, maximumWalkingSpeed} = query;
+ const {
+ from: fromLocations,
+ to: toLocations,
+ minimumWalkingSpeed,
+ maximumWalkingSpeed,
+ maximumWalkingDuration,
+ } = query;
const paths = [];
@@ -22,7 +29,18 @@ export default class RoadPlannerBirdsEye implements IRoadPlanner {
for (const from of fromLocations) {
for (const to of toLocations) {
- paths.push(this.getPathBetweenLocations(from, to, minimumWalkingSpeed, maximumWalkingSpeed));
+
+ const path = this.getPathBetweenLocations(
+ from,
+ to,
+ minimumWalkingSpeed,
+ maximumWalkingSpeed,
+ maximumWalkingDuration,
+ );
+
+ if (path) {
+ paths.push(path);
+ }
}
}
}
@@ -33,8 +51,9 @@ export default class RoadPlannerBirdsEye implements IRoadPlanner {
private getPathBetweenLocations(
from: ILocation,
to: ILocation,
- minWalkingSpeed: SpeedkmH,
- maxWalkingSpeed: SpeedkmH,
+ minWalkingSpeed: SpeedKmH,
+ maxWalkingSpeed: SpeedKmH,
+ maxWalkingDuration: DurationMs,
): IPath {
const distance = Geo.getDistanceBetweenLocations(from, to);
@@ -47,14 +66,16 @@ export default class RoadPlannerBirdsEye implements IRoadPlanner {
average: (minDuration + maxDuration) / 2,
};
- return {
- steps: [{
- startLocation: from,
- stopLocation: to,
- duration,
- distance,
- travelMode: TravelMode.Walking,
- }],
- };
+ if (duration.maximum > maxWalkingDuration) {
+ return;
+ }
+
+ return new Path([{
+ startLocation: from,
+ stopLocation: to,
+ duration,
+ distance,
+ travelMode: TravelMode.Walking,
+ }]);
}
}
diff --git a/src/planner/stops/IReachableStopsFinder.ts b/src/planner/stops/IReachableStopsFinder.ts
index 5ce8deb7..99ed8105 100644
--- a/src/planner/stops/IReachableStopsFinder.ts
+++ b/src/planner/stops/IReachableStopsFinder.ts
@@ -1,16 +1,23 @@
+import ReachableStopsFinderMode from "../../enums/ReachableStopsFinderMode";
import IStop from "../../fetcher/stops/IStop";
-import { DurationMs, SpeedkmH } from "../../interfaces/units";
-import ReachableStopsFinderMode from "./ReachableStopsFinderMode";
+import { DurationMs, SpeedKmH } from "../../interfaces/units";
+/**
+ * A reachable stops finder searches for stops that are reachable "on foot" . This can mean e.g. a folding bike if
+ * that scenario is implemented by the registered [[IReachableStopsFinder]]
+ */
export default interface IReachableStopsFinder {
findReachableStops: (
sourceOrTargetStop: IStop,
mode: ReachableStopsFinderMode,
maximumDuration: DurationMs,
- minimumSpeed: SpeedkmH,
+ minimumSpeed: SpeedKmH,
) => Promise;
}
+/**
+ * An [[IReachableStop]] wraps an [[IStop]] instance and the estimated duration to get to that stop
+ */
export interface IReachableStop {
stop: IStop;
duration: DurationMs;
diff --git a/src/planner/stops/ReachableStopsFinderBirdsEye.test.ts b/src/planner/stops/ReachableStopsFinderBirdsEye.test.ts
index 86523581..9abf9ea5 100644
--- a/src/planner/stops/ReachableStopsFinderBirdsEye.test.ts
+++ b/src/planner/stops/ReachableStopsFinderBirdsEye.test.ts
@@ -1,9 +1,9 @@
import "jest";
import LDFetch from "ldfetch";
+import ReachableStopsFinderMode from "../../enums/ReachableStopsFinderMode";
import StopsFetcherLDFetch from "../../fetcher/stops/ld-fetch/StopsFetcherLDFetch";
import Units from "../../util/Units";
import ReachableStopsFinderBirdsEye from "./ReachableStopsFinderBirdsEye";
-import ReachableStopsFinderMode from "./ReachableStopsFinderMode";
const DE_LIJN_STOPS_URLS = [
"http://openplanner.ilabt.imec.be/delijn/Antwerpen/stops",
diff --git a/src/planner/stops/ReachableStopsFinderBirdsEye.ts b/src/planner/stops/ReachableStopsFinderBirdsEye.ts
index defb77a9..f306d005 100644
--- a/src/planner/stops/ReachableStopsFinderBirdsEye.ts
+++ b/src/planner/stops/ReachableStopsFinderBirdsEye.ts
@@ -1,13 +1,17 @@
import { inject, injectable } from "inversify";
+import ReachableStopsFinderMode from "../../enums/ReachableStopsFinderMode";
import IStop from "../../fetcher/stops/IStop";
import IStopsProvider from "../../fetcher/stops/IStopsProvider";
-import { DurationMs, SpeedkmH } from "../../interfaces/units";
+import { DurationMs, SpeedKmH } from "../../interfaces/units";
import TYPES from "../../types";
import Geo from "../../util/Geo";
import Units from "../../util/Units";
import IReachableStopsFinder, { IReachableStop } from "./IReachableStopsFinder";
-import ReachableStopsFinderMode from "./ReachableStopsFinderMode";
+/**
+ * This [[IReachableStopsFinder]] determines its reachable stops based on the birds's-eye distance
+ * to the source or target stop.
+ */
@injectable()
export default class ReachableStopsFinderBirdsEye implements IReachableStopsFinder {
private readonly stopsProvider: IStopsProvider;
@@ -22,24 +26,24 @@ export default class ReachableStopsFinderBirdsEye implements IReachableStopsFind
sourceOrTargetStop: IStop,
mode: ReachableStopsFinderMode,
maximumDuration: DurationMs,
- minimumSpeed: SpeedkmH,
+ minimumSpeed: SpeedKmH,
): Promise {
// Mode can be ignored since birds eye view distance is identical
- const allStops = await this.stopsProvider.getAllStops();
+ const reachableStops: IReachableStop[] = [{stop: sourceOrTargetStop, duration: 0}];
- return allStops.map((possibleTarget: IStop): IReachableStop => {
- if (possibleTarget.id === sourceOrTargetStop.id) {
- return {stop: sourceOrTargetStop, duration: 0};
- }
+ const allStops = await this.stopsProvider.getAllStops();
+ allStops.forEach((possibleTarget: IStop) => {
const distance = Geo.getDistanceBetweenStops(sourceOrTargetStop, possibleTarget);
const duration = Units.toDuration(distance, minimumSpeed);
if (duration <= maximumDuration) {
- return {stop: possibleTarget, duration};
+ reachableStops.push({stop: possibleTarget, duration});
}
- }).filter((reachableStop) => !!reachableStop);
+ });
+
+ return reachableStops;
}
}
diff --git a/src/planner/stops/ReachableStopsFinderBirdsEyeCached.ts b/src/planner/stops/ReachableStopsFinderBirdsEyeCached.ts
index cc290197..2f474856 100644
--- a/src/planner/stops/ReachableStopsFinderBirdsEyeCached.ts
+++ b/src/planner/stops/ReachableStopsFinderBirdsEyeCached.ts
@@ -1,12 +1,16 @@
import { inject, injectable } from "inversify";
+import ReachableStopsFinderMode from "../../enums/ReachableStopsFinderMode";
import IStop from "../../fetcher/stops/IStop";
import IStopsProvider from "../../fetcher/stops/IStopsProvider";
-import { DurationMs, SpeedkmH } from "../../interfaces/units";
+import { DurationMs, SpeedKmH } from "../../interfaces/units";
import TYPES from "../../types";
import IReachableStopsFinder, { IReachableStop } from "./IReachableStopsFinder";
import ReachableStopsFinderBirdsEye from "./ReachableStopsFinderBirdsEye";
-import ReachableStopsFinderMode from "./ReachableStopsFinderMode";
+/**
+ * This [[IReachableStopsFinder]] forms a caching layer in front of a [[ReachableStopsFinderBirdsEye]] instance,
+ * so all the distances don't have to be calculated repeatedly
+ */
@injectable()
export default class ReachableStopsFinderBirdsEyeCached implements IReachableStopsFinder {
private readonly reachableStopsFinder: ReachableStopsFinderBirdsEye;
@@ -23,7 +27,7 @@ export default class ReachableStopsFinderBirdsEyeCached implements IReachableSto
sourceOrTargetStop: IStop,
mode: ReachableStopsFinderMode,
maximumDuration: DurationMs,
- minimumSpeed: SpeedkmH,
+ minimumSpeed: SpeedKmH,
): Promise {
// Mode can be ignored since birds eye view distance is identical
diff --git a/src/planner/stops/ReachableStopsFinderOnlySelf.ts b/src/planner/stops/ReachableStopsFinderOnlySelf.ts
new file mode 100644
index 00000000..dd23ffa5
--- /dev/null
+++ b/src/planner/stops/ReachableStopsFinderOnlySelf.ts
@@ -0,0 +1,23 @@
+import { injectable } from "inversify";
+import ReachableStopsFinderMode from "../../enums/ReachableStopsFinderMode";
+import IStop from "../../fetcher/stops/IStop";
+import { DurationMs, SpeedKmH } from "../../interfaces/units";
+import IReachableStopsFinder, { IReachableStop } from "./IReachableStopsFinder";
+
+/**
+ * This [[IReachableStopsFinder]] just returns the passed source or target stop.
+ *
+ * This can be a valid strategy to optimize speed if the user doesn't want to travel by foot to another stop
+ */
+@injectable()
+export default class ReachableStopsFinderOnlySelf implements IReachableStopsFinder {
+
+ public async findReachableStops(
+ sourceOrTargetStop: IStop,
+ mode: ReachableStopsFinderMode,
+ maximumDuration: DurationMs,
+ minimumSpeed: SpeedKmH,
+ ): Promise {
+ return [{ stop: sourceOrTargetStop, duration: 0 }];
+ }
+}
diff --git a/src/planner/stops/ReachableStopsFinderRoadPlanner.test.ts b/src/planner/stops/ReachableStopsFinderRoadPlanner.test.ts
index e567b4a9..5d83f7b2 100644
--- a/src/planner/stops/ReachableStopsFinderRoadPlanner.test.ts
+++ b/src/planner/stops/ReachableStopsFinderRoadPlanner.test.ts
@@ -1,9 +1,9 @@
import "jest";
import LDFetch from "ldfetch";
+import ReachableStopsFinderMode from "../../enums/ReachableStopsFinderMode";
import StopsFetcherLDFetch from "../../fetcher/stops/ld-fetch/StopsFetcherLDFetch";
import Units from "../../util/Units";
import RoadPlannerBirdsEye from "../road/RoadPlannerBirdsEye";
-import ReachableStopsFinderMode from "./ReachableStopsFinderMode";
import ReachableStopsFinderRoadPlanner from "./ReachableStopsFinderRoadPlanner";
const ldFetch = new LDFetch({ headers: { Accept: "application/ld+json" } });
@@ -20,7 +20,7 @@ test("[ReachableStopsFinderRoadPlanner] reachable stops", async () => {
expect(sourceStop).toBeDefined();
- // Get reachable stops in 50 km (10h at 5km/h)
+ // Get reachable stops in 5 km (1h at 5km/h)
const reachableStops = await reachableStopsFinder.findReachableStops(
sourceStop,
ReachableStopsFinderMode.Source,
diff --git a/src/planner/stops/ReachableStopsFinderRoadPlanner.ts b/src/planner/stops/ReachableStopsFinderRoadPlanner.ts
index 9c9206dc..c1327d83 100644
--- a/src/planner/stops/ReachableStopsFinderRoadPlanner.ts
+++ b/src/planner/stops/ReachableStopsFinderRoadPlanner.ts
@@ -1,17 +1,24 @@
import { AsyncIterator } from "asynciterator";
import { inject, injectable } from "inversify";
+import ReachableStopsFinderMode from "../../enums/ReachableStopsFinderMode";
import IStop from "../../fetcher/stops/IStop";
import IStopsProvider from "../../fetcher/stops/IStopsProvider";
import ILocation from "../../interfaces/ILocation";
import IPath from "../../interfaces/IPath";
-import { DurationMs, SpeedkmH } from "../../interfaces/units";
+import { DurationMs, SpeedKmH } from "../../interfaces/units";
import IResolvedQuery from "../../query-runner/IResolvedQuery";
import TYPES from "../../types";
+import Geo from "../../util/Geo";
import Iterators from "../../util/Iterators";
+import Units from "../../util/Units";
import IRoadPlanner from "../road/IRoadPlanner";
import IReachableStopsFinder, { IReachableStop } from "./IReachableStopsFinder";
-import ReachableStopsFinderMode from "./ReachableStopsFinderMode";
+/**
+ * This [[IReachableStopsFinder]] uses the registered [[IRoadPlanner]] to find reachable stops.
+ * It makes an initial selection of stops based on bird's-eye distance, after which a road planner query gets executed
+ * for each of these stops.
+ */
@injectable()
export default class ReachableStopsFinderRoadPlanner implements IReachableStopsFinder {
private readonly stopsProvider: IStopsProvider;
@@ -29,7 +36,7 @@ export default class ReachableStopsFinderRoadPlanner implements IReachableStopsF
sourceOrTargetStop: IStop,
mode: ReachableStopsFinderMode,
maximumDuration: DurationMs,
- minimumSpeed: SpeedkmH,
+ minimumSpeed: SpeedKmH,
): Promise {
const minimumDepartureTime = new Date();
@@ -45,10 +52,21 @@ export default class ReachableStopsFinderRoadPlanner implements IReachableStopsF
minimumWalkingSpeed: minimumSpeed,
};
- const allStops = await this.stopsProvider.getAllStops();
+ const allStops: IStop[] = await this.stopsProvider.getAllStops();
+
+ const stopsInsideCircleArea: IStop[] = [];
+ for (const stop of allStops) {
+ const distance = Geo.getDistanceBetweenStops(sourceOrTargetStop, stop);
+ const duration = Units.toDuration(distance, minimumSpeed);
+
+ if (duration <= maximumDuration) {
+ stopsInsideCircleArea.push(stop);
+ }
+ }
+
const reachableStops: IReachableStop[] = [{stop: sourceOrTargetStop, duration: 0}];
- await Promise.all(allStops.map(async (possibleTarget: IStop) => {
+ await Promise.all(stopsInsideCircleArea.map(async (possibleTarget: IStop) => {
if (possibleTarget.id !== sourceOrTargetStop.id) {
const query = Object.assign({}, baseQuery, {
diff --git a/src/planner/stops/ReachableStopsFinderRoadPlannerCached.ts b/src/planner/stops/ReachableStopsFinderRoadPlannerCached.ts
index 9a475f07..f373f29a 100644
--- a/src/planner/stops/ReachableStopsFinderRoadPlannerCached.ts
+++ b/src/planner/stops/ReachableStopsFinderRoadPlannerCached.ts
@@ -1,13 +1,17 @@
import { inject, injectable } from "inversify";
+import ReachableStopsFinderMode from "../../enums/ReachableStopsFinderMode";
import IStop from "../../fetcher/stops/IStop";
import IStopsProvider from "../../fetcher/stops/IStopsProvider";
-import { DurationMs, SpeedkmH } from "../../interfaces/units";
+import { DurationMs, SpeedKmH } from "../../interfaces/units";
import TYPES from "../../types";
import IRoadPlanner from "../road/IRoadPlanner";
import IReachableStopsFinder, { IReachableStop } from "./IReachableStopsFinder";
-import ReachableStopsFinderMode from "./ReachableStopsFinderMode";
import ReachableStopsFinderRoadPlanner from "./ReachableStopsFinderRoadPlanner";
+/**
+ * This [[IReachableStopsFinder]] forms a caching layer in front of a [[ReachableStopsFinderRoadPlanner]] instance,
+ * so all the queries don't have to be executed repeatedly
+ */
@injectable()
export default class ReachableStopsFinderRoadPlannerCached implements IReachableStopsFinder {
private readonly reachableStopsFinder: ReachableStopsFinderRoadPlanner;
@@ -25,7 +29,7 @@ export default class ReachableStopsFinderRoadPlannerCached implements IReachable
sourceOrTargetStop: IStop,
mode: ReachableStopsFinderMode,
maximumDuration: DurationMs,
- minimumSpeed: SpeedkmH,
+ minimumSpeed: SpeedKmH,
): Promise {
const cacheKey = `${sourceOrTargetStop.id} ${mode} ${maximumDuration} ${minimumSpeed}`;
diff --git a/src/query-runner/ILocationResolver.ts b/src/query-runner/ILocationResolver.ts
index a4f4f93c..1115a20e 100644
--- a/src/query-runner/ILocationResolver.ts
+++ b/src/query-runner/ILocationResolver.ts
@@ -1,6 +1,9 @@
import IStop from "../fetcher/stops/IStop";
import ILocation from "../interfaces/ILocation";
+/**
+ * A location resolver turns an [[ILocation]], [[IStop]] or a string into an [[ILocation]]
+ */
export default interface ILocationResolver {
resolve: (location: ILocation | IStop | string) => Promise;
}
diff --git a/src/query-runner/IQueryRunner.ts b/src/query-runner/IQueryRunner.ts
index 6103a303..c245f3ba 100644
--- a/src/query-runner/IQueryRunner.ts
+++ b/src/query-runner/IQueryRunner.ts
@@ -2,7 +2,10 @@ import { AsyncIterator } from "asynciterator";
import IPath from "../interfaces/IPath";
import IQuery from "../interfaces/IQuery";
+/**
+ * A query runner has a `run` method that turns an [[IQuery]] into an AsyncIterator of [[IPath]] instances.
+ * It does this by executing one or more algorithms on the query, depending on the implementation.
+ */
export default interface IQueryRunner {
-
run(query: IQuery): Promise>;
}
diff --git a/src/query-runner/IResolvedQuery.ts b/src/query-runner/IResolvedQuery.ts
index 210fe29f..f8f625dd 100644
--- a/src/query-runner/IResolvedQuery.ts
+++ b/src/query-runner/IResolvedQuery.ts
@@ -1,6 +1,11 @@
import ILocation from "../interfaces/ILocation";
-import { DurationMs, SpeedkmH } from "../interfaces/units";
+import { DurationMs, SpeedKmH } from "../interfaces/units";
+/**
+ * A resolved query is the result of transforming an input [[IQuery]] by adding defaults for any missing parameters and
+ * by resolving the endpoints (`from` and `to`). Classes using this resolved query don't have to worry about any missing
+ * or unrealistic parameters
+ */
export default interface IResolvedQuery {
from?: ILocation[];
to?: ILocation[];
@@ -8,10 +13,11 @@ export default interface IResolvedQuery {
maximumArrivalTime?: Date;
roadOnly?: boolean;
publicTransportOnly?: boolean;
- minimumWalkingSpeed?: SpeedkmH;
- maximumWalkingSpeed?: SpeedkmH;
+ minimumWalkingSpeed?: SpeedKmH;
+ maximumWalkingSpeed?: SpeedKmH;
maximumWalkingDuration?: DurationMs;
minimumTransferDuration?: DurationMs;
maximumTransferDuration?: DurationMs;
maximumTransfers?: number;
+ maximumTravelDuration?: DurationMs;
}
diff --git a/src/query-runner/LocationResolverConvenience.test.ts b/src/query-runner/LocationResolverConvenience.test.ts
new file mode 100644
index 00000000..3da63c76
--- /dev/null
+++ b/src/query-runner/LocationResolverConvenience.test.ts
@@ -0,0 +1,60 @@
+import "jest";
+import LDFetch from "ldfetch";
+import StopsFetcherLDFetch from "../fetcher/stops/ld-fetch/StopsFetcherLDFetch";
+import LocationResolverConvenience from "./LocationResolverConvenience";
+
+const ldFetch = new LDFetch({ headers: { Accept: "application/ld+json" } });
+
+const stopsFetcher = new StopsFetcherLDFetch(ldFetch);
+stopsFetcher.setAccessUrl("https://irail.be/stations/NMBS");
+
+const locationResolver = new LocationResolverConvenience(stopsFetcher);
+
+describe("[LocationResolverConvenience]", () => {
+
+ it("Input 'Kortrijk' (exact stop name)", async () => {
+
+ const location = await locationResolver.resolve("Kortrijk");
+
+ expect(location).toBeDefined();
+ expect(location.latitude).toBeCloseTo(50.82, 2);
+ });
+
+ it("Input 'Narnia' (non-existent stop name)", async () => {
+ expect.assertions(1);
+ expect(locationResolver.resolve(
+ "Narnia",
+ )).rejects.toBeDefined();
+ });
+
+ it("Input {id: 'http://...'}", async () => {
+
+ const location = await locationResolver.resolve(
+ { id: "http://irail.be/stations/NMBS/008896008" },
+ );
+
+ expect(location).toBeDefined();
+ expect(location.latitude).toBeCloseTo(50.82, 2);
+ });
+
+ it("Input 'http://...'", async () => {
+
+ const location = await locationResolver.resolve(
+ "http://irail.be/stations/NMBS/008896008",
+ );
+
+ expect(location).toBeDefined();
+ expect(location.latitude).toBeCloseTo(50.82, 2);
+ });
+
+ it("Input {longitude: ..., latitude: ...}", async () => {
+
+ const location = await locationResolver.resolve(
+ { latitude: 50.824506, longitude: 3.264549 },
+ );
+
+ expect(location).toBeDefined();
+ expect(location.latitude).toBeCloseTo(50.82, 2);
+ });
+
+});
diff --git a/src/query-runner/LocationResolverConvenience.ts b/src/query-runner/LocationResolverConvenience.ts
new file mode 100644
index 00000000..992bc859
--- /dev/null
+++ b/src/query-runner/LocationResolverConvenience.ts
@@ -0,0 +1,53 @@
+import { inject, injectable } from "inversify";
+import LocationResolverError from "../errors/LocationResolverError";
+import IStop from "../fetcher/stops/IStop";
+import IStopsProvider from "../fetcher/stops/IStopsProvider";
+import ILocation from "../interfaces/ILocation";
+import TYPES from "../types";
+import ILocationResolver from "./ILocationResolver";
+import LocationResolverDefault from "./LocationResolverDefault";
+
+/**
+ * Location resolver that allows stop names as input
+ * Falls back to LocationResolverDefault
+ */
+@injectable()
+export default class LocationResolverConvenience implements ILocationResolver {
+ private readonly stopsProvider: IStopsProvider;
+ private readonly defaultLocationResolver: ILocationResolver;
+
+ private allStops: IStop[];
+
+ constructor(
+ @inject(TYPES.StopsProvider) stopsProvider: IStopsProvider,
+ ) {
+ this.stopsProvider = stopsProvider;
+ this.defaultLocationResolver = new LocationResolverDefault(this.stopsProvider);
+ }
+
+ public async resolve(input: ILocation | IStop | string): Promise {
+
+ if (typeof input === "string" && !this.isId(input)) {
+
+ if (!this.allStops) {
+ this.allStops = await this.stopsProvider.getAllStops();
+ }
+
+ const matchingStop = this.allStops.find((stop: IStop) => stop.name === input);
+
+ if (matchingStop) {
+ return matchingStop;
+ }
+
+ return Promise.reject(
+ new LocationResolverError(`Location "${input}" is a string, but not an ID and not a valid stop name`),
+ );
+ }
+
+ return this.defaultLocationResolver.resolve(input);
+ }
+
+ private isId(testString: string): boolean {
+ return testString.indexOf("http://") === 0 || testString.indexOf("https://") === 0;
+ }
+}
diff --git a/src/query-runner/LocationResolverDefault.ts b/src/query-runner/LocationResolverDefault.ts
index 8283d19e..88513161 100644
--- a/src/query-runner/LocationResolverDefault.ts
+++ b/src/query-runner/LocationResolverDefault.ts
@@ -1,10 +1,18 @@
import { inject, injectable } from "inversify";
+import LocationResolverError from "../errors/LocationResolverError";
import IStop from "../fetcher/stops/IStop";
import IStopsProvider from "../fetcher/stops/IStopsProvider";
import ILocation from "../interfaces/ILocation";
import TYPES from "../types";
import ILocationResolver from "./ILocationResolver";
+/**
+ * This default location resolver resolves [[ILocation]] instances by their `id` (`http(s)://...`)
+ *
+ * If only an `id` string is passed, it returns an [[ILocation]] with all available information.
+ *
+ * If an incomplete [[ILocation]] (but with an `id`) is passed, it gets supplemented as well.
+ */
@injectable()
export default class LocationResolverDefault implements ILocationResolver {
private readonly stopsProvider: IStopsProvider;
@@ -23,7 +31,7 @@ export default class LocationResolverDefault implements ILocationResolver {
return this.resolveById(input);
}
- return Promise.reject(`Location "${input}" is a string, but not an ID`);
+ return Promise.reject(new LocationResolverError(`Location "${input}" is a string, but not an ID`));
}
const location: ILocation = input;
@@ -37,7 +45,9 @@ export default class LocationResolverDefault implements ILocationResolver {
}
if (!hasCoords) {
- return Promise.reject(`Location "${JSON.stringify(input)}" should have latitude and longitude`);
+ return Promise.reject(
+ new LocationResolverError(`Location "${JSON.stringify(input)}" should have latitude and longitude`),
+ );
}
return location;
@@ -55,7 +65,7 @@ export default class LocationResolverDefault implements ILocationResolver {
};
}
- throw new Error(`No fetcher for id ${id}`);
+ return Promise.reject(new LocationResolverError(`No fetcher for id ${id}`));
}
private isId(testString: string): boolean {
diff --git a/src/query-runner/QueryRunnerDefault.ts b/src/query-runner/QueryRunnerDefault.ts
index d78e60cf..829e47da 100644
--- a/src/query-runner/QueryRunnerDefault.ts
+++ b/src/query-runner/QueryRunnerDefault.ts
@@ -1,6 +1,8 @@
import { AsyncIterator } from "asynciterator";
import { inject, injectable } from "inversify";
+import { cat } from "shelljs";
import Defaults from "../Defaults";
+import InvalidQueryError from "../errors/InvalidQueryError";
import ILocation from "../interfaces/ILocation";
import IPath from "../interfaces/IPath";
import IQuery from "../interfaces/IQuery";
@@ -11,6 +13,12 @@ import ILocationResolver from "./ILocationResolver";
import IQueryRunner from "./IQueryRunner";
import IResolvedQuery from "./IResolvedQuery";
+/**
+ * This default query runner only accepts public transport queries (`publicTransportOnly = true`).
+ * It uses the registered [[IPublicTransportPlanner]] to execute them.
+ *
+ * The default `minimumDepartureTime` is *now*. The default `maximumArrivalTime` is `minimumDepartureTime + 2 hours`.
+ */
@injectable()
export default class QueryRunnerDefault implements IQueryRunner {
private locationResolver: ILocationResolver;
@@ -31,7 +39,7 @@ export default class QueryRunnerDefault implements IQueryRunner {
return this.publicTransportPlanner.plan(resolvedQuery);
} else {
- return Promise.reject("Query not supported");
+ throw new InvalidQueryError("Query should have publicTransportOnly = true");
}
}
@@ -56,7 +64,8 @@ export default class QueryRunnerDefault implements IQueryRunner {
from, to,
minimumWalkingSpeed, maximumWalkingSpeed, walkingSpeed,
maximumWalkingDuration, maximumWalkingDistance,
- minimumTransferDuration, maximumTransferDuration, maximumTransfers,
+ minimumTransferDuration, maximumTransferDuration, maximumTransferDistance,
+ maximumTransfers,
minimumDepartureTime, maximumArrivalTime,
...other
} = query;
@@ -76,15 +85,25 @@ export default class QueryRunnerDefault implements IQueryRunner {
resolvedQuery.maximumArrivalTime = newMaximumArrivalTime;
}
- resolvedQuery.from = await this.resolveEndpoint(from);
- resolvedQuery.to = await this.resolveEndpoint(to);
+ try {
+ resolvedQuery.from = await this.resolveEndpoint(from);
+ resolvedQuery.to = await this.resolveEndpoint(to);
+
+ } catch (e) {
+ return Promise.reject(new InvalidQueryError(e));
+ }
+
resolvedQuery.minimumWalkingSpeed = minimumWalkingSpeed || walkingSpeed || Defaults.defaultMinimumWalkingSpeed;
resolvedQuery.maximumWalkingSpeed = maximumWalkingSpeed || walkingSpeed || Defaults.defaultMaximumWalkingSpeed;
+
resolvedQuery.maximumWalkingDuration = maximumWalkingDuration ||
Units.toDuration(maximumWalkingDistance, resolvedQuery.minimumWalkingSpeed) || Defaults.defaultWalkingDuration;
resolvedQuery.minimumTransferDuration = minimumTransferDuration || Defaults.defaultMinimumTransferDuration;
- resolvedQuery.maximumTransferDuration = maximumTransferDuration || Defaults.defaultMaximumTransferDuration;
+ resolvedQuery.maximumTransferDuration = maximumTransferDuration ||
+ Units.toDuration(maximumTransferDistance, resolvedQuery.minimumWalkingSpeed) ||
+ Defaults.defaultMaximumTransferDuration;
+
resolvedQuery.maximumTransfers = maximumTransfers || Defaults.defaultMaximumTransfers;
return resolvedQuery;
diff --git a/src/query-runner/earliest-arrival-first/LinearQueryIterator.ts b/src/query-runner/earliest-arrival-first/LinearQueryIterator.ts
new file mode 100644
index 00000000..3f6abd73
--- /dev/null
+++ b/src/query-runner/earliest-arrival-first/LinearQueryIterator.ts
@@ -0,0 +1,40 @@
+import { AsyncIterator } from "asynciterator";
+import { DurationMs } from "../../interfaces/units";
+import IResolvedQuery from "../IResolvedQuery";
+
+// Inspired by IntegerIterator
+export default class LinearQueryIterator extends AsyncIterator {
+ private readonly baseQuery: IResolvedQuery;
+ private timespan: DurationMs;
+ private index: number;
+ private readonly a: DurationMs;
+ private readonly b: DurationMs;
+
+ constructor(baseQuery: IResolvedQuery, a: DurationMs, b: DurationMs) {
+ super();
+
+ this.baseQuery = baseQuery;
+ this.index = 1;
+ this.a = a;
+ this.b = b;
+
+ this.timespan = a * this.index + b;
+
+ this.readable = true;
+ }
+
+ public read(): IResolvedQuery {
+ if (this.closed) {
+ return null;
+ }
+
+ const {minimumDepartureTime} = this.baseQuery;
+ const maximumArrivalTime = new Date(minimumDepartureTime.getTime() + this.timespan);
+
+ this.index++;
+ this.timespan = this.a * this.index + this.b;
+
+ return Object.assign({}, this.baseQuery, {maximumArrivalTime});
+ }
+
+}
diff --git a/src/query-runner/earliest-arrival-first/QueryRunnerEarliestArrivalFirst.test.ts b/src/query-runner/earliest-arrival-first/QueryRunnerEarliestArrivalFirst.test.ts
new file mode 100644
index 00000000..2d86bcc8
--- /dev/null
+++ b/src/query-runner/earliest-arrival-first/QueryRunnerEarliestArrivalFirst.test.ts
@@ -0,0 +1,111 @@
+import "jest";
+import LDFetch from "ldfetch";
+import Context from "../../Context";
+import TravelMode from "../../enums/TravelMode";
+import ConnectionsFetcherLazy from "../../fetcher/connections/lazy/ConnectionsFetcherLazy";
+import StopsFetcherLDFetch from "../../fetcher/stops/ld-fetch/StopsFetcherLDFetch";
+import IPath from "../../interfaces/IPath";
+import IStep from "../../interfaces/IStep";
+import CSAProfile from "../../planner/public-transport/CSAProfile";
+import JourneyExtractorProfile from "../../planner/public-transport/JourneyExtractorProfile";
+import ReachableStopsFinderBirdsEyeCached from "../../planner/stops/ReachableStopsFinderBirdsEyeCached";
+import Units from "../../util/Units";
+import LocationResolverDefault from "../LocationResolverDefault";
+import QueryRunnerEarliestArrivalFirst from "./QueryRunnerEarliestArrivalFirst";
+describe("[QueryRunnerExponential]", () => {
+ jest.setTimeout(100000);
+
+ let publicTransportResult;
+
+ const query = {
+ publicTransportOnly: true,
+ from: "http://irail.be/stations/NMBS/008896925", // Ingelmunster
+ to: "http://irail.be/stations/NMBS/008892007", // Ghent-Sint-Pieters
+ minimumDepartureTime: new Date(),
+ maximumTransferDuration: Units.fromHours(.5),
+ };
+
+ const createEarliestArrivalQueryRunner = () => {
+ const ldFetch = new LDFetch({ headers: { Accept: "application/ld+json" } });
+
+ const connectionFetcher = new ConnectionsFetcherLazy(ldFetch);
+ connectionFetcher.setTravelMode(TravelMode.Train);
+ connectionFetcher.setAccessUrl("https://graph.irail.be/sncb/connections");
+
+ const stopsFetcher = new StopsFetcherLDFetch(ldFetch);
+ stopsFetcher.setAccessUrl("https://irail.be/stations/NMBS");
+
+ const locationResolver = new LocationResolverDefault(stopsFetcher);
+ const reachableStopsFinder = new ReachableStopsFinderBirdsEyeCached(stopsFetcher);
+
+ const context = new Context();
+
+ const createJourneyExtractor = () => {
+ return new JourneyExtractorProfile(
+ locationResolver,
+ );
+ };
+
+ const createPlanner = () => {
+ return new CSAProfile(
+ connectionFetcher,
+ locationResolver,
+ reachableStopsFinder,
+ reachableStopsFinder,
+ reachableStopsFinder,
+ createJourneyExtractor(),
+ );
+ };
+
+ return new QueryRunnerEarliestArrivalFirst(
+ context,
+ connectionFetcher,
+ locationResolver,
+ createPlanner,
+ reachableStopsFinder,
+ reachableStopsFinder,
+ reachableStopsFinder,
+ );
+ };
+
+ const result: IPath[] = [];
+
+ beforeAll(async (done) => {
+
+ const queryRunner = createEarliestArrivalQueryRunner();
+
+ publicTransportResult = await queryRunner.run(query);
+
+ await publicTransportResult.take(3)
+ .on("data", (path: IPath) => {
+ result.push(path);
+ })
+ .on("end", () => {
+ done();
+ });
+ });
+
+ it("Correct departure and arrival stop", () => {
+ checkStops(result, query);
+ });
+});
+
+const checkStops = (result, query) => {
+ expect(result).toBeDefined();
+ expect(result.length).toBeGreaterThanOrEqual(1);
+
+ for (const path of result) {
+ expect(path.steps).toBeDefined();
+
+ expect(path.steps.length).toBeGreaterThanOrEqual(1);
+
+ let currentLocation = query.from;
+ path.steps.forEach((step: IStep) => {
+ expect(step).toBeDefined();
+ expect(currentLocation).toEqual(step.startLocation.id);
+ currentLocation = step.stopLocation.id;
+ });
+
+ expect(query.to).toEqual(currentLocation);
+ }
+};
diff --git a/src/query-runner/earliest-arrival-first/QueryRunnerEarliestArrivalFirst.ts b/src/query-runner/earliest-arrival-first/QueryRunnerEarliestArrivalFirst.ts
new file mode 100644
index 00000000..62527c1d
--- /dev/null
+++ b/src/query-runner/earliest-arrival-first/QueryRunnerEarliestArrivalFirst.ts
@@ -0,0 +1,200 @@
+import { AsyncIterator } from "asynciterator";
+import { PromiseProxyIterator } from "asynciterator-promiseproxy";
+import { inject, injectable, interfaces, tagged } from "inversify";
+import Context from "../../Context";
+import Defaults from "../../Defaults";
+import EventType from "../../enums/EventType";
+import ReachableStopsSearchPhase from "../../enums/ReachableStopsSearchPhase";
+import InvalidQueryError from "../../errors/InvalidQueryError";
+import IConnectionsProvider from "../../fetcher/connections/IConnectionsProvider";
+import ILocation from "../../interfaces/ILocation";
+import IPath from "../../interfaces/IPath";
+import IQuery from "../../interfaces/IQuery";
+import { DurationMs } from "../../interfaces/units";
+import Path from "../../planner/Path";
+import CSAEarliestArrival from "../../planner/public-transport/CSAEarliestArrival";
+import IPublicTransportPlanner from "../../planner/public-transport/IPublicTransportPlanner";
+import JourneyExtractorEarliestArrival from "../../planner/public-transport/JourneyExtractorEarliestArrival";
+import IReachableStopsFinder from "../../planner/stops/IReachableStopsFinder";
+import TYPES from "../../types";
+import FilterUniqueIterator from "../../util/iterators/FilterUniqueIterator";
+import FlatMapIterator from "../../util/iterators/FlatMapIterator";
+import Units from "../../util/Units";
+import ILocationResolver from "../ILocationResolver";
+import IQueryRunner from "../IQueryRunner";
+import IResolvedQuery from "../IResolvedQuery";
+import LinearQueryIterator from "./LinearQueryIterator";
+
+@injectable()
+export default class QueryRunnerEarliestArrivalFirst implements IQueryRunner {
+
+ private readonly context: Context;
+ private readonly connectionsProvider: IConnectionsProvider;
+ private readonly locationResolver: ILocationResolver;
+ private readonly publicTransportPlannerFactory: interfaces.Factory;
+
+ private readonly journeyExtractorEarliestArrival: JourneyExtractorEarliestArrival;
+
+ private readonly initialReachableStopsFinder: IReachableStopsFinder;
+ private readonly transferReachableStopsFinder: IReachableStopsFinder;
+ private readonly finalReachableStopsFinder: IReachableStopsFinder;
+
+ constructor(
+ @inject(TYPES.Context)
+ context: Context,
+ @inject(TYPES.ConnectionsProvider)
+ connectionsProvider: IConnectionsProvider,
+ @inject(TYPES.LocationResolver)
+ locationResolver: ILocationResolver,
+ @inject(TYPES.PublicTransportPlannerFactory)
+ publicTransportPlannerFactory: interfaces.Factory,
+ @inject(TYPES.ReachableStopsFinder)
+ @tagged("phase", ReachableStopsSearchPhase.Initial)
+ initialReachableStopsFinder: IReachableStopsFinder,
+ @inject(TYPES.ReachableStopsFinder)
+ @tagged("phase", ReachableStopsSearchPhase.Transfer)
+ transferReachableStopsFinder: IReachableStopsFinder,
+ @inject(TYPES.ReachableStopsFinder)
+ @tagged("phase", ReachableStopsSearchPhase.Final)
+ finalReachableStopsFinder: IReachableStopsFinder,
+ ) {
+ this.context = context;
+ this.connectionsProvider = connectionsProvider;
+ this.locationResolver = locationResolver;
+ this.publicTransportPlannerFactory = publicTransportPlannerFactory;
+
+ this.initialReachableStopsFinder = initialReachableStopsFinder;
+ this.transferReachableStopsFinder = transferReachableStopsFinder;
+ this.finalReachableStopsFinder = finalReachableStopsFinder;
+
+ this.journeyExtractorEarliestArrival = new JourneyExtractorEarliestArrival(
+ locationResolver,
+ context,
+ );
+ }
+
+ public async run(query: IQuery): Promise> {
+ const baseQuery: IResolvedQuery = await this.resolveBaseQuery(query);
+
+ if (baseQuery.publicTransportOnly) {
+
+ const earliestArrivalPlanner = new CSAEarliestArrival(
+ this.connectionsProvider,
+ this.locationResolver,
+ this.initialReachableStopsFinder,
+ this.transferReachableStopsFinder,
+ this.finalReachableStopsFinder,
+ this.journeyExtractorEarliestArrival,
+ this.context,
+ );
+
+ const earliestArrivalIterator = await earliestArrivalPlanner.plan(baseQuery);
+
+ const path: IPath = await new Promise((resolve) => {
+ earliestArrivalIterator
+ .take(1)
+ .on("data", (result: IPath) => {
+ resolve(result);
+ })
+ .on("end", () => {
+ resolve(null);
+ });
+ });
+
+ if (path === null && this.context) {
+ this.context.emit(EventType.AbortQuery, "This query has no results");
+ }
+
+ let initialTimeSpan: DurationMs = Units.fromHours(1);
+ let travelDuration: DurationMs;
+
+ if (path && path.steps && path.steps.length > 0) {
+ const firstStep = path.steps[0];
+ const lastStep = path.steps[path.steps.length - 1];
+
+ initialTimeSpan = lastStep.stopTime.getTime() - baseQuery.minimumDepartureTime.getTime();
+ travelDuration = lastStep.stopTime.getTime() - firstStep.startTime.getTime();
+ }
+
+ baseQuery.maximumTravelDuration = travelDuration * 2;
+
+ const queryIterator = new LinearQueryIterator(baseQuery, Units.fromHours(1.5), initialTimeSpan);
+
+ const subQueryIterator = new FlatMapIterator(
+ queryIterator,
+ this.runSubquery.bind(this),
+ );
+
+ const prependedIterator = subQueryIterator.prepend([path]);
+
+ return new FilterUniqueIterator(prependedIterator, Path.compareEquals);
+
+ } else {
+ throw new InvalidQueryError("Query should have publicTransportOnly = true");
+ }
+ }
+
+ private runSubquery(query: IResolvedQuery): AsyncIterator {
+ this.context.emit(EventType.SubQuery, query);
+
+ const planner = this.publicTransportPlannerFactory() as IPublicTransportPlanner;
+
+ return new PromiseProxyIterator(() => planner.plan(query));
+ }
+
+ private async resolveEndpoint(endpoint: string | string[] | ILocation | ILocation[]): Promise {
+
+ if (Array.isArray(endpoint)) {
+ const promises = (endpoint as Array)
+ .map((singleEndpoint: string | ILocation) =>
+ this.locationResolver.resolve(singleEndpoint),
+ );
+
+ return await Promise.all(promises);
+
+ } else {
+ return [await this.locationResolver.resolve(endpoint)];
+ }
+ }
+
+ private async resolveBaseQuery(query: IQuery): Promise {
+ // tslint:disable:trailing-comma
+ const {
+ from, to,
+ minimumWalkingSpeed, maximumWalkingSpeed, walkingSpeed,
+ maximumWalkingDuration, maximumWalkingDistance,
+ minimumTransferDuration, maximumTransferDuration, maximumTransferDistance,
+ maximumTransfers,
+ minimumDepartureTime,
+ ...other
+ } = query;
+ // tslint:enable:trailing-comma
+
+ const resolvedQuery: IResolvedQuery = Object.assign({}, other);
+
+ resolvedQuery.minimumDepartureTime = minimumDepartureTime || new Date();
+
+ try {
+ resolvedQuery.from = await this.resolveEndpoint(from);
+ resolvedQuery.to = await this.resolveEndpoint(to);
+
+ } catch (e) {
+ return Promise.reject(new InvalidQueryError(e));
+ }
+
+ resolvedQuery.minimumWalkingSpeed = minimumWalkingSpeed || walkingSpeed || Defaults.defaultMinimumWalkingSpeed;
+ resolvedQuery.maximumWalkingSpeed = maximumWalkingSpeed || walkingSpeed || Defaults.defaultMaximumWalkingSpeed;
+
+ resolvedQuery.maximumWalkingDuration = maximumWalkingDuration ||
+ Units.toDuration(maximumWalkingDistance, resolvedQuery.minimumWalkingSpeed) || Defaults.defaultWalkingDuration;
+
+ resolvedQuery.minimumTransferDuration = minimumTransferDuration || Defaults.defaultMinimumTransferDuration;
+ resolvedQuery.maximumTransferDuration = maximumTransferDuration ||
+ Units.toDuration(maximumTransferDistance, resolvedQuery.minimumWalkingSpeed) ||
+ Defaults.defaultMaximumTransferDuration;
+
+ resolvedQuery.maximumTransfers = maximumTransfers || Defaults.defaultMaximumTransfers;
+
+ return resolvedQuery;
+ }
+}
diff --git a/src/query-runner/exponential/ExponentialQueryIterator.ts b/src/query-runner/exponential/ExponentialQueryIterator.ts
index 5f874b8f..a5525a2b 100644
--- a/src/query-runner/exponential/ExponentialQueryIterator.ts
+++ b/src/query-runner/exponential/ExponentialQueryIterator.ts
@@ -2,7 +2,10 @@ import { AsyncIterator } from "asynciterator";
import { DurationMs } from "../../interfaces/units";
import IResolvedQuery from "../IResolvedQuery";
-// Inspired by IntegerIterator
+/**
+ * This AsyncIterator emits [[IResolvedQuery]] instances with exponentially increasing `maximumArrivalTime`.
+ * For each emitted query, the time frame gets doubled (x2).
+ */
export default class ExponentialQueryIterator extends AsyncIterator {
private readonly baseQuery: IResolvedQuery;
private timespan: DurationMs;
@@ -28,5 +31,4 @@ export default class ExponentialQueryIterator extends AsyncIterator {
-
- private store: Path[];
-
- constructor(source: AsyncIterator) {
- super(source, {
- maxBufferSize: 1,
- autoStart: false,
- });
-
- this.store = [];
- }
-
- public _filter(path: IPath): boolean {
-
- const isUnique = !this.store
- .some((storedPath: Path) => storedPath.equals(path));
-
- if (isUnique) {
- this.store.push(path as Path);
- }
-
- return isUnique;
- }
-}
diff --git a/src/query-runner/exponential/QueryRunnerExponential.test.ts b/src/query-runner/exponential/QueryRunnerExponential.test.ts
index 06985aef..32e71ada 100644
--- a/src/query-runner/exponential/QueryRunnerExponential.test.ts
+++ b/src/query-runner/exponential/QueryRunnerExponential.test.ts
@@ -1,14 +1,14 @@
import "jest";
import LDFetch from "ldfetch";
import Context from "../../Context";
-import ConnectionsFetcherLazy from "../../fetcher/connections/ld-fetch/ConnectionsFetcherLazy";
+import TravelMode from "../../enums/TravelMode";
+import ConnectionsFetcherLazy from "../../fetcher/connections/lazy/ConnectionsFetcherLazy";
import StopsFetcherLDFetch from "../../fetcher/stops/ld-fetch/StopsFetcherLDFetch";
import IPath from "../../interfaces/IPath";
import IStep from "../../interfaces/IStep";
-import JourneyExtractorDefault from "../../planner/public-transport/JourneyExtractorDefault";
-import PublicTransportPlannerCSAProfile from "../../planner/public-transport/PublicTransportPlannerCSAProfile";
+import CSAProfile from "../../planner/public-transport/CSAProfile";
+import JourneyExtractorProfile from "../../planner/public-transport/JourneyExtractorProfile";
import ReachableStopsFinderBirdsEyeCached from "../../planner/stops/ReachableStopsFinderBirdsEyeCached";
-import TravelMode from "../../TravelMode";
import Units from "../../util/Units";
import LocationResolverDefault from "../LocationResolverDefault";
import QueryRunnerExponential from "./QueryRunnerExponential";
@@ -42,13 +42,13 @@ describe("[QueryRunnerExponential]", () => {
const context = new Context();
const createJourneyExtractor = () => {
- return new JourneyExtractorDefault(
+ return new JourneyExtractorProfile(
locationResolver,
);
};
const createPlanner = () => {
- return new PublicTransportPlannerCSAProfile(
+ return new CSAProfile(
connectionFetcher,
locationResolver,
reachableStopsFinder,
diff --git a/src/query-runner/exponential/QueryRunnerExponential.ts b/src/query-runner/exponential/QueryRunnerExponential.ts
index cde5f40b..c834d12e 100644
--- a/src/query-runner/exponential/QueryRunnerExponential.ts
+++ b/src/query-runner/exponential/QueryRunnerExponential.ts
@@ -1,27 +1,42 @@
import { AsyncIterator } from "asynciterator";
+import { PromiseProxyIterator } from "asynciterator-promiseproxy";
import { inject, injectable, interfaces } from "inversify";
import Context from "../../Context";
import Defaults from "../../Defaults";
-import EventType from "../../EventType";
+import EventType from "../../enums/EventType";
+import InvalidQueryError from "../../errors/InvalidQueryError";
import ILocation from "../../interfaces/ILocation";
import IPath from "../../interfaces/IPath";
import IQuery from "../../interfaces/IQuery";
+import Path from "../../planner/Path";
import IPublicTransportPlanner from "../../planner/public-transport/IPublicTransportPlanner";
import TYPES from "../../types";
-import Emiterator from "../../util/iterators/Emiterator";
+import FilterUniqueIterator from "../../util/iterators/FilterUniqueIterator";
+import FlatMapIterator from "../../util/iterators/FlatMapIterator";
import Units from "../../util/Units";
import ILocationResolver from "../ILocationResolver";
import IQueryRunner from "../IQueryRunner";
import IResolvedQuery from "../IResolvedQuery";
import ExponentialQueryIterator from "./ExponentialQueryIterator";
-import FilterUniquePathsIterator from "./FilterUniquePathsIterator";
-import SubqueryIterator from "./SubqueryIterator";
+/**
+ * This exponential query runner only accepts public transport queries (`publicTransportOnly = true`).
+ * It uses the registered [[IPublicTransportPlanner]] to execute them.
+ *
+ * To improve the user perceived performance, the query gets split into subqueries
+ * with exponentially increasing time frames:
+ *
+ * ```
+ * minimumDepartureTime + 15 minutes, 30 minutes, 60 minutes, 120 minutes...
+ * ```
+ *
+ * In the current implementation, the `maximumArrivalTime` is ignored
+ */
@injectable()
export default class QueryRunnerExponential implements IQueryRunner {
- private locationResolver: ILocationResolver;
- private publicTransportPlannerFactory: interfaces.Factory;
- private context: Context;
+ private readonly locationResolver: ILocationResolver;
+ private readonly publicTransportPlannerFactory: interfaces.Factory;
+ private readonly context: Context;
constructor(
@inject(TYPES.Context)
@@ -40,33 +55,27 @@ export default class QueryRunnerExponential implements IQueryRunner {
const baseQuery: IResolvedQuery = await this.resolveBaseQuery(query);
if (baseQuery.publicTransportOnly) {
-
const queryIterator = new ExponentialQueryIterator(baseQuery, 15 * 60 * 1000);
- // const emitQueryIterator = new Emiterator(
- // queryIterator,
- // this.context,
- // EventType.QueryExponential,
- // );
- const subqueryIterator = new SubqueryIterator(
+ const subqueryIterator = new FlatMapIterator(
queryIterator,
this.runSubquery.bind(this),
);
- return new FilterUniquePathsIterator(subqueryIterator);
+ return new FilterUniqueIterator(subqueryIterator, Path.compareEquals);
} else {
- return Promise.reject("Query not supported");
+ throw new InvalidQueryError("Query should have publicTransportOnly = true");
}
}
- private async runSubquery(query: IResolvedQuery): Promise> {
+ private runSubquery(query: IResolvedQuery): AsyncIterator {
// TODO investigate if publicTransportPlanner can be reused or reuse some of its aggregated data
- this.context.emit(EventType.QueryExponential, query);
+ this.context.emit(EventType.SubQuery, query);
const planner = this.publicTransportPlannerFactory() as IPublicTransportPlanner;
- return planner.plan(query);
+ return new PromiseProxyIterator(() => planner.plan(query));
}
private async resolveEndpoint(endpoint: string | string[] | ILocation | ILocation[]): Promise {
@@ -90,7 +99,8 @@ export default class QueryRunnerExponential implements IQueryRunner {
from, to,
minimumWalkingSpeed, maximumWalkingSpeed, walkingSpeed,
maximumWalkingDuration, maximumWalkingDistance,
- minimumTransferDuration, maximumTransferDuration, maximumTransfers,
+ minimumTransferDuration, maximumTransferDuration, maximumTransferDistance,
+ maximumTransfers,
minimumDepartureTime,
...other
} = query;
@@ -100,15 +110,25 @@ export default class QueryRunnerExponential implements IQueryRunner {
resolvedQuery.minimumDepartureTime = minimumDepartureTime || new Date();
- resolvedQuery.from = await this.resolveEndpoint(from);
- resolvedQuery.to = await this.resolveEndpoint(to);
+ try {
+ resolvedQuery.from = await this.resolveEndpoint(from);
+ resolvedQuery.to = await this.resolveEndpoint(to);
+
+ } catch (e) {
+ return Promise.reject(new InvalidQueryError(e));
+ }
+
resolvedQuery.minimumWalkingSpeed = minimumWalkingSpeed || walkingSpeed || Defaults.defaultMinimumWalkingSpeed;
resolvedQuery.maximumWalkingSpeed = maximumWalkingSpeed || walkingSpeed || Defaults.defaultMaximumWalkingSpeed;
+
resolvedQuery.maximumWalkingDuration = maximumWalkingDuration ||
Units.toDuration(maximumWalkingDistance, resolvedQuery.minimumWalkingSpeed) || Defaults.defaultWalkingDuration;
resolvedQuery.minimumTransferDuration = minimumTransferDuration || Defaults.defaultMinimumTransferDuration;
- resolvedQuery.maximumTransferDuration = maximumTransferDuration || Defaults.defaultMaximumTransferDuration;
+ resolvedQuery.maximumTransferDuration = maximumTransferDuration ||
+ Units.toDuration(maximumTransferDistance, resolvedQuery.minimumWalkingSpeed) ||
+ Defaults.defaultMaximumTransferDuration;
+
resolvedQuery.maximumTransfers = maximumTransfers || Defaults.defaultMaximumTransfers;
return resolvedQuery;
diff --git a/src/query-runner/exponential/SubqueryIterator.test.ts b/src/query-runner/exponential/SubqueryIterator.test.ts
deleted file mode 100644
index 4d6632b5..00000000
--- a/src/query-runner/exponential/SubqueryIterator.test.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { ArrayIterator } from "asynciterator";
-import "jest";
-import SubqueryIterator from "./SubqueryIterator";
-
-const ALPHABET = "abc";
-
-const queryIterator = new ArrayIterator([1, 2, 3]);
-
-const subqueryIterator = new SubqueryIterator(queryIterator, (num) => {
- return new Promise((resolve) => {
- const array = Array(num).fill(ALPHABET[num - 1]);
-
- resolve(new ArrayIterator(array));
- });
-});
-
-test("[SubqueryIterator]", (done) => {
-
- let current = 0;
- const expected = ["a", "b", "b", "c", "c", "c"];
-
- subqueryIterator.each((str) => {
- expect(expected[current++]).toBe(str);
- });
-
- subqueryIterator.on("end", () => done());
-
-});
diff --git a/src/query-runner/exponential/SubqueryIterator.ts b/src/query-runner/exponential/SubqueryIterator.ts
deleted file mode 100644
index e749e5e3..00000000
--- a/src/query-runner/exponential/SubqueryIterator.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { AsyncIterator, BufferedIterator } from "asynciterator";
-
-export default class SubqueryIterator extends BufferedIterator {
- private queryIterator: AsyncIterator;
- private callback: (query: Q) => Promise>;
-
- private currentResultIterator: AsyncIterator;
- private currentResultPushed: number;
- private isLastResultIterator = false;
-
- constructor(queryIterator: AsyncIterator, run: (query: Q) => Promise>) {
- super();
-
- this.queryIterator = queryIterator;
- this.callback = run;
-
- this.queryIterator.once("end", () => {
- this.isLastResultIterator = true;
- });
- }
-
- public _read(count: number, done: () => void) {
- if (!this.currentResultIterator) {
- const query = this.queryIterator.read();
-
- if (query) {
- this.runSubquery(query, done);
-
- } else {
- this.waitForSubquery(done);
- }
-
- return;
- }
-
- this.pushItemsAsync(done);
- }
-
- private runSubquery(subquery: Q, done: () => void) {
- const self = this;
-
- this.callback(subquery)
- .then((resultIterator: AsyncIterator) => {
- self.currentResultIterator = resultIterator;
- self.currentResultPushed = 0;
-
- self.currentResultIterator.once("end", () => {
- delete self.currentResultIterator;
-
- // Close if last iterator
- if (self.isLastResultIterator) {
- self.close();
- }
-
- // Iterator was empty
- if (self.currentResultPushed === 0 && !this.closed) {
- self._read(null, done);
- }
- });
-
- this.pushItemsAsync(done);
- });
- }
-
- private waitForSubquery(done: () => void) {
- this.queryIterator.once("readable", () => {
- const query = this.queryIterator.read();
-
- this.runSubquery(query, done);
- });
- }
-
- private pushItemsAsync(done) {
- this.currentResultIterator.on("readable", () => {
- this.pushItemsSync();
- done();
- });
- }
-
- private pushItemsSync(): boolean {
- let item = this.currentResultIterator.read();
- let hasPushed = false;
-
- while (item) {
- this._push(item);
- this.currentResultPushed++;
- hasPushed = true;
-
- item = this.currentResultIterator.read();
- }
-
- return hasPushed;
- }
-}
diff --git a/src/test/test-connections-iterator-2.ts b/src/test/test-connections-iterator-2.ts
index ac9a9cfb..9172efd9 100644
--- a/src/test/test-connections-iterator-2.ts
+++ b/src/test/test-connections-iterator-2.ts
@@ -1,6 +1,6 @@
import LDFetch from "ldfetch";
-import ConnectionsIteratorLazy from "../fetcher/connections/ld-fetch/ConnectionsIteratorLazy";
-import TravelMode from "../TravelMode";
+import TravelMode from "../enums/TravelMode";
+import ConnectionsIteratorLazy from "../fetcher/connections/lazy/ConnectionsIteratorLazy";
const ldFetch = new LDFetch({ headers: { Accept: "application/ld+json" } });
diff --git a/src/test/test-connections-iterator.ts b/src/test/test-connections-iterator.ts
index 19c9f10a..decba271 100644
--- a/src/test/test-connections-iterator.ts
+++ b/src/test/test-connections-iterator.ts
@@ -1,6 +1,6 @@
import LDFetch from "ldfetch";
import "reflect-metadata";
-import TravelMode from "../TravelMode";
+import TravelMode from "../enums/TravelMode";
/*
const ldFetch = new LDFetch({ headers: { Accept: "application/ld+json" } });
@@ -16,7 +16,7 @@ const config = {
const connectionsFetcher = new ConnectionsFetcherLDFetch(ldFetch);
connectionsFetcher.setTravelMode(TravelMode.Train);
connectionsFetcher.setAccessUrl("https://graph.irail.be/sncb/connections");
-connectionsFetcher.setConfig(config);
+connectionsFetcher.setIteratorOptions(config);
// iterator.setLowerBound(new Date(2018, 10, 2, 10));
(async () => {
diff --git a/src/types.ts b/src/types.ts
index 146cd498..d8835c42 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,6 +1,6 @@
+import TravelMode from "./enums/TravelMode";
import IConnectionsFetcher from "./fetcher/connections/IConnectionsFetcher";
import IStopsFetcher from "./fetcher/stops/IStopsFetcher";
-import TravelMode from "./TravelMode";
const TYPES = {
Context: Symbol("Context"),
diff --git a/src/util/BinarySearch.test.ts b/src/util/BinarySearch.test.ts
new file mode 100644
index 00000000..de946f39
--- /dev/null
+++ b/src/util/BinarySearch.test.ts
@@ -0,0 +1,73 @@
+import "jest";
+import BinarySearch from "./BinarySearch";
+
+describe("[BinarySearch]", () => {
+
+ const array = [1, 2, 3, 3, 5, 6, 6, 6, 6, 7, 7];
+ const search = new BinarySearch(array, (a) => a);
+
+ describe("findFirstIndex", () => {
+
+ it("key exists", () => {
+ const resultIndex = search.findFirstIndex(6);
+ const expectedIndex = 5;
+
+ expect(resultIndex).toBe(expectedIndex);
+ });
+
+ it("key doesn\'t exist / belongs at the start", () => {
+ const resultIndex = search.findFirstIndex(0);
+ const expectedIndex = 0;
+
+ expect(resultIndex).toBe(expectedIndex);
+ });
+
+ it("key doesn\'t exist / belongs in middle", () => {
+ const resultIndex = search.findFirstIndex(4);
+ const expectedIndex = 4;
+
+ expect(resultIndex).toBe(expectedIndex);
+ });
+
+ it("key doesn\'t exist / belongs at the end", () => {
+ const resultIndex = search.findFirstIndex(8);
+ const expectedIndex = 10;
+
+ expect(resultIndex).toBe(expectedIndex);
+ });
+
+ });
+
+ describe("findLastIndex", () => {
+
+ it("key exists", () => {
+ const resultIndex = search.findLastIndex(6);
+ const expectedIndex = 8;
+
+ expect(resultIndex).toBe(expectedIndex);
+ });
+
+ it("key doesn\'t exist / belongs at the start", () => {
+ const resultIndex = search.findLastIndex(0);
+ const expectedIndex = 0;
+
+ expect(resultIndex).toBe(expectedIndex);
+ });
+
+ it("key doesn\'t exist / belongs in middle", () => {
+ const resultIndex = search.findLastIndex(4);
+ const expectedIndex = 3;
+
+ expect(resultIndex).toBe(expectedIndex);
+ });
+
+ it("key doesn\'t exist / belongs at the end", () => {
+ const resultIndex = search.findLastIndex(8);
+ const expectedIndex = 10;
+
+ expect(resultIndex).toBe(expectedIndex);
+ });
+
+ });
+
+});
diff --git a/src/util/BinarySearch.ts b/src/util/BinarySearch.ts
new file mode 100644
index 00000000..0e19e4db
--- /dev/null
+++ b/src/util/BinarySearch.ts
@@ -0,0 +1,52 @@
+/**
+ * Util class with binary search procedures
+ * Assumes that array is in ascending order according to predicate
+ */
+export default class BinarySearch {
+ private readonly array: T[];
+ private readonly predicate: (item: T) => number;
+
+ constructor(array: T[], predicate: (item: T) => number) {
+ this.array = array;
+ this.predicate = predicate;
+ }
+
+ /**
+ * Find the first index of the given key, or the index before which that key would be hypothetically spliced in
+ * Adapted from: https://algorithmsandme.com/first-occurrence-of-element/
+ */
+ public findFirstIndex(key: number, start: number = 0, end: number = (this.array.length - 1)): number {
+ while (start < end) {
+ const mid = start + Math.floor((end - start) / 2);
+
+ if (this.predicate(this.array[mid]) >= key) {
+ end = mid;
+
+ } else {
+ start = mid + 1;
+ }
+ }
+
+ return start;
+ }
+
+ /**
+ * Find the last index of the given key, or the index after which that key would be hypothetically spliced in
+ * Adapted from: https://www.algorithmsandme.com/last-occurrence-of-element-with-binary-search/
+ */
+ public findLastIndex(key: number, start: number = 0, end: number = (this.array.length - 1)): number {
+ while (start < end) {
+ const mid = start + Math.floor(((end - start) + 1) / 2);
+
+ if (this.predicate(this.array[mid]) <= key) {
+ start = mid;
+
+ } else {
+ end = mid - 1;
+ }
+ }
+
+ return start;
+ }
+
+}
diff --git a/src/util/Geo.ts b/src/util/Geo.ts
index 583cd194..6ab118a5 100644
--- a/src/util/Geo.ts
+++ b/src/util/Geo.ts
@@ -3,7 +3,15 @@ import IStop from "../fetcher/stops/IStop";
import ILocation from "../interfaces/ILocation";
import { DistanceM } from "../interfaces/units";
+/**
+ * Utility class with geographic functions
+ */
export default class Geo {
+
+ /**
+ * Calculate the distance between two [[ILocation]] instances using the haversine formula
+ * @returns distance is meters ([[DistanceM]])
+ */
public static getDistanceBetweenLocations(start: ILocation, stop: ILocation): DistanceM {
const { longitude: depLongitude, latitude: depLatitude } = start;
const { longitude: arrLongitude, latitude: arrLatitude } = stop;
@@ -24,7 +32,20 @@ export default class Geo {
});
}
+ /**
+ * Calculate tge distance between two [[IStop]] instances using the haversine formula
+ * @returns distance is meters ([[DistanceM]])
+ */
public static getDistanceBetweenStops(start: IStop, stop: IStop) {
return this.getDistanceBetweenLocations(start as ILocation, stop as ILocation);
}
+
+ /**
+ * Get the geo id of an [[ILocation]]
+ * @param location
+ * @returns geo id string
+ */
+ public static getId(location: ILocation): string {
+ return `geo:${location.latitude},${location.longitude}`;
+ }
}
diff --git a/src/util/Iterators.ts b/src/util/Iterators.ts
index 7947ea5c..28147de0 100644
--- a/src/util/Iterators.ts
+++ b/src/util/Iterators.ts
@@ -1,9 +1,15 @@
import { AsyncIterator } from "asynciterator";
+/**
+ * Utility class with functions to operate on AsyncIterator instances
+ */
export default class Iterators {
+ /**
+ * Returns an array representation of an AsyncIterator.
+ * Assumes the iterator will end sometime
+ */
public static toArray(iterator: AsyncIterator): Promise {
-
const array = [];
iterator.each((item: T) => array.push(item));
@@ -12,6 +18,9 @@ export default class Iterators {
});
}
+ /**
+ * Returns the first element of an AsyncIterator.
+ */
public static getFirst(iterator: AsyncIterator): Promise {
return new Promise((resolve) => {
iterator.on("readable", () => {
@@ -20,12 +29,15 @@ export default class Iterators {
});
}
- public static find(iterator: AsyncIterator, callback: (element: T) => boolean): Promise {
+ /**
+ * Iterates over elements of an AsyncIterator, returning the first element ´predicate´ returns truthy for.
+ */
+ public static find(iterator: AsyncIterator, predicate: (element: T) => boolean): Promise {
return new Promise((resolve, reject) => {
iterator.on("readable", () => {
let element = iterator.read();
- while (element && !callback(element)) {
+ while (element && !predicate(element)) {
element = iterator.read();
}
diff --git a/src/util/Rdf.ts b/src/util/Rdf.ts
index f776afc3..f4b8d1b3 100644
--- a/src/util/Rdf.ts
+++ b/src/util/Rdf.ts
@@ -1,8 +1,27 @@
import { Triple } from "rdf-js";
+/**
+ * Utility class with functions dealing with rdf triples
+ */
export default class Rdf {
- public static matchesTriple(subject: string, predicate: string, object: string): (triple: Triple) => boolean {
+ /**
+ * Creates a triple matcher callback function for use in e.g. an Array#filter() expression
+ *
+ * For example:
+ * ```
+ * tripleArray.filter(Rdf.matchesTriple('someSubject', null, null));
+ * ```
+ * @param subject can be null if not wanting to match by subject
+ * @param predicate can be null if not wanting to match by predicate
+ * @param object can be null if not wanting to match by object
+ */
+ public static matchesTriple(
+ subject: string | null,
+ predicate: string | null,
+ object: string | null,
+ ): (triple: Triple) => boolean {
+
return (triple: Triple) => {
if (subject && triple.subject.value !== subject) {
return false;
@@ -20,6 +39,18 @@ export default class Rdf {
};
}
+ /**
+ * Rename the predicate of a triple based on a map with original predicates as keys and
+ * replacement predicates as values
+ *
+ * For example:
+ * ```
+ * const transformedTriple = Rdf.transformPredicate({
+ * "oldPredicate1": "newPredicate1",
+ * "oldPredicate2": "newPredicate2",
+ * }, someTriple));
+ * ```
+ */
public static transformPredicate(transformMap: { [oldPredicate: string]: string }, triple: Triple): Triple {
if (triple.predicate.value in transformMap) {
triple.predicate.value = transformMap[triple.predicate.value];
@@ -27,6 +58,18 @@ export default class Rdf {
return triple;
}
+ /**
+ * Rename the object of a triple based on a map with original objects as keys and
+ * replacement objects as values
+ *
+ * For example:
+ * ```
+ * const transformedTriple = Rdf.transformObject({
+ * "oldObject1": "newObject1",
+ * "oldObject2": "newObject2",
+ * }, someTriple));
+ * ```
+ */
public static transformObject(transformMap: { [oldObject: string]: string }, triple: Triple): Triple {
if (triple.object.value in transformMap) {
triple.object.value = transformMap[triple.object.value];
@@ -34,6 +77,9 @@ export default class Rdf {
return triple;
}
+ /**
+ * Log an array of triples to the console as a table with three columns: subject, predicate and object
+ */
public static logTripleTable(triples: Triple[]): void {
console.table(triples.map((triple: Triple) => ({
subject: triple.subject.value,
diff --git a/src/util/Units.test.ts b/src/util/Units.test.ts
index fc8ee6d1..7b6da3b5 100644
--- a/src/util/Units.test.ts
+++ b/src/util/Units.test.ts
@@ -1,12 +1,12 @@
import "jest";
-import { DistanceM, DurationMs, SpeedkmH } from "../interfaces/units";
+import { DistanceM, DurationMs, SpeedKmH } from "../interfaces/units";
import Units from "./Units";
test("[Units] toSpeed", () => {
const distance: DistanceM = 10000;
const duration: DurationMs = 3600000;
- const speed: SpeedkmH = Units.toSpeed(distance, duration);
+ const speed: SpeedKmH = Units.toSpeed(distance, duration);
expect(speed).toBeDefined();
expect(speed).toEqual(10);
@@ -15,7 +15,7 @@ test("[Units] toSpeed", () => {
test("[Units] toDuration", () => {
const distance: DistanceM = 10000;
- const speed: SpeedkmH = 10;
+ const speed: SpeedKmH = 10;
const duration: DurationMs = Units.toDuration(distance, speed);
expect(duration).toBeDefined();
@@ -25,7 +25,7 @@ test("[Units] toDuration", () => {
test("[Units] toDistance", () => {
const duration: DurationMs = 3600000;
- const speed: SpeedkmH = 10;
+ const speed: SpeedKmH = 10;
const distance: DistanceM = Units.toDistance(speed, duration);
expect(distance).toBeDefined();
diff --git a/src/util/Units.ts b/src/util/Units.ts
index 4a4337fb..efbda615 100644
--- a/src/util/Units.ts
+++ b/src/util/Units.ts
@@ -1,15 +1,19 @@
-import { DistanceM, DurationMs, SpeedkmH } from "../interfaces/units";
+import { DistanceM, DurationMs, SpeedKmH } from "../interfaces/units";
+/**
+ * Utility class with calculation functions dealing with [[DistanceM]], [[DurationMs]] and [[SpeedKmH]]
+ */
export default class Units {
- public static toSpeed(distance: DistanceM, duration: DurationMs): SpeedkmH {
+
+ public static toSpeed(distance: DistanceM, duration: DurationMs): SpeedKmH {
return (distance / duration) * 3600;
}
- public static toDistance(duration: DurationMs, speed: SpeedkmH): DistanceM {
+ public static toDistance(duration: DurationMs, speed: SpeedKmH): DistanceM {
return (speed * duration) / 3600;
}
- public static toDuration(distance: DistanceM, speed: SpeedkmH): DurationMs {
+ public static toDuration(distance: DistanceM, speed: SpeedKmH): DurationMs {
// tslint:disable-next-line:no-bitwise
return ((distance / speed) * 3600 | 0);
}
diff --git a/src/util/iterators/AsyncArrayIterator.ts b/src/util/iterators/AsyncArrayIterator.ts
new file mode 100644
index 00000000..243dfa55
--- /dev/null
+++ b/src/util/iterators/AsyncArrayIterator.ts
@@ -0,0 +1,35 @@
+import { BufferedIterator } from "asynciterator";
+import { DurationMs } from "../../interfaces/units";
+
+/**
+ * An AsyncIterator that emits the items of a given array, asynchronously.
+ * Optionally accepts an interval (in ms) between each emitted item
+ *
+ * This class is most useful in tests
+ */
+export default class AsyncArrayIterator extends BufferedIterator {
+ private currentIndex: number = 0;
+ private readonly array: T[];
+ private readonly interval: DurationMs;
+
+ constructor(array: T[], interval: DurationMs = 0) {
+ super();
+
+ this.array = array;
+ this.interval = interval;
+ }
+
+ public _read(count: number, done: () => void): void {
+ if (this.currentIndex === this.array.length) {
+ this.close();
+ return done();
+ }
+
+ const self = this;
+
+ setTimeout(() => {
+ self._push(self.array[self.currentIndex++]);
+ done();
+ }, this.interval);
+ }
+}
diff --git a/src/util/iterators/Emiterator.ts b/src/util/iterators/Emiterator.ts
index 3b54f8a3..7d3d51d3 100644
--- a/src/util/iterators/Emiterator.ts
+++ b/src/util/iterators/Emiterator.ts
@@ -1,6 +1,6 @@
import { AsyncIterator, SimpleTransformIterator, SimpleTransformIteratorOptions } from "asynciterator";
import Context from "../../Context";
-import EventType from "../../EventType";
+import EventType from "../../enums/EventType";
/**
* Lazily emits an event of specified type for each item that passes through source iterator
diff --git a/src/util/iterators/ExpandingIterator.test.ts b/src/util/iterators/ExpandingIterator.test.ts
new file mode 100644
index 00000000..75f1aea3
--- /dev/null
+++ b/src/util/iterators/ExpandingIterator.test.ts
@@ -0,0 +1,98 @@
+import "jest";
+import ExpandingIterator from "./ExpandingIterator";
+
+describe("[ExpandingIterator]", () => {
+
+ it("async push", (done) => {
+
+ const expandingIterator = new ExpandingIterator();
+ const expected = [1, 2, 3, 4, 5, 6, 8];
+
+ let currentWrite = 0;
+ let currentRead = 0;
+
+ const interval = setInterval(() => {
+ // console.log("Writing", expected[currentWrite]);
+ expandingIterator.write(expected[currentWrite]);
+
+ if (++currentWrite === expected.length) {
+ // console.log("Closing");
+ expandingIterator.close();
+ clearInterval(interval);
+ }
+ }, 1);
+
+ expandingIterator.each((str: number) => {
+ // console.log("Reading", str);
+ expect(expected[currentRead++]).toBe(str);
+ });
+
+ expandingIterator.on("end", () => done());
+ });
+
+ it("sync push", (done) => {
+
+ const expandingIterator = new ExpandingIterator();
+ const expected = [1, 2, 3, 4, 5, 6, 8];
+
+ let currentWrite = 0;
+ let currentRead = 0;
+
+ for (; currentWrite < expected.length; currentWrite++) {
+ // console.log("Writing", expected[currentWrite]);
+ expandingIterator.write(expected[currentWrite]);
+ }
+
+ expandingIterator.close();
+
+ expandingIterator.each((str: number) => {
+ // console.log("Reading", str);
+ expect(expected[currentRead++]).toBe(str);
+ });
+
+ expandingIterator.on("end", () => done());
+ });
+
+ it("mixed push", (done) => {
+
+ const expandingIterator = new ExpandingIterator();
+ const expected = [1, 2, 3, 4, 5, 6, 8];
+
+ let currentWrite = 0;
+ let currentRead = 0;
+
+ for (; currentWrite < 3; currentWrite++) {
+ // console.log("Writing", expected[currentWrite]);
+ expandingIterator.write(expected[currentWrite]);
+ }
+
+ const interval = setInterval(() => {
+ // console.log("Writing", expected[currentWrite]);
+ expandingIterator.write(expected[currentWrite]);
+
+ if (++currentWrite < expected.length) {
+ // console.log("Writing", expected[currentWrite]);
+ expandingIterator.write(expected[currentWrite]);
+
+ } else {
+ // console.log("Closing");
+ expandingIterator.close();
+ clearInterval(interval);
+ }
+
+ if (++currentWrite === expected.length) {
+ // console.log("Closing");
+ expandingIterator.close();
+ clearInterval(interval);
+ }
+ }, 1);
+
+ expandingIterator.each((str: number) => {
+ // console.log("Reading", str);
+ expect(expected[currentRead++]).toBe(str);
+ });
+
+ expandingIterator.on("end", () => done());
+ });
+
+});
diff --git a/src/util/iterators/ExpandingIterator.ts b/src/util/iterators/ExpandingIterator.ts
new file mode 100644
index 00000000..28153cb4
--- /dev/null
+++ b/src/util/iterators/ExpandingIterator.ts
@@ -0,0 +1,45 @@
+import { AsyncIterator } from "asynciterator";
+
+export default class ExpandingIterator extends AsyncIterator {
+
+ private buffer: T[];
+ private shouldClose: boolean;
+
+ constructor() {
+ super();
+
+ this.buffer = [];
+ this.shouldClose = false;
+ }
+
+ public read(): T {
+ let item;
+
+ if (this.buffer.length) {
+ item = this.buffer.shift();
+
+ } else {
+ item = null;
+
+ if (this.shouldClose) {
+ this.close();
+
+ }
+
+ this.readable = false;
+ }
+
+ return item;
+ }
+
+ public write(item: T): void {
+ if (!this.shouldClose) {
+ this.buffer.push(item);
+ this.readable = true;
+ }
+ }
+
+ public closeAfterFlush(): void {
+ this.shouldClose = true;
+ }
+}
diff --git a/src/util/iterators/FilterUniqueIterator.test.ts b/src/util/iterators/FilterUniqueIterator.test.ts
new file mode 100644
index 00000000..41bb127c
--- /dev/null
+++ b/src/util/iterators/FilterUniqueIterator.test.ts
@@ -0,0 +1,25 @@
+import { ArrayIterator } from "asynciterator";
+import "jest";
+import FilterUniqueIterator from "./FilterUniqueIterator";
+
+describe("[FilterUniqueIterator]", () => {
+
+ it("basic", (done) => {
+
+ const numberIterator = new ArrayIterator([1, 1, 2, 3, 4, 5, 5, 5, 5, 6, 1, 5, 3, 5, 8]);
+ const filterUniqueIterator = new FilterUniqueIterator(
+ numberIterator,
+ (a, b) => a === b,
+ );
+
+ let current = 0;
+ const expected = [1, 2, 3, 4, 5, 6, 8];
+
+ filterUniqueIterator.each((str: number) => {
+ expect(expected[current++]).toBe(str);
+ });
+
+ filterUniqueIterator.on("end", () => done());
+ });
+
+});
diff --git a/src/util/iterators/FilterUniqueIterator.ts b/src/util/iterators/FilterUniqueIterator.ts
new file mode 100644
index 00000000..14c8bec5
--- /dev/null
+++ b/src/util/iterators/FilterUniqueIterator.ts
@@ -0,0 +1,35 @@
+import { AsyncIterator, SimpleTransformIterator } from "asynciterator";
+
+/**
+ * An AsyncIterator that emits only the unique items emitted by a source iterator.
+ * Uniqueness is determined by a comparator callback function
+ *
+ * Note: All (unique) items get stored in an array internally
+ */
+export default class FilterUniqueIterator extends SimpleTransformIterator {
+
+ private readonly comparator: (object: T, otherObject: T) => boolean;
+ private store: T[];
+
+ constructor(source: AsyncIterator, comparator: (object: T, otherObject: T) => boolean) {
+ super(source, {
+ maxBufferSize: 1,
+ autoStart: false,
+ });
+
+ this.comparator = comparator;
+ this.store = [];
+ }
+
+ public _filter(object: T): boolean {
+
+ const isUnique = !this.store
+ .some((storedObject: T) => this.comparator(object, storedObject));
+
+ if (isUnique) {
+ this.store.push(object);
+ }
+
+ return isUnique;
+ }
+}
diff --git a/src/util/iterators/FlatMapIterator.test.ts b/src/util/iterators/FlatMapIterator.test.ts
new file mode 100644
index 00000000..c9cb4d80
--- /dev/null
+++ b/src/util/iterators/FlatMapIterator.test.ts
@@ -0,0 +1,40 @@
+import { ArrayIterator } from "asynciterator";
+import "jest";
+import AsyncArrayIterator from "./AsyncArrayIterator";
+import FlatMapIterator from "./FlatMapIterator";
+
+const ALPHABET = "abc";
+const expected = ["a", "b", "b", "c", "c", "c"];
+
+describe("[FlatMapIterator]", () => {
+
+ const runTest = (QueryIterator, ResultIterator, done) => {
+ const queryIterator = new QueryIterator([1, 2, 3], 10);
+
+ const flatMapIterator = new FlatMapIterator(queryIterator, (num) => {
+ const array = Array(num).fill(ALPHABET[num - 1]);
+
+ return new ResultIterator(array, 10);
+ });
+
+ let current = 0;
+
+ flatMapIterator.each((str) => {
+ expect(expected[current++]).toBe(str);
+ });
+
+ flatMapIterator.on("end", () => done());
+ };
+
+ it("Subqueries from ArrayIterator / Results from ArrayIterator", (done) => {
+ runTest(ArrayIterator, ArrayIterator, done);
+ });
+
+ it("Subqueries from ArrayIterator / Results from BufferedIterator", (done) => {
+ runTest(ArrayIterator, AsyncArrayIterator, done);
+ });
+
+ it("Subqueries from BufferedIterator / Results from BufferedIterator", (done) => {
+ runTest(AsyncArrayIterator, AsyncArrayIterator, done);
+ });
+});
diff --git a/src/util/iterators/FlatMapIterator.ts b/src/util/iterators/FlatMapIterator.ts
new file mode 100644
index 00000000..59eb7f4d
--- /dev/null
+++ b/src/util/iterators/FlatMapIterator.ts
@@ -0,0 +1,90 @@
+import { AsyncIterator } from "asynciterator";
+
+/**
+ * This AsyncIterator maps every item of a query AsyncIterator to a result AsyncIterator by passing it through a
+ * `run` function. All result AsyncIterator get concatenated to form the FlatMapIterator
+ *
+ * ```javascript
+ * +-----+ +-----+
+ * queryIterator |0 & 9| +---+ + |1 & 8| +---+ + ...
+ * +-----+ | +-----+ |
+ * v v
+ * +-----------------------+ +-----------------------+
+ * resultIterators |01|02|04|05|06|07|08|09| + |11|12|13|14|15|16|17|18| + ...
+ * +-----------------------+ +-----------------------+
+ *
+ * +-----------------------------------------------+
+ * FlatMapIterator |01|02|04|05|06|07|08|09|11|12|13|14|15|16|17|18| ...
+ * +-----------------------------------------------+
+ * ```
+ */
+export default class FlatMapIterator extends AsyncIterator {
+ private queryIterator: AsyncIterator;
+ private callback: (query: Q) => AsyncIterator;
+
+ private currentResultIterator: AsyncIterator;
+ private isLastResultIterator = false;
+
+ constructor(queryIterator: AsyncIterator, run: (query: Q) => AsyncIterator) {
+ super();
+
+ this.queryIterator = queryIterator;
+ this.callback = run;
+
+ this.queryIterator.once("end", () => {
+ this.isLastResultIterator = true;
+ });
+
+ this.readable = true;
+ }
+
+ public read(): R {
+ if (this.closed) {
+ return null;
+ }
+
+ if (!this.currentResultIterator) {
+ const query: Q = this.queryIterator.read();
+
+ if (query) {
+ this.runQuery(query);
+
+ } else {
+ this.readable = false;
+ this.queryIterator.once("readable", () => {
+ this.readable = true;
+ });
+ }
+ }
+
+ if (this.currentResultIterator) {
+ const item = this.currentResultIterator.read();
+
+ if (!item) {
+ this.readable = false;
+ }
+
+ return item;
+ }
+
+ return null;
+ }
+
+ private runQuery(query: Q) {
+ this.currentResultIterator = this.callback(query);
+ this.readable = this.currentResultIterator.readable;
+
+ this.currentResultIterator.on("readable", () => {
+ this.readable = true;
+ });
+
+ this.currentResultIterator.on("end", () => {
+ if (this.isLastResultIterator) {
+ this.close();
+ }
+
+ this.currentResultIterator = null;
+ this.readable = true;
+ });
+ }
+}
diff --git a/src/util/iterators/MergeIterator.ts b/src/util/iterators/MergeIterator.ts
index 384ff3a7..8416a79a 100644
--- a/src/util/iterators/MergeIterator.ts
+++ b/src/util/iterators/MergeIterator.ts
@@ -1,10 +1,10 @@
import { AsyncIterator, BufferedIterator } from "asynciterator";
/**
- * Asynciterator that merges a number of source asynciterators based on the passed selector function.
+ * AsyncIterator that merges a number of source asynciterators based on the passed selector function.
* The selector function gets passed an array of values read from each of the asynciterators.
* Values can be undefined if their respective source iterator has ended.
- * It should return the index in that array of the value to select.
+ * The selector function should return the index in that array of the value to select.
* @param condensed When true, undefined values are filtered from the array passed to the selector function
*/
export default class MergeIterator extends BufferedIterator {
diff --git a/tsconfig.json b/tsconfig.json
index b381d735..e2adfa36 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,6 @@
{
"compilerOptions": {
- "target": "es5",
+ "target": "ES2017",
"lib": ["esnext", "dom"],
"types": ["reflect-metadata"],
"module": "commonjs",
diff --git a/tslint.json b/tslint.json
index 8d17f7e4..81d71924 100644
--- a/tslint.json
+++ b/tslint.json
@@ -7,7 +7,13 @@
"rules": {
"no-console": false,
"object-literal-sort-keys": false,
- "no-empty-interface": false
+ "no-empty-interface": false,
+ "no-shadowed-variable": [
+ true,
+ {
+ "temporalDeadZone": false
+ }
+ ]
},
"rulesDirectory": []
}
diff --git a/typedoc.config.js b/typedoc.config.js
new file mode 100644
index 00000000..fa7e335d
--- /dev/null
+++ b/typedoc.config.js
@@ -0,0 +1,17 @@
+module.exports = {
+ out: 'docs/code',
+ includes: 'src/',
+ exclude: [
+ '**/*+(test).ts',
+ '**/src/demo.*',
+ '**/src/inversify.config.ts',
+ '**/src/test/*',
+ '**/fetcher/connections/tests/**/*',
+ ],
+ mode: 'file',
+ excludePrivate: true,
+ excludeNotExported: false,
+ excludeExternals: false,
+ includeDeclarations: false,
+ theme: 'minimal',
+};
diff --git a/typedoc.js b/typedoc.js
deleted file mode 100644
index f888992e..00000000
--- a/typedoc.js
+++ /dev/null
@@ -1,12 +0,0 @@
-module.exports = {
- out: 'docs/code',
- includes: 'src/',
- exclude: [
- '**/*+(config|test).ts',
- ],
- mode: 'file',
- excludePrivate: true,
- excludeNotExported: true,
- excludeExternals: true,
- theme: 'minimal',
-};
diff --git a/webpack.config.js b/webpack.config.js
index cd163ddc..0a356e30 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,8 +1,22 @@
const path = require("path");
+// These modules are imported by ldfetch, but are never actually used because (right now) we only fetch jsonld files
+const excludeModules = [
+ "rdfa-processor",
+ //"rdf-canonize",
+ "rdfxmlprocessor",
+ "xmldom",
+ 'n3'
+];
+
+const excludeAlias = excludeModules.reduce((alias, moduleName) => {
+ alias[moduleName] = path.resolve(__dirname, "webpack/mockModule.js");
+ return alias;
+}, {});
+
module.exports = {
entry: "./src/index.ts",
- devtool: 'cheap-module-source-map',
+ devtool: "source-map",//"cheap-module-source-map",
module: {
rules: [
{
@@ -13,13 +27,17 @@ module.exports = {
]
},
resolve: {
- extensions: [".ts", ".js"]
+ extensions: [".ts", ".js"],
+ alias: {
+ ...excludeAlias,
+ "q": path.resolve(__dirname, "webpack/shimQ.js")
+ }
},
output: {
filename: "bundle.js",
- path: path.resolve(__dirname, 'dist'),
- library: 'Planner',
- libraryTarget: 'umd',
- libraryExport: 'default',
+ path: path.resolve(__dirname, "dist"),
+ library: "Planner",
+ libraryTarget: "umd",
+ libraryExport: "default"
}
};
diff --git a/webpack/mockModule.js b/webpack/mockModule.js
new file mode 100644
index 00000000..f053ebf7
--- /dev/null
+++ b/webpack/mockModule.js
@@ -0,0 +1 @@
+module.exports = {};
diff --git a/webpack/shimQ.js b/webpack/shimQ.js
new file mode 100644
index 00000000..174bd4d0
--- /dev/null
+++ b/webpack/shimQ.js
@@ -0,0 +1,14 @@
+// Mocks the (only) user of the q promise library by ldfetch
+
+module.exports = {
+ defer: () => {
+ const deferred = {};
+
+ deferred.promise = new Promise((resolve, reject) => {
+ deferred.resolve = resolve;
+ deferred.reject = reject;
+ });
+
+ return deferred;
+ }
+};