diff --git a/backend/controllers/getCongestion.js b/backend/controllers/getCongestion.js index 118977d..0497548 100644 --- a/backend/controllers/getCongestion.js +++ b/backend/controllers/getCongestion.js @@ -1,52 +1,22 @@ -const axios = require("axios"); -const API_URL = "https://route.ls.hereapi.com/routing/7.2/calculateroute.json"; +const getColorForPercentage = require("../utils/getColorForPercentage"); -async function getCongestionData(route, results, departureTime) { - let promises = []; - for (let i = 0; i < route.length; i++) { - let p = axios.get(API_URL, { - params: { - waypoint0: `${route[i].begin.lat},${route[i].begin.lng}`, - waypoint1: `${route[i].end.lat},${route[i].end.lng}`, - mode: "fastest;car;traffic:enabled", - apiKey: process.env.here_api_key, - departure: departureTime, - }, - }); - promises.push(p); - } - try { - let responses = await Promise.all(promises); - let result = []; - for (let i = 0; i < route.length; i++) { - let { baseTime, travelTime } = responses[ - i - ].data.response.route[0].summary; - let congestionValue = travelTime / baseTime; - let congestionColor = - route[i].transport.mode === "pedestrian" - ? "green" - : getCongestionColor(congestionValue); - - result = [...result, { ...route[i], congestionColor }]; - } - results.push(result); - } catch (err) { - console.log(err); - } -} -async function calculateCongestion(routes, departureTime) { - let results = []; - for (let i = 0; i < routes.length; i++) { - await getCongestionData(routes[i], results, departureTime); - } - return results; +async function calculateCongestion(congestionParams) { + const congestionData = + congestionParams && congestionParams.length + ? congestionParams.map((param, index) => { + const { baseDuration, duration } = param + ? param + : { baseDuration: 1, duration: 1 }; + const congestionValue = + duration / baseDuration < 1 ? 1 : duration / baseDuration; + const normalizedCongestion = Math.min(congestionValue - 1, 1); + return { + congestionValue, + congestionColor: getColorForPercentage(normalizedCongestion), + }; + }) + : []; + return congestionData; } -function getCongestionColor(congestionValue) { - if (congestionValue < 1.25) return "green"; - else if (congestionValue >= 1.25 && congestionValue < 1.5) return "blue"; - else if (congestionValue >= 1.5 && congestionValue < 2) return "orange"; - else if (congestionValue >= 2) return "red"; -} module.exports = calculateCongestion; diff --git a/backend/controllers/getPM2_5.js b/backend/controllers/getPM2_5.js index c934e99..67365ab 100644 --- a/backend/controllers/getPM2_5.js +++ b/backend/controllers/getPM2_5.js @@ -10,7 +10,7 @@ exports.updateDatabase = async () => { let lat1 = 28.08652, lat2 = 28.921631, long1 = 76.730347, - long2 = 77.631226; + long2 = 77.631226; // bouding coordinates for delhi region const api_url = `https://api.waqi.info/map/bounds/?latlng=${lat1},${long1},${lat2},${long2}&token=${process.env.waqi_api_key}`; await axios .get(api_url) @@ -41,7 +41,7 @@ exports.updateDatabase = async () => { // console.log(database); }; -exports.getPM2_5 = (lat, lng) => { +exports.getPM2_5 = (lat, lng, currentDatabase) => { const x1 = lat; const y1 = lng; @@ -58,7 +58,6 @@ exports.getPM2_5 = (lat, lng) => { return 12742 * Math.asin(Math.sqrt(a)); // 2 * R; R = 6371 km } - const currentDatabase = [...database]; // use contant value for a particular calculation as database is updated every 10 seconds for (let i = 0; i < currentDatabase.length; i++) { const x2 = currentDatabase[i].latitude; const y2 = currentDatabase[i].longitude; @@ -84,19 +83,45 @@ exports.getPMColor = (routes = [], minPm, maxPm) => { routes.forEach((route) => { colorizedPmRoutes = [ ...colorizedPmRoutes, - route && route.length > 0 - ? route.map((section) => { - const normalizedPm = - maxPm > minPm ? (section.pmValue - minPm) / (maxPm - minPm) : -1; - return { - ...section, - normalizedPm, - pmColor: getColorForPercentage(normalizedPm), - }; - }) - : {}, + { + ...route, + sections: + route.sections && route.sections.length > 0 + ? route.sections.map((section) => { + const newSpans = section.spans.map((span) => { + const normalizedPm = + maxPm > minPm + ? (span.pmValue - minPm) / (maxPm - minPm) + : -1; + return { + ...span, + pmColor: getColorForPercentage(normalizedPm), + }; + }); + return { + ...section, + spans: newSpans, + }; + }) + : [], + }, ]; }); // console.log(colorizedPmRoutes) return colorizedPmRoutes; }; + +exports.calculatePmValuesList = async (locationsList) => { + const currentDatabase = [...database]; // use contant value for a particular calculation as database is updated every 10 seconds + const promises = locationsList.map((location) => { + return new Promise((resolve, reject) => + resolve(this.getPM2_5(location.lat, location.lng, currentDatabase)) + ); + }); + try { + const responses = await Promise.all(promises); + return responses; + } catch (err) { + console.log(err); + } +}; diff --git a/backend/utils/flexiblePolyline.js b/backend/utils/flexiblePolyline.js new file mode 100644 index 0000000..38ade7a --- /dev/null +++ b/backend/utils/flexiblePolyline.js @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2019 HERE Europe B.V. + * Licensed under MIT, see full license in LICENSE + * SPDX-License-Identifier: MIT + * License-Filename: LICENSE + */ +const DEFAULT_PRECISION = 5; + +const ENCODING_TABLE = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +const DECODING_TABLE = [ + 62, + -1, + -1, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + -1, + -1, + -1, + -1, + 63, + -1, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, +]; + +const FORMAT_VERSION = 1; + +const ABSENT = 0; +const LEVEL = 1; +const ALTITUDE = 2; +const ELEVATION = 3; +// Reserved values 4 and 5 should not be selectable +const CUSTOM1 = 6; +const CUSTOM2 = 7; + +const Num = typeof BigInt !== "undefined" ? BigInt : Number; + +function decode(encoded) { + const decoder = decodeUnsignedValues(encoded); + const header = decodeHeader(decoder[0], decoder[1]); + + const factorDegree = 10 ** header.precision; + const factorZ = 10 ** header.thirdDimPrecision; + const { thirdDim } = header; + + let lastLat = 0; + let lastLng = 0; + let lastZ = 0; + const res = []; + + let i = 2; + for (; i < decoder.length; ) { + const deltaLat = toSigned(decoder[i]) / factorDegree; + const deltaLng = toSigned(decoder[i + 1]) / factorDegree; + lastLat += deltaLat; + lastLng += deltaLng; + + if (thirdDim) { + const deltaZ = toSigned(decoder[i + 2]) / factorZ; + lastZ += deltaZ; + res.push([lastLat, lastLng, lastZ]); + i += 3; + } else { + res.push([lastLat, lastLng]); + i += 2; + } + } + + if (i !== decoder.length) { + throw new Error("Invalid encoding. Premature ending reached"); + } + + return { + ...header, + polyline: res, + }; +} + +function decodeChar(char) { + const charCode = char.charCodeAt(0); + return DECODING_TABLE[charCode - 45]; +} + +function decodeUnsignedValues(encoded) { + let result = Num(0); + let shift = Num(0); + const resList = []; + + encoded.split("").forEach((char) => { + const value = Num(decodeChar(char)); + result |= (value & Num(0x1f)) << shift; + if ((value & Num(0x20)) === Num(0)) { + resList.push(result); + result = Num(0); + shift = Num(0); + } else { + shift += Num(5); + } + }); + + if (shift > 0) { + throw new Error("Invalid encoding"); + } + + return resList; +} + +function decodeHeader(version, encodedHeader) { + if (+version.toString() !== FORMAT_VERSION) { + throw new Error("Invalid format version"); + } + const headerNumber = +encodedHeader.toString(); + const precision = headerNumber & 15; + const thirdDim = (headerNumber >> 4) & 7; + const thirdDimPrecision = (headerNumber >> 7) & 15; + return { precision, thirdDim, thirdDimPrecision }; +} + +function toSigned(val) { + // Decode the sign from an unsigned value + let res = val; + if (res & Num(1)) { + res = ~res; + } + res >>= Num(1); + return +res.toString(); +} + +function encode({ + precision = DEFAULT_PRECISION, + thirdDim = ABSENT, + thirdDimPrecision = 0, + polyline, +}) { + // Encode a sequence of lat,lng or lat,lng(,{third_dim}). Note that values should be of type BigNumber + // `precision`: how many decimal digits of precision to store the latitude and longitude. + // `third_dim`: type of the third dimension if present in the input. + // `third_dim_precision`: how many decimal digits of precision to store the third dimension. + + const multiplierDegree = 10 ** precision; + const multiplierZ = 10 ** thirdDimPrecision; + const encodedHeaderList = encodeHeader( + precision, + thirdDim, + thirdDimPrecision + ); + const encodedCoords = []; + + let lastLat = Num(0); + let lastLng = Num(0); + let lastZ = Num(0); + polyline.forEach((location) => { + const lat = Num(Math.round(location[0] * multiplierDegree)); + encodedCoords.push(encodeScaledValue(lat - lastLat)); + lastLat = lat; + + const lng = Num(Math.round(location[1] * multiplierDegree)); + encodedCoords.push(encodeScaledValue(lng - lastLng)); + lastLng = lng; + + if (thirdDim) { + const z = Num(Math.round(location[2] * multiplierZ)); + encodedCoords.push(encodeScaledValue(z - lastZ)); + lastZ = z; + } + }); + + return [...encodedHeaderList, ...encodedCoords].join(""); +} + +function encodeHeader(precision, thirdDim, thirdDimPrecision) { + // Encode the `precision`, `third_dim` and `third_dim_precision` into one encoded char + if (precision < 0 || precision > 15) { + throw new Error("precision out of range. Should be between 0 and 15"); + } + if (thirdDimPrecision < 0 || thirdDimPrecision > 15) { + throw new Error( + "thirdDimPrecision out of range. Should be between 0 and 15" + ); + } + if (thirdDim < 0 || thirdDim > 7 || thirdDim === 4 || thirdDim === 5) { + throw new Error("thirdDim should be between 0, 1, 2, 3, 6 or 7"); + } + + const res = (thirdDimPrecision << 7) | (thirdDim << 4) | precision; + return encodeUnsignedNumber(FORMAT_VERSION) + encodeUnsignedNumber(res); +} + +function encodeUnsignedNumber(val) { + // Uses variable integer encoding to encode an unsigned integer. Returns the encoded string. + let res = ""; + let numVal = Num(val); + while (numVal > 0x1f) { + const pos = (numVal & Num(0x1f)) | Num(0x20); + res += ENCODING_TABLE[pos]; + numVal >>= Num(5); + } + return res + ENCODING_TABLE[numVal]; +} + +function encodeScaledValue(value) { + // Transform a integer `value` into a variable length sequence of characters. + // `appender` is a callable where the produced chars will land to + let numVal = Num(value); + const negative = numVal < 0; + numVal <<= Num(1); + if (negative) { + numVal = ~numVal; + } + + return encodeUnsignedNumber(numVal); +} + +module.exports = { + encode, + decode, + + ABSENT, + LEVEL, + ALTITUDE, + ELEVATION, +}; diff --git a/backend/views/travelData.js b/backend/views/travelData.js index b771a20..d4b6902 100644 --- a/backend/views/travelData.js +++ b/backend/views/travelData.js @@ -1,110 +1,190 @@ const axios = require("axios"); const querystring = require("querystring"); const calculateCongestion = require("../controllers/getCongestion"); -const { getPM2_5, getPMColor } = require("../controllers/getPM2_5"); -const API_URL = "https://intermodal.router.hereapi.com/v8/routes"; +const { + getPMColor, + calculatePmValuesList, +} = require("../controllers/getPM2_5"); +const { decode } = require("../utils/flexiblePolyline"); +const API_URL = "https://router.hereapi.com/v8/routes"; function getTravelData(req, res) { - let queryStr = req.params.query; - let queryObj = querystring.parse(queryStr); - let origin = queryObj.origin; - let dest = queryObj.dest; - let departureTime = queryObj.departureTime - ? new Date(queryObj.departureTime) - : new Date(); - - let p = axios.get(API_URL, { + const queryStr = req && req.params ? req.params.query : null; + const queryObj = queryStr ? querystring.parse(queryStr) : null; + const origin = queryObj && queryObj.origin ? queryObj.origin : null; + const dest = queryObj && queryObj.dest ? queryObj.dest : null; + const departureTime = + queryObj && queryObj.departureTime + ? queryObj.departureTime + : new Date().toISOString(); + const transportMode = + queryObj && queryObj.transportMode ? queryObj.transportMode : "car"; + // validate data + if (!origin || !dest) { + const validate = [!origin, !dest]; + res.status(400).json({ + msg: ["Select a valid origin", "Select a valid destination"].filter( + (val, index) => validate[index] + ), + }); + } + let intermodalApiCall = axios.get(API_URL, { params: { apiKey: process.env.here_api_key, - alternatives: 10, + alternatives: 6, destination: dest, origin: origin, return: "polyline,travelSummary", - "transit[modes]": "-subway,-lightRail,-highSpeedTrain,-cityTrain", // remove undesirable transports - departureTime: departureTime, + transportMode, + departureTime, + spans: "names,length", }, }); - p.then(async (response) => { - let result = []; - let minPm = 500, - maxPm = 0; - for (var i = 0; i < response.data.routes.length; i++) { + intermodalApiCall + .then(async (response) => { + const routes = + response.data && response.data.routes ? [...response.data.routes] : []; + let result = []; + let minPm = 1000; + let maxPm = 0; + for (var i = 0; i < routes.length; i++) { + try { + const updatedRoute = await fetchCongestionAndPM( + routes[i], + maxPm, + minPm + ); + result = [...result, updatedRoute.route]; + (maxPm = updatedRoute.maxPm), (minPm = updatedRoute.minPm); + } catch (err) { + console.log(err); + } + } try { - const formattedRoute = await formatData( - response.data.routes[i], + const pmResult = getPMColor(result, minPm, maxPm); + const summary = { + routesCount: pmResult.length, + minPm, maxPm, - minPm - ); - result = [...result, formattedRoute.routeData]; - (maxPm = formattedRoute.maxPm), (minPm = formattedRoute.minPm); + }; + res.json({ routes: pmResult, summary }).status(200); } catch (err) { console.log(err); + res.json({ msg: "Error in processing data" }).status(400); } - } - try { - const pmResult = getPMColor(result, minPm, maxPm); - const finalResult = await calculateCongestion(pmResult, departureTime); - res.json(finalResult).status(200); - } catch (err) { - console.log(err); - res.json({ msg: "Error in processing data" }).status(400); - } - }).catch((error) => { - if (error.response) { - if (error.response.status == 400) { - res.json({ msg: error.response.title }).status(400); - } else if (error.response.status == 401) { + }) + .catch((error) => { + if (error.response) { + if (error.response.status == 400) { + res.json({ msg: error.response.title }).status(400); + } else if (error.response.status == 401) { + res + .json({ msg: "error while authorisation to the HERE map server" }) + .status(401); + } else { + res + .json({ + msg: "Unknown error occured while fetching the data from server", + }) + .status(error.response.status); + } + } else if (error.request) { res - .json({ msg: "error while authorisation to the HERE map server" }) - .status(401); + .json({ msg: "The request was made but no response was received" }) + .status(500); } else { - res - .json({ - msg: "Unknown error occured while fetching the data from server", - }) - .status(error.response.status); + res.json({ msg: "Internal server error" }).status(500); } - } else if (error.request) { - res - .json({ msg: "The request was made but no response was received" }) - .status(500); - } else { - res.json({ msg: "Internal server error" }).status(500); - } - }); + }); } -async function formatData(route, maxPm, minPm) { - let routeData = []; - for (let i = 0; i < route.sections.length; i++) { - try { - const pmValue = await getPM2_5( - (route.sections[i].departure.place.location.lat + - route.sections[i].arrival.place.location.lat) / - 2, - (route.sections[i].departure.place.location.lng + - route.sections[i].arrival.place.location.lng) / - 2 - ); // get PM of midpoint - maxPm = Math.max(pmValue, maxPm); - minPm = Math.min(pmValue, minPm); - // console.log(pmvalue); - let travelTime = route.sections[i].travelSummary.duration; - const sec = { - travelTime, - distance: route.sections[i].travelSummary.length, - pmValue, - begin: route.sections[i].departure.place.location, - end: route.sections[i].arrival.place.location, - transport: route.sections[i].transport, - polyline: route.sections[i].polyline, - }; - // console.log(sec) - routeData = [...routeData, sec]; - } catch (err) { - console.log(err); +async function fetchCongestionAndPM(route, maxPm, minPm) { + try { + const congestionParams = + route && route.sections + ? route.sections.map((section) => { + return { + duration: section.travelSummary.duration, + baseDuration: section.travelSummary.baseDuration, + }; + }) + : []; + const decodedPolylines = + route && route.sections + ? route.sections.map((section) => { + return decode(section.polyline).polyline; + }) + : []; + const midPointLocations = + route && route.sections + ? route.sections.map((section, index) => { + let midSpans = []; + const spanSize = section.spans.length; + for (let j = 0; j < spanSize - 1; j++) { + const lat = + decodedPolylines[index][section.spans[j].offset][0] + + decodedPolylines[index][section.spans[j + 1].offset][0] / 2; + const lng = + decodedPolylines[index][section.spans[j].offset][1] + + decodedPolylines[index][section.spans[j + 1].offset][1] / 2; + midSpans = [...midSpans, { lat, lng }]; + } + const lat = + decodedPolylines[index][section.spans[spanSize - 2].offset][0] + + decodedPolylines[index][section.spans[spanSize - 1].offset][0] / + 2; + const lng = + decodedPolylines[index][section.spans[spanSize - 2].offset][1] + + decodedPolylines[index][section.spans[spanSize - 1].offset][1] / + 2; + midSpans = [...midSpans, { lat, lng }]; + return midSpans; + }) + : []; + let spansList = []; + let exposure = 0; + for (let i = 0; i < route.sections.length; i++) { + const section = route.sections[i]; + const pmValues = await calculatePmValuesList(midPointLocations[i]); + minPm = Math.min(...pmValues); + maxPm = Math.max(...pmValues); + let spans = []; + for (let j = 0; j < section.spans.length; j++) { + exposure += section.spans[j].length * pmValues[j]; + spans = [ + ...spans, + { + ...section.spans[j], + pmValue: pmValues[j], + }, + ]; + } + spansList = [...spansList, spans]; } + const congestionData = await calculateCongestion(congestionParams); + const updatedRoute = { + ...route, + sections: + route && route.sections + ? route.sections.map((section, i) => { + return { + ...section, + travelSummary: { + ...section.travelSummary, + ...congestionData[i], + }, + spans: spansList[i], + }; + }) + : [], + routeSummary: { + exposure, + }, + }; + // console.log(updatedRoute); + return { route: updatedRoute, maxPm, minPm }; + } catch (err) { + console.log(err); } - return { routeData, maxPm, minPm }; } module.exports = getTravelData;