UNPKG

@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

740 lines (735 loc) 35.4 kB
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' : ''} agent-${index}"> <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 fill="currentColor" 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.filter(item => !item.hidden).map(item => ` <li class="geoapify-rp-sdk-menu-item ${item.disabled ? 'geoapify-rp-sdk-menu-item-disabled' : ''}" 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 && timeLabels.length > 0 && 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 && distanceLabels.length > 0 && 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', showTimelineLabels: false }; } 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(); } getShowTimelineLabels() { return this.options.showTimelineLabels; } setShowTimelineLabels(value) { this.options.showTimelineLabels = 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.inputData = value.getRoutingOptions(); 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.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; } return []; } drawTimelines(timelines, solution) { if (timelines && solution) { this.generateTimelinesData(timelines, solution); } this.container.innerHTML = ''; // clear if (this.options.showTimelineLabels) { const labels = this.options.timelineType === 'distance' ? this.getDistanceLabels() : this.getTimeLabels(); if (labels && labels.length > 0) { const labelsHtml = labels.map((label, labelIndex) => { let transform = 'translateX(-50%)'; if (labelIndex === 0) { transform = 'translateX(0)'; } else if (labelIndex === labels.length - 1) { transform = 'translateX(-100%)'; } return `<span class="geoapify-rp-sdk-timeline-label" style="left: ${label.position}; transform: ${transform};">${label.label}</span>`; }).join(''); const widerClass = this.options.hasLargeDescription ? 'geoapify-rp-sdk-wider' : ''; const headerHtml = ` <div class="geoapify-rp-sdk-timeline-labels-row flex-container items-center"> <div class="geoapify-rp-sdk-timeline-item-agent flex-container items-center padding-top-5 padding-bottom-5 ${widerClass}"></div> <div class="geoapify-rp-sdk-timeline-labels-container flex-main"> <div class="geoapify-rp-sdk-timeline-labels">${labelsHtml}</div> </div> </div> `; this.container.insertAdjacentHTML('beforeend', headerHtml); } } timelines === null || timelines === void 0 ? void 0 : timelines.forEach((timeline, index) => { const html = this.timelineTemplate(timeline, index, this.options.timelineType, this.getTimeLabels() || [], this.getDistanceLabels() || [], 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 agentPlan = this.result.getAgentPlan(+agentIndex); if (agentPlan && waypointIndex) { const waypoint = agentPlan.getWaypoints()[+waypointIndex]; el.addEventListener('mouseover', () => { this.emit('onWaypointHover', waypoint, +agentIndex); }); el.addEventListener('click', () => { this.emit('onWaypointClick', waypoint, +agentIndex); }); } }); } 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.getAgentPlans().map((agentPlan) => { if (!agentPlan) { return 0; } return agentPlan.getDistance(); })); let maxTime = Math.max.apply(Math, result.getAgentPlans().map((agentPlan) => { if (!agentPlan) { return 0; } return agentPlan.getEndTime(); })); if (this.options.showTimelineLabels) { const labelWidth = this.getTimelineLabelsWidth(); if ((!this.options.timeLabels || this.options.timeLabels.length === 0) && maxTime > 0) { this.options.timeLabels = this.generateTimeLabels(maxTime, labelWidth); } if ((!this.options.distanceLabels || this.options.distanceLabels.length === 0) && maxDistance > 0) { this.options.distanceLabels = this.generateDistanceLabels(maxDistance, labelWidth); } } result.getAgentPlans() .filter((agentPlan) => agentPlan !== undefined).forEach((agentPlan) => { const timeline = timelines[agentPlan.getAgentIndex()]; timeline.timelineLength = ((agentPlan.getEndTime() - agentPlan.getStartTime()) / maxTime * 100) + '%'; timeline.distanceLineLength = (agentPlan.getDistance() / maxDistance * 100) + '%'; timeline.itemsByDistance = []; timeline.timelineLeft = (agentPlan.getStartTime() / maxTime * 100) + '%'; this.generateItemsByTime(timeline, agentPlan, maxTime, result, unit); this.generateItemsByDistance(timeline, agentPlan, maxDistance); }); } emit(eventName, ...args) { if (!this.eventListeners[eventName]) { return; } this.eventListeners[eventName].forEach(handler => { handler(...args); }); } 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); }); } generateTimeLabels(maxTime, width) { if (!Number.isFinite(maxTime) || maxTime <= 0) { return []; } const minLabelPx = 70; const maxLabels = Math.max(2, Math.floor((width || 600) / minLabelPx)); const stepCandidatesHours = [0.5, 1, 2, 3, 4, 6, 12, 24, 48]; const totalHours = maxTime / 3600; let stepHours = stepCandidatesHours[stepCandidatesHours.length - 1]; for (const candidate of stepCandidatesHours) { const count = Math.floor(totalHours / candidate) + 1; if (count <= maxLabels) { stepHours = candidate; break; } } const stepSeconds = stepHours * 3600; const rawLabels = []; for (let t = 0; t <= maxTime; t += stepSeconds) { rawLabels.push({ time: t, position: (t / maxTime * 100) + '%', label: RoutePlannerFormatter.toPrettyTime(t) }); } if (rawLabels.length === 0 || rawLabels[rawLabels.length - 1].time < maxTime) { rawLabels.push({ time: maxTime, position: '100%', label: RoutePlannerFormatter.toPrettyTime(maxTime) }); } if (rawLabels.length > 1 && width > 0) { const last = rawLabels[rawLabels.length - 1]; const prev = rawLabels[rawLabels.length - 2]; const pxGap = (last.time - prev.time) / maxTime * width; if (pxGap < minLabelPx) { rawLabels.splice(rawLabels.length - 2, 1); } } return rawLabels.map(({ position, label }) => ({ position, label })); } generateDistanceLabels(maxDistance, width) { if (!Number.isFinite(maxDistance) || maxDistance <= 0) { return []; } const minLabelPx = 70; const maxLabels = Math.max(2, Math.floor((width || 600) / minLabelPx)); const stepCandidatesKm = [0.5, 1, 2, 5, 10, 20, 25, 50, 100, 200]; const totalKm = maxDistance / 1000; let stepKm = stepCandidatesKm[stepCandidatesKm.length - 1]; for (const candidate of stepCandidatesKm) { const count = Math.floor(totalKm / candidate) + 1; if (count <= maxLabels) { stepKm = candidate; break; } } const stepMeters = stepKm * 1000; const rawLabels = []; for (let d = 0; d <= maxDistance; d += stepMeters) { rawLabels.push({ distance: d, position: (d / maxDistance * 100) + '%', label: RoutePlannerFormatter.toPrettyDistance(Math.round(d)) }); } if (rawLabels.length === 0 || rawLabels[rawLabels.length - 1].distance < maxDistance) { rawLabels.push({ distance: maxDistance, position: '100%', label: RoutePlannerFormatter.toPrettyDistance(Math.round(maxDistance)) }); } if (rawLabels.length > 1 && width > 0) { const last = rawLabels[rawLabels.length - 1]; const prev = rawLabels[rawLabels.length - 2]; const pxGap = (last.distance - prev.distance) / maxDistance * width; if (pxGap < minLabelPx) { rawLabels.splice(rawLabels.length - 2, 1); } } return rawLabels.map(({ position, label }) => ({ position, label })); } getTimelineLabelsWidth() { var _a; const containerWidth = ((_a = this.container) === null || _a === void 0 ? void 0 : _a.clientWidth) || 0; const agentWidth = this.options.hasLargeDescription ? 200 : 140; const gap = 10; const width = containerWidth - agentWidth - gap; if (width > 0) { return width; } return containerWidth || 600; } 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 agentPlan = this.result.getAgentPlan(+agentIndex); if (agentPlan) { const waypoint = agentPlan.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(); const popupPosition = window.getComputedStyle(this.waypointPopupContainer).position; const offsetY = popupPosition === 'fixed' ? 0 : window.scrollY; const offsetX = popupPosition === 'fixed' ? 0 : window.scrollX; this.waypointPopupContainer.style.top = `${rect.bottom + offsetY + 10}px`; let left = rect.left + offsetX + (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.generateAgentMenuItemsOnThreeDotClick(threeDotMenu); this.toggleThreeDotMenu(threeDotMenu); } } }); this.container.addEventListener('click', (e) => { const target = e.target; const menuItem = target.closest('.geoapify-rp-sdk-menu-item'); if (menuItem) { if (menuItem.classList.contains('geoapify-rp-sdk-menu-item-disabled')) { return; // Don't process clicks on disabled items } 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(); } }); } } generateAgentMenuItemsOnThreeDotClick(threeDotMenu) { const agentIndexAttr = threeDotMenu.getAttribute('data-agent-index'); const agentIndex = agentIndexAttr ? +agentIndexAttr : -1; if (agentIndex !== -1) { const processedMenuItems = this.generateMenuItemsForAgent(agentIndex); this.updateMenuItems(threeDotMenu, processedMenuItems); } } generateMenuItemsForAgent(agentIndex) { let menuItems = [...(this.options.agentMenuItems || [])]; if (this.eventListeners['beforeAgentMenuShow'] && this.eventListeners['beforeAgentMenuShow'].length > 0) { const handler = this.eventListeners['beforeAgentMenuShow'][0]; // Only use the first (and should be only) handler try { const result = handler(agentIndex, menuItems); if (result !== undefined) { menuItems = result; } } catch (error) { console.error(`Error in beforeAgentMenuShow event handler:`, error); } } return menuItems; } updateMenuItems(threeDotMenu, menuItems) { const menuList = threeDotMenu.querySelector('.geoapify-rp-sdk-menu-list'); if (menuList) { const menuItemsHtml = menuItems.filter(item => !item.hidden).map(item => ` <li class="geoapify-rp-sdk-menu-item ${item.disabled ? 'geoapify-rp-sdk-menu-item-disabled' : ''}" data-key="${item.key}">${item.label}</li> `).join(''); menuList.innerHTML = menuItemsHtml; } } 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'; } }); } }