@geoapify/route-planner-sdk
Version:
TypeScript SDK for the Geoapify Route Planner API. Supports route optimization, delivery planning, and timeline visualization in browser and Node.js
577 lines (572 loc) • 28 kB
JavaScript
import { RoutePlannerFormatter } from "./helpers/route-planner-formatter";
export class RoutePlannerTimeline {
constructor(container, inputData, result, options) {
this.WAYPOINT_POPUP_INITIALIZED_ATTRIBUTE = 'data-rp-timeline-popup-listeners';
this.WAYPOINT_POPUP_CONTAINER_ID = 'geoapify-rp-sdk-waypoint-popup';
this.defaultColors = ["#ff4d4d", "#1a8cff", "#00cc66", "#b300b3", "#e6b800", "#ff3385",
"#0039e6", "#408000", "#ffa31a", "#990073", "#cccc00", "#cc5200", "#6666ff", "#009999"];
this.timelineTemplate = (timeline, index, timelineType, timeLabels, distanceLabels, agentMenuItems) => `
<div class="geoapify-rp-sdk-timeline-item flex-container items-center ${index % 2 === 0 ? 'geoapify-rp-sdk-even' : ''}">
<div class="geoapify-rp-sdk-timeline-item-agent flex-container items-center padding-top-5 padding-bottom-5 ${this.options.hasLargeDescription ? 'geoapify-rp-sdk-wider' : ''}">
${agentMenuItems && agentMenuItems.length > 0 ? `
<div class="geoapify-rp-sdk-three-dot-menu" data-agent-index="${timeline.agentIndex}">
<button class="geoapify-rp-sdk-three-dot-button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 512">
<path d="M64 360a56 56 0 1 0 0 112 56 56 0 1 0 0-112zm0-160a56 56 0 1 0 0 112 56 56 0 1 0 0-112zM120 96A56 56 0 1 0 8 96a56 56 0 1 0 112 0z"/>
</svg>
</button>
<ul class="geoapify-rp-sdk-menu-list">
${agentMenuItems.map(item => `
<li class="geoapify-rp-sdk-menu-item" data-key="${item.key}">${item.label}</li>
`).join('')}
</ul>
</div>
` : ''}
<div style="color: ${timeline.color}" class="flex-main geoapify-rp-sdk-agent-info margin-right-10 flex-container column">
<span class="geoapify-rp-sdk-mat-subtitle-2">${timeline.label}</span>
<span class="geoapify-rp-sdk-mat-caption description">${timeline.description}</span>
</div>
</div>
<div class="geoapify-rp-sdk-timeline flex-main" style="margin-left: 10px; position: relative; height: 100%; min-height: 45px;">
<div class="geoapify-rp-sdk-line"></div>
${timeline.timelineLength && timelineType === 'time' ? `<div class="geoapify-rp-sdk-value-line"
style="background-color: ${timeline.color}; width: ${timeline.timelineLength}; left: ${timeline.timelineLeft};"></div>
` : ''}
${timeline.distanceLineLength && timelineType === 'distance' ? `<div class="geoapify-rp-sdk-value-line"
style="background-color: ${timeline.color}; width: ${timeline.distanceLineLength};"></div>` : ''}
${timelineType === 'time' ? `
${(timeline.itemsByTime || []).map((item, i) => `
<div class="geoapify-rp-sdk-solution-item"
style="left: ${item.position}; width: ${item.minWidth || ''}" data-tooltip="${item.description}"
data-agent-index="${timeline.agentIndex}" data-waypoint-index="${i}">
${item.form === 'full' ? `<div class="geoapify-rp-sdk-solution-item-full" style="width: 100%; background-color: ${timeline.color};"></div>` : ''}
${item.form === 'standard' ? `<div class="geoapify-rp-sdk-solution-item-standard" style="background-color: ${timeline.color};"></div>` : ''}
${item.form === 'minimal' ? `<div class="geoapify-rp-sdk-solution-item-minimal" style="background-color: ${timeline.color};"></div>` : ''}
</div>`).join('')}
` : ''}
${timelineType === 'distance' ? `
${(timeline.itemsByDistance || []).map((item, i) => `<div class="geoapify-rp-sdk-solution-item" style="left: ${item.position};" data-tooltip="${item.description}"
data-agent-index="${timeline.agentIndex}" data-waypoint-index="${i}">
<div class="geoapify-rp-sdk-solution-item-minimal" style="background-color: ${timeline.color};"></div>
</div> `).join('')}
` : ''}
${timeLabels && timelineType === 'time' ? `
<div class="geoapify-rp-sdk-label-vertical-lines">
${(timeLabels || []).map(label => `
<div class="geoapify-rp-sdk-label-vertical-line" style="left: ${label.position};"></div>
`).join('')}
</div>
` : ''}
${distanceLabels && timelineType === 'distance' ? `
<div class="geoapify-rp-sdk-label-vertical-lines">
${(distanceLabels || []).map(label => `<div class="geoapify-rp-sdk-label-vertical-line" style="left: ${label.position};"></div>`).join('')}
</div>
` : ''}
<div id="global-tooltip" class="geoapify-rp-sdk-custom-tooltip" style="display: none;"></div>
</div>
</div>
`;
this.waypointPopupContainer = null;
this.eventListeners = {};
this.container = container;
this.result = result;
this.inputData = inputData;
if (options) {
this.options = options;
}
else {
this.options = {
hasLargeDescription: false,
timelineType: 'time',
agentColors: this.defaultColors,
capacityUnit: 'items'
};
}
this.generateAgentTimeline();
this.initializeThreeDotMenus();
}
getHasLargeDescription() {
return this.options.hasLargeDescription;
}
setHasLargeDescription(value) {
this.options.hasLargeDescription = value;
this.generateAgentTimeline();
}
getTimelineType() {
return this.options.timelineType;
}
setTimelineType(value) {
this.options.timelineType = value;
this.generateAgentTimeline();
}
getAgentColors() {
return this.options.agentColors;
}
setAgentColors(value) {
this.options.agentColors = value;
this.generateAgentTimeline();
}
getCapacityUnit() {
return this.options.capacityUnit;
}
setCapacityUnit(value) {
this.options.capacityUnit = value;
this.generateAgentTimeline();
}
getTimeLabels() {
return this.options.timeLabels;
}
setTimeLabels(value) {
this.options.timeLabels = value;
this.generateAgentTimeline();
}
getDistanceLabels() {
return this.options.distanceLabels;
}
setDistanceLabels(value) {
this.options.distanceLabels = value;
this.generateAgentTimeline();
}
getAgentLabel() {
return this.options.agentLabel;
}
setAgentLabel(value) {
this.options.agentLabel = value;
this.generateAgentTimeline();
}
getAgentMenuItems() {
return this.options.agentMenuItems;
}
setAgentMenuItems(value) {
this.options.agentMenuItems = value;
this.generateAgentTimeline();
this.initializeThreeDotMenus();
}
getResult() {
return this.result;
}
setResult(value) {
this.result = value;
this.generateAgentTimeline();
this.initializeThreeDotMenus();
}
on(eventName, handler) {
if (!this.eventListeners[eventName]) {
this.eventListeners[eventName] = [];
}
if (!this.eventListeners[eventName].includes(handler)) {
this.eventListeners[eventName].push(handler);
}
}
off(eventName, handler) {
if (!this.eventListeners[eventName]) {
return;
}
const index = this.eventListeners[eventName].indexOf(handler);
if (index > -1) {
this.eventListeners[eventName].splice(index, 1);
}
}
getAgentColorByIndex(index) {
return this.options.agentColors[(index % this.options.agentColors.length + this.options.agentColors.length) % this.options.agentColors.length];
}
generateAgentTimeline() {
let timelines;
if (this.result && !this.result.getRawData()) {
const maxIndex = this.result.getRawData().properties.params.agents.length;
// create timelines based on result
timelines = [];
for (let i = 0; i <= maxIndex; i++) {
timelines.push({
label: `${this.options.agentLabel ? this.options.agentLabel : 'agent'} ${i + 1}`,
mode: this.result.getData().inputData.mode,
color: this.getAgentColorByIndex(i),
description: '',
routeVisible: true,
agentIndex: i,
timelineLength: "",
distanceLineLength: "",
itemsByDistance: [],
itemsByTime: [],
timelineLeft: "100%"
});
}
this.drawTimelines(timelines, this.result);
return timelines;
}
else if (this.inputData) {
timelines = this.inputData.agents.map((agent, index) => {
var _a;
const label = `${this.options.agentLabel ? this.options.agentLabel : 'Agent'} ${index + 1}`;
if (label.length >= 10) {
this.options.hasLargeDescription = true;
}
return {
label: label,
mode: (_a = this.inputData) === null || _a === void 0 ? void 0 : _a.mode,
color: this.getAgentColorByIndex(index),
description: this.generateAgentDescription(agent, this.options.hasLargeDescription).description,
routeVisible: true,
agentIndex: index,
timelineLength: "",
distanceLineLength: "",
itemsByDistance: [],
itemsByTime: [],
timelineLeft: "100%"
};
});
this.drawTimelines(timelines, this.result);
return timelines;
}
}
drawTimelines(timelines, solution) {
if (timelines && solution) {
this.generateTimelinesData(timelines, solution);
}
this.container.innerHTML = ''; // clear
timelines === null || timelines === void 0 ? void 0 : timelines.forEach((timeline, index) => {
const html = this.timelineTemplate(timeline, index, this.options.timelineType, this.options.timeLabels || [], this.options.distanceLabels || [], this.options.agentMenuItems || []);
this.container.insertAdjacentHTML('beforeend', html);
if (this.result) {
const waypointElements = this.container.querySelectorAll('.geoapify-rp-sdk-solution-item');
waypointElements.forEach((el) => {
const agentIndex = el.getAttribute('data-agent-index');
const waypointIndex = el.getAttribute('data-waypoint-index');
const agentSolution = this.result.getAgentSolutions().find(sol => sol.getAgentIndex() === +agentIndex);
if (agentSolution && waypointIndex) {
const waypoint = agentSolution.getWaypoints()[+waypointIndex];
el.addEventListener('mouseover', () => {
this.emit('onWaypointHover', waypoint);
});
}
});
}
});
if (this.options.showWaypointPopup) {
if (this.options.waypointPopupGenerator) {
this.initializeWaypointPopups();
}
else {
this.initializeGlobalTooltip();
}
}
}
generateTimelinesData(timelines, result) {
const unit = this.options.capacityUnit || 'items';
let maxDistance = Math.max.apply(Math, result.getAgentSolutions().map((agentPlan) => {
return agentPlan.getDistance();
}));
let maxTime = Math.max.apply(Math, result.getAgentSolutions().map((agentPlan) => {
return agentPlan.getTime() + agentPlan.getStartTime();
}));
result.getAgentSolutions().forEach((agentPlan) => {
var _a, _b;
const timeline = timelines[agentPlan.getAgentIndex()];
timeline.timelineLength = ((agentPlan.getTime() - (((_a = agentPlan.getWaypoints()) === null || _a === void 0 ? void 0 : _a.length) ? agentPlan.getWaypoints()[0].getStartTime() : 0)) / maxTime * 100) + '%';
timeline.distanceLineLength = (agentPlan.getDistance() / maxDistance * 100) + '%';
timeline.itemsByDistance = [];
timeline.timelineLeft = ((agentPlan.getStartTime() || (((_b = agentPlan.getWaypoints()) === null || _b === void 0 ? void 0 : _b.length) ? agentPlan.getWaypoints()[0].getStartTime() : 0)) / maxTime * 100) + '%';
this.generateItemsByTime(timeline, agentPlan, maxTime, result, unit);
this.generateItemsByDistance(timeline, agentPlan, maxDistance);
});
}
emit(eventName, data) {
if (!this.eventListeners[eventName]) {
return;
}
this.eventListeners[eventName].forEach(handler => {
try {
handler(data);
}
catch (error) {
console.error(`Error in event handler for "${eventName}":`, error);
}
});
}
generateItemsByTime(timeline, agentPlan, maxTime, solution, unit) {
timeline.itemsByTime = [];
agentPlan.getWaypoints().forEach((waypoint, index) => {
const duration = (waypoint.getDuration() || 0);
const actualWidth = (duration / maxTime);
const descriptionItems = [];
const title = [...new Set(waypoint.getActions().map(action => action.getType().charAt(0).toUpperCase() + action.getType().slice(1)))].join(' / ');
descriptionItems.push(`${index + 1}: ${title}`);
if (duration) {
descriptionItems.push(`Duration: ${RoutePlannerFormatter.toPrettyTime(waypoint.getDuration())}`);
}
descriptionItems.push(`Time before: ${RoutePlannerFormatter.toPrettyTime(waypoint.getStartTime())}`);
descriptionItems.push(`Time after: ${RoutePlannerFormatter.toPrettyTime(waypoint.getStartTime() + duration)}`);
const actionsData = waypoint.getActions().map(action => {
const description = [];
if (action) {
let job = action.getJobIndex() && action.getJobIndex() >= 0 ? solution.getData().inputData.jobs[action.getJobIndex()] : undefined;
if (job === null || job === void 0 ? void 0 : job.pickup_amount) {
description.push(`pickup ${job.pickup_amount} ${unit}`);
}
if (job === null || job === void 0 ? void 0 : job.delivery_amount) {
description.push(`deliver ${job.delivery_amount} ${unit}`);
}
}
let shipment = action.getShipmentIndex() && action.getShipmentIndex() >= 0 ? solution.getData().inputData.shipments[action.getShipmentIndex()] : undefined;
if (shipment) {
if (shipment.amount) {
description.push(action.getType() === 'pickup' ? `pickup ${shipment.amount} ${unit}` : `deliver ${shipment.amount} ${unit}`);
}
}
return description.join(', ');
}).filter((actionDescription) => actionDescription).join('; ');
if (actionsData) {
descriptionItems.push(`Actions: ${actionsData}`);
}
const timeItem = {
type: 'job',
actualWidth: 100 * actualWidth + '%',
position: (100 * (waypoint.getStartTime() + duration / 2) / maxTime) + '%',
form: 'full',
minWidth: 100 * actualWidth + '%',
description: descriptionItems.join('\n')
};
timeline.itemsByTime.push(timeItem);
});
}
generateItemsByDistance(timeline, agentPlan, maxDistance) {
let distance = 0;
agentPlan.getLegs().forEach((leg, index) => {
const from = agentPlan.getWaypoints()[leg.getFromWaypointIndex()];
const to = agentPlan.getWaypoints()[leg.getToWaypointIndex()];
if (index === 0) {
const descriptionItems = [];
const title = [...new Set(from.getActions().map(action => action.getType().charAt(0).toUpperCase() + action.getType().slice(1)))].join(' / ');
descriptionItems.push(title);
descriptionItems.push('Distance traveled: 0');
const distanceItem = {
type: 'job',
actualWidth: "0",
position: "0",
form: 'minimal',
minWidth: "10px",
description: descriptionItems.join('\n')
};
timeline.itemsByDistance.push(distanceItem);
}
distance += leg.getDistance();
const descriptionItems = [];
const title = [...new Set(to.getActions().map(action => action.getType().charAt(0).toUpperCase() + action.getType().slice(1)))].join(' / ');
descriptionItems.push(title);
descriptionItems.push(`Distance traveled: ${RoutePlannerFormatter.toPrettyDistance(distance)}`);
const distanceItem = {
type: 'job',
actualWidth: '0',
position: (distance / maxDistance * 100) + '%',
form: 'minimal',
minWidth: "10px",
description: descriptionItems.join('\n')
};
timeline.itemsByDistance.push(distanceItem);
});
}
generateAgentDescription(agent, hasLargeDescription) {
const descriptionItems = [];
if (agent.pickup_capacity && agent.delivery_capacity) {
descriptionItems.push(`${agent.pickup_capacity} ${this.options.capacityUnit} / ${agent.delivery_capacity} ${this.options.capacityUnit}`);
}
else if (agent.pickup_capacity || agent.delivery_capacity) {
descriptionItems.push(`${agent.pickup_capacity || agent.delivery_capacity} ${this.options.capacityUnit}`);
}
if (agent.time_windows) {
descriptionItems.push(agent.time_windows.map((timeFrame) => `${RoutePlannerFormatter.toPrettyTime(timeFrame[0])}-${RoutePlannerFormatter.toPrettyTime(timeFrame[1])}`).join(', '));
}
if (agent.capabilities) {
descriptionItems.push(...agent.capabilities);
}
if (descriptionItems.join(", ").length > 20) {
hasLargeDescription = true;
}
return {
description: descriptionItems.join(', '),
hasLargeDescription: hasLargeDescription
};
}
initializeGlobalTooltip() {
if (document.getElementById('global-tooltip-listener'))
return; // already added
const idMarker = document.createElement('div');
idMarker.id = 'global-tooltip-listener';
document.body.appendChild(idMarker);
// Create global tooltip
const tooltip = document.createElement('div');
tooltip.id = 'global-tooltip';
tooltip.className = 'geoapify-rp-sdk-custom-tooltip';
tooltip.style.display = 'none';
document.body.appendChild(tooltip);
// Mouse over handler
document.addEventListener('mouseover', (e) => {
var _a;
const target = e.target;
const tooltipText = (_a = target.closest('.geoapify-rp-sdk-solution-item')) === null || _a === void 0 ? void 0 : _a.getAttribute('data-tooltip');
if (tooltipText) {
const rect = target.getBoundingClientRect();
tooltip.innerText = tooltipText;
tooltip.style.display = 'block';
tooltip.style.left = `${e.clientX}px`;
tooltip.style.top = `${rect.bottom + 6}px`;
tooltip.classList.add('geoapify-rp-sdk-show');
}
});
// Mouse out handler
document.addEventListener('mouseout', (e) => {
const target = e.target;
if (target.closest('.geoapify-rp-sdk-solution-item')) {
tooltip.classList.remove('geoapify-rp-sdk-show');
tooltip.style.display = 'none';
}
});
}
createWaypointPopupContainer() {
const existingContainer = document.getElementById(this.WAYPOINT_POPUP_CONTAINER_ID);
if (existingContainer) {
this.waypointPopupContainer = existingContainer;
}
else {
this.waypointPopupContainer = document.createElement('div');
this.waypointPopupContainer.id = 'geoapify-rp-sdk-waypoint-popup';
this.waypointPopupContainer.className = 'geoapify-rp-sdk-custom-tooltip';
this.waypointPopupContainer.style.opacity = '1';
this.waypointPopupContainer.style.display = 'none';
document.body.appendChild(this.waypointPopupContainer);
document.addEventListener('mouseover', (e) => {
if (!this.waypointPopupContainer || this.waypointPopupContainer.style.display === 'none') {
return;
}
const target = e.target;
// Check if the hover was outside the popup container AND outside a trigger element
const hoverInsidePopup = this.waypointPopupContainer.contains(target);
const hoverOnTrigger = target.closest('.geoapify-rp-sdk-solution-item') !== null;
if (!hoverInsidePopup && !hoverOnTrigger) {
this.hideWaypointPopup();
}
});
}
}
initializeWaypointPopups() {
if (this.container.getAttribute(this.WAYPOINT_POPUP_INITIALIZED_ATTRIBUTE) === 'true') {
return;
}
this.container.setAttribute('data-rp-timeline-popup-listeners', 'true');
this.createWaypointPopupContainer();
this.container.addEventListener('mouseover', (e) => {
const target = e.target;
const waypointElement = target.closest('.geoapify-rp-sdk-solution-item');
if (waypointElement) {
const agentIndex = waypointElement.getAttribute('data-agent-index');
const waypointIndex = waypointElement.getAttribute('data-waypoint-index');
if (agentIndex !== null && waypointIndex !== null) {
const agentSolution = this.result.getAgentSolutions().find(sol => sol.getAgentIndex() === +agentIndex);
if (agentSolution) {
const waypoint = agentSolution.getWaypoints()[+waypointIndex];
if (this.options.waypointPopupGenerator) {
try {
const popupContentElement = this.options.waypointPopupGenerator(waypoint);
this.showWaypointPopup(waypointElement, popupContentElement);
}
catch (error) {
console.error('Error generating waypoint popup content:', error);
}
}
else {
this.hideWaypointPopup();
}
}
}
}
});
}
showWaypointPopup(triggerElement, contentElement) {
if (!this.waypointPopupContainer) {
console.error('Waypoint popup container not initialized.');
return;
}
this.waypointPopupContainer.innerHTML = '';
this.waypointPopupContainer.appendChild(contentElement);
const rect = triggerElement.getBoundingClientRect();
this.waypointPopupContainer.style.top = `${rect.bottom + window.scrollY + 10}px`;
let left = rect.left + window.scrollX + (rect.width / 2) - (this.waypointPopupContainer.offsetWidth / 2);
const viewportWidth = window.innerWidth;
const popupWidth = this.waypointPopupContainer.offsetWidth;
if (left + popupWidth > viewportWidth - 10) {
left = viewportWidth - popupWidth - 10;
}
if (left < 10) {
left = 10;
}
this.waypointPopupContainer.style.left = `${left}px`;
this.waypointPopupContainer.style.display = 'block';
}
hideWaypointPopup() {
if (this.waypointPopupContainer) {
this.waypointPopupContainer.style.display = 'none';
}
}
initializeThreeDotMenus() {
if (this.container.getAttribute('data-rp-timeline-menu-listeners') === 'true') {
return;
}
this.container.setAttribute('data-rp-timeline-menu-listeners', 'true');
this.container.addEventListener('click', (e) => {
const target = e.target;
const threeDotButton = target.closest('.geoapify-rp-sdk-three-dot-button');
if (threeDotButton) {
const threeDotMenu = threeDotButton.closest('.geoapify-rp-sdk-three-dot-menu');
if (threeDotMenu) {
this.toggleThreeDotMenu(threeDotMenu);
}
}
});
this.container.addEventListener('click', (e) => {
const target = e.target;
const menuItem = target.closest('.geoapify-rp-sdk-menu-item');
if (menuItem) {
const threeDotMenu = menuItem.closest('.geoapify-rp-sdk-three-dot-menu');
const agentIndexAttr = threeDotMenu === null || threeDotMenu === void 0 ? void 0 : threeDotMenu.getAttribute('data-agent-index');
const agentIndex = agentIndexAttr ? +agentIndexAttr : -1;
const key = menuItem.getAttribute('data-key');
if (key && agentIndex !== -1 && this.options.agentMenuItems) {
const selectedMenuItem = this.options.agentMenuItems.find(item => item.key === key);
if (selectedMenuItem && selectedMenuItem.callback) {
selectedMenuItem.callback(agentIndex);
}
}
this.closeAllThreeDotMenus();
}
});
if (!document.getElementById('geoapify-rp-sdk-document-click-listener-flag')) {
const flag = document.createElement('div');
flag.id = 'geoapify-rp-sdk-document-click-listener-flag';
flag.style.display = 'none';
document.body.appendChild(flag);
document.addEventListener('click', (e) => {
const target = e.target;
const isClickInsideMenuOrButton = target.closest('.geoapify-rp-sdk-three-dot-menu') !== null || target.closest('.geoapify-rp-sdk-three-dot-button') !== null;
if (!isClickInsideMenuOrButton) {
this.closeAllThreeDotMenus();
}
});
}
}
toggleThreeDotMenu(threeDotMenuElement) {
const menuList = threeDotMenuElement.querySelector('.geoapify-rp-sdk-menu-list');
if (menuList) {
this.closeAllThreeDotMenus(threeDotMenuElement);
menuList.style.display = menuList.style.display === 'block' ? 'none' : 'block';
}
}
closeAllThreeDotMenus(excludeMenuElement) {
const openMenus = this.container.querySelectorAll('.geoapify-rp-sdk-menu-list');
openMenus.forEach(menu => {
const menuElement = menu.closest('.geoapify-rp-sdk-three-dot-menu');
if (menuElement !== excludeMenuElement) {
menu.style.display = 'none';
}
});
}
}