/**
* 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);