/** * CO2Calculator.js 1.0.0 * Description * * Copyright 2020, Avidly Denmark ApS * https://www.avidlyagency.com/ * * Licensed under MIT * * Released on: Marts, 2020 */ (function () { 'use strict'; let self, autocompleteOrigin, autocompleteDestination, terminalRoutes, vesselRoutes, vesselPorts, csvFiles, co2Factors; const vesselPortsAddresses = []; const calculatedRoutes = { unifeeder: [], alternative: [] }; // Define constructor this.CO2Calculator = function () { self = this; // Define option defaults const defaults = { selectors: { origin: null, destination: null, pol: null, pod: null, containers: null, submit: null, }, co2Factors: { truck: 62, vessel: 51.66, rail: 22, el_rail: 22, barge: 33.33 }, containerWeight: 15000, googleMapsAPI: "", restrictedCountries: [], on: { submit: null, success: null, error: null }, onCompleted: null }; // Create options by extending defaults with the passed in arguments if (arguments[0] && typeof arguments[0] === "object") { co2Factors = extendDefaults(defaults.co2Factors, arguments[0].co2Factors); self.options = extendDefaults(defaults, arguments[0]); } csvFiles = { vesselPorts: this.options.filesPath + "vessel-ports.csv", vesselRoutes: this.options.filesPath + "vessel-routes.csv", terminalRoutes: this.options.filesPath + "terminal-routes.csv" } document.addEventListener("onSubmit", function (e) { if (typeof self.options.on.submit === "function") { self.options.on.submit(e.detail.button); } }); document.addEventListener("onSuccess", function (e) { if (typeof self.options.on.success === "function") { self.options.on.success(e.detail.result); } }); document.addEventListener("onError", function (e) { if (typeof self.options.on.error === "function") { self.options.on.error(e.detail.message); } }); self.options.selectors.submit.addEventListener("click", function () { triggerEvent("onSubmit", {button: this}); // Reset routes object container calculatedRoutes.unifeeder = []; calculatedRoutes.alternative = []; // Calculate route by truck for alternative route. calculateRouteByTruck(self.options.selectors.origin.value, self.options.selectors.destination.value, function (distance) { calculatedRoutes.alternative.push({ transport: "truck", origin: self.options.selectors.origin.value, destination: self.options.selectors.destination.value, distance: distance, co2: getCalculateCO2Emission("truck", distance) }); }); // Next calculate Unifeeder route. calculateUnifeederRoute(); }); getDataFromLocalCSV(csvFiles.terminalRoutes, {}, function (data) { terminalRoutes = data; }); getDataFromLocalCSV(csvFiles.vesselRoutes, [], function (data) { vesselRoutes = data; if(self.options.selectors.pol && self.options.selectors.pod) setupAutocompletePortInputs(); }); getDataFromLocalCSV(csvFiles.vesselPorts, {}, function (data) { vesselPorts = data; const ports = Object.values(vesselPorts); for (let i = 0; i < ports.length; i++) { vesselPortsAddresses.push(ports[i].address); } }); loadGoogleMapsAPI(); }; // Extend defaults with user options function extendDefaults(source, properties) { let property; for (property in properties) { if (properties.hasOwnProperty(property)) { source[property] = properties[property]; } } return source; } function loadGoogleMapsAPI() { const existingMapsScript = document.getElementById("google-maps-api"); if (!existingMapsScript) { const script = document.createElement("script"); script.id = "google-maps-api"; script.src = "https://maps.googleapis.com/maps/api/js?key=" + self.options.googleMapsAPI + "&libraries=places&language=en"; document.body.appendChild(script); script.onload = function () { setupAutocompleteOnInputs(); } } if (existingMapsScript) setupAutocompleteOnInputs(); } function setupAutocompleteOnInputs() { const options = { componentRestrictions: {country: self.options.restrictedCountries} }; autocompleteOrigin = new google.maps.places.Autocomplete(self.options.selectors.origin, options); autocompleteDestination = new google.maps.places.Autocomplete(self.options.selectors.destination, options); } function setupAutocompletePortInputs() { const pol = self.options.selectors.pol; const pod = self.options.selectors.pod; const vesselRoutesArray = Object.keys(vesselRoutes); let options = ``; for (let i = 0; i < vesselRoutesArray.length; i++) options += ``; pol.innerHTML = options; pod.innerHTML = ``; pod.disabled = true; pol.addEventListener('change', function () { const key = Object.keys(vesselRoutes)[pol.options.selectedIndex - 1]; const routes = vesselRoutes[key] ?? []; let options = ``; for (let i = 0; i < routes.length; i++) options += ``; pod.innerHTML = options; pod.disabled = pol.options.selectedIndex === 0; }); } function calculateUnifeederRoute() { const place = autocompleteOrigin.getPlace(); const latlng = {lat: place.geometry.location.lat(), lng: place.geometry.location.lng()}; getCountryAndPostalCodes(autocompleteOrigin, function (codes) { const {countryCode, postalCode} = codes; getDataFromLocalCSV(self.options.filesPath + "postal-codes/" + countryCode + ".csv", {}, function (data) { const route = data[postalCode]; if (route === undefined) return triggerEvent("onError", {message: "Postal code " + postalCode + " doesn't exist in " + countryCode + ".csv."}); const pol = self.options.selectors.pol?.options[self.options.selectors.pol?.options.selectedIndex].value; if(pol) { const polPossiblePorts = Object.keys(terminalRoutes).filter(key => terminalRoutes[key].port === pol); let polPossibleAddresses = polPossiblePorts.map(port => terminalRoutes[port].address); polPossibleAddresses.unshift(vesselPorts[pol].address); getNearestPortIndexByAddress(latlng, polPossibleAddresses, function (nearestPortIndex) { if(nearestPortIndex > 0) { const portCode = polPossiblePorts[nearestPortIndex - 1]; addStepsToUnifeederRoute(latlng, terminalRoutes[portCode], portCode, pol); } else { addStepsToUnifeederRoute(latlng, vesselPorts[pol], false, pol); } }); } else { // Does the route contain pre-carriage if (route.terminal && terminalRoutes[route.terminal]) { const truckDestination = terminalRoutes[route.terminal]; addStepsToUnifeederRoute(latlng, truckDestination, route.terminal, truckDestination.port); } else { getNearestPortIndexByAddress(latlng, vesselPortsAddresses, function (nearestPortIndex) { const portCode = Object.keys(vesselPorts)[nearestPortIndex]; addStepsToUnifeederRoute(latlng, vesselPorts[portCode], false, portCode); }); } } }); }); } function getCountryAndPostalCodes(autocompleteField, callback) { const place = autocompleteField.getPlace(); const latlng = {lat: place.geometry.location.lat(), lng: place.geometry.location.lng()}; const geocoder = new google.maps.Geocoder; geocoder.geocode({'location': latlng}, function (results, status) { if (status !== "OK") triggerEvent("onError", {message: status}); const addressComponents = results[0].address_components; let countryCode = addressComponents.reduce((acc, line) => line.types.includes("country") ? line.short_name.toLowerCase() : acc, {}); let postalCode = addressComponents.reduce((acc, line) => line.types.includes("postal_code") ? line.long_name : acc, null); if(!postalCode) return triggerEvent("onError", {message: "Can't find postal code from: " + place.formatted_address}); if (countryCode === "gb" || countryCode === "pt") { postalCode = postalCode.split(' ')[0]; } else { postalCode = postalCode.replace(/\D/g, ''); } return callback({countryCode, postalCode}); }); } function addStepsToUnifeederRoute(trackOrigin, truckDestination, addPreCarriage, departPortCode) { const allVesselRoutes = vesselRoutes[departPortCode], possibleVesselRoutes = [], possibleVesselRoutesAddresses = []; if (allVesselRoutes === undefined) return triggerEvent("onError", {message: "Port " + departPortCode + " doesn't exist in vessel-routes.csv."}); if (vesselPorts[departPortCode] === undefined) return triggerEvent("onError", {message: "Port " + departPortCode + " doesn't exist in vessel-ports.csv."}); getCountryAndPostalCodes(autocompleteDestination, function (codes) { const {countryCode, postalCode} = codes; getDataFromLocalCSV(self.options.filesPath + "/postal-codes/" + countryCode + ".csv", {}, function (data) { const onCarriageRoute = data[postalCode]; if (onCarriageRoute === undefined) return triggerEvent("onError", {message: "Postal code " + postalCode + " doesn't exist in " + countryCode + ".csv."}); const pod = self.options.selectors.pod?.options[self.options.selectors.pod?.options.selectedIndex].value; if(pod) { const pol = self.options.selectors.pol?.options[self.options.selectors.pol?.options.selectedIndex].value; const vesselRoute = vesselRoutes[pol].reduce((acc, port) => pod === port.arrival ? port : acc, null); possibleVesselRoutes.push(vesselRoute); possibleVesselRoutesAddresses.push(vesselPorts[pol].address); } else { // Find possible vessel routes inside same country for (let p = 0; p < allVesselRoutes.length; p++) { const vesselPort = vesselPorts[allVesselRoutes[p].arrival]; if (vesselPort === undefined) return triggerEvent("onError", {message: "Port " + allVesselRoutes[p].arrival + " doesn't exist in vessel-ports.csv"}); if (vesselPort.country.toUpperCase() !== countryCode.toUpperCase()) continue; possibleVesselRoutes.push(allVesselRoutes[p]); possibleVesselRoutesAddresses.push(vesselPort.address); } // If we can't find and possible vessel routes inside same country, expand to search in all vessel routes from that port. if(possibleVesselRoutes.length === 0) { const port = allVesselRoutes.reduce((acc, port) => terminalRoutes[onCarriageRoute.terminal]?.port === port.arrival ? port : acc, null); // Is there on-carriage? if(port) { possibleVesselRoutes.push(port); possibleVesselRoutesAddresses.push(vesselPorts[port.arrival].address); } else { for (let p = 0; p < allVesselRoutes.length; p++) { possibleVesselRoutes.push(allVesselRoutes[p]); possibleVesselRoutesAddresses.push(vesselPorts[allVesselRoutes[p].arrival].address); } } } } getNearestPortIndexByAddress(self.options.selectors.destination.value, possibleVesselRoutesAddresses, function (nearestPortIndex) { const nearestPort = possibleVesselRoutes[nearestPortIndex]; const addOnCarriageRoute = onCarriageRoute.terminal && terminalRoutes[onCarriageRoute.terminal].port === nearestPort.arrival; const truckOriginAddress = addOnCarriageRoute ? terminalRoutes[onCarriageRoute.terminal].address : vesselPorts[nearestPort.arrival].address; // Calculate first truck route calculateRouteByTruck(trackOrigin, truckDestination.address, function (distance) { // Add first truck route to unifeeder array calculatedRoutes.unifeeder.push({ transport: "truck", origin: self.options.selectors.origin.value, destination: truckDestination.address, distance: distance, type: "km", co2: getCalculateCO2Emission("truck", distance) }); // Add rail route if necessary if (addPreCarriage) { const transport = (truckDestination.electriefied === "YES") ? "el_rail" : truckDestination.mode.toLowerCase(); calculatedRoutes.unifeeder.push({ transport: truckDestination.mode.toLowerCase(), origin: truckDestination.address, destination: vesselPorts[departPortCode].address, distance: truckDestination.distance, type: "km", co2: getCalculateCO2Emission(transport, truckDestination.distance) }); } // Add vessel route calculatedRoutes.unifeeder.push({ transport: "vessel", origin: vesselPorts[departPortCode].address + " (" + departPortCode + ")", destination: vesselPorts[nearestPort.arrival].address + " (" + nearestPort.arrival + ")", distance: nearestPort.distance * 0.6214, type: "miles", co2: getCalculateCO2Emission("vessel", nearestPort.distance) }); // Does the route contain on-carriage if (addOnCarriageRoute) { const railDetails = terminalRoutes[onCarriageRoute.terminal]; // const transport = (railDetails.electriefied === "YES") ? "el_rail" : truckDestination.mode.toLowerCase(); const transport = (railDetails.electriefied === "YES") ? "el_rail" : railDetails.mode.toLowerCase(); calculatedRoutes.unifeeder.push({ transport: railDetails.mode.toLowerCase(), origin: vesselPorts[nearestPort.arrival].address, destination: railDetails.address, distance: railDetails.distance, type: "km", co2: getCalculateCO2Emission(transport, railDetails.distance) }); } // Add last truck route and end calculation calculateRouteByTruck(truckOriginAddress, self.options.selectors.destination.value, function (data) { calculatedRoutes.unifeeder.push({ transport: "truck", origin: truckOriginAddress, destination: self.options.selectors.destination.value, distance: data, type: "km", co2: getCalculateCO2Emission("truck", data) }); triggerEvent("onSuccess", {result: calculatedRoutes}); }); }); }); }); }); } function getNearestPortIndexByAddress(origin, portAddresses, callback) { const addressChunks = [], addresses = [], increase = 25; let indexedAddresses = 0, increaseIndex = 0; for (let i = 0; i < portAddresses.length; i += increase) { addressChunks.push(portAddresses.slice(i, i + increase)); } for (let i = 0; i < addressChunks.length; i++) { getNearestAddress(origin, addressChunks[i], increaseIndex, function (routes) { indexedAddresses += routes.length; for (let r = 0; r < routes.length; r++) { addresses.push(routes[r]); } if (indexedAddresses === portAddresses.length) { addresses.sort(sortByDistDM); const routesPriority = []; for (let a = 0; a < addresses.length; a++) { routesPriority.push(addresses[a].index); } return callback(routesPriority[0]); } }); increaseIndex += increase; } } function getNearestAddress(origin, addresses, startIndex, callback) { const distanceMatrixService = new google.maps.DistanceMatrixService(); const matrixRequest = { origins: [origin], destinations: addresses, travelMode: google.maps.TravelMode.DRIVING, avoidFerries: true }; distanceMatrixService.getDistanceMatrix(matrixRequest, function (response, status) { if (status !== "OK") triggerEvent("onError", {message: status}); const routes = response.rows[0].elements; for (let i = 0; i < routes.length; i++) { const route = routes[i]; route.index = startIndex + i; } return callback(routes); }); } function calculateRouteByTruck(origin, destination, callback) { const directionsService = new google.maps.DirectionsService(); const request = { origin: origin, destination: destination, travelMode: google.maps.TravelMode.DRIVING, avoidFerries: true }; directionsService.route(request, function (response, status) { if (status !== "OK") return triggerEvent("onError", {message: "Can't find any route between " + origin + " and " + destination}); const distance = response.routes[0].legs[0].distance.value / 1000; callback(distance); }); } function sortByDistDM(a, b) { return (a.distance.value - b.distance.value) } function getCalculateCO2Emission(transport, distance) { const payload = self.options.selectors.containers.value * self.options.containerWeight || 0 return (((distance * payload) / 1000) * co2Factors[transport]) / 1000; } function triggerEvent(eventName, data) { let event; if (window.CustomEvent) { event = new CustomEvent(eventName, {detail: data}); } else { event = document.createEvent('CustomEvent'); event.initCustomEvent(eventName, true, true, {detail: data}); } document.dispatchEvent(event); } function getDataFromLocalCSV(url, subDataType, callback) { const request = new XMLHttpRequest(); request.open("GET", url, true); request.onload = function () { const lines = request.responseText.split(/\r\n|\n/); const headers = lines[0].split(';'); const result = {}; for (let i = 1; i < lines.length; i++) { const data = lines[i].split(';'); if (data.length === headers.length) { const line = {}; for (let j = 1; j < headers.length; j++) { line[headers[j].toLowerCase()] = isNaN(data[j]) ? data[j] : Number(data[j]); } if (Array.isArray(subDataType)) { if (!(data[0] in result)) result[data[0]] = []; result[data[0]].push(line); } else { result[data[0]] = line; } } } return callback(result); }; request.send(); } }).call(this);