Spaces:
Sleeping
Sleeping
| /* app.js - lifecycle bootstrap for the Bergamo field service routing demo */ | |
| (async function () { | |
| 'use strict'; | |
| var DAY_START = 8 * 60; | |
| var DAY_END = 18 * 60; | |
| var DEFAULT_CENTER = [45.698, 9.677]; | |
| var utils = window.FSR.utils; | |
| var config = await fetch('/sf-config.json').then(function (response) { return response.json(); }); | |
| var uiModel = await fetch('/generated/ui-model.json').then(function (response) { return response.json(); }); | |
| var app = document.getElementById('sf-app'); | |
| app.className = 'sf-app fsr-app'; | |
| var backend = SF.createBackend({ baseUrl: '' }); | |
| var statusBar = SF.createStatusBar({ constraints: uiModel.constraints || [] }); | |
| var currentPlan = null; | |
| var currentRoutes = null; | |
| var demoData = null; | |
| var bootstrapError = null; | |
| var lastAnalysis = null; | |
| var routeMap = null; | |
| var renderer = null; | |
| var routeGeometry = null; | |
| var focusedRouteId = null; | |
| var layout = window.FSR.createAppLayout({ | |
| SF: SF, | |
| app: app, | |
| config: config, | |
| statusBar: statusBar, | |
| onSolve: function () { loadAndSolve(); }, | |
| onPause: function () { pauseSolve(); }, | |
| onResume: function () { resumeSolve(); }, | |
| onCancel: function () { cancelSolve(); }, | |
| onAnalyze: function () { openAnalysis(); }, | |
| onMapTabShown: function () { | |
| window.setTimeout(function () { | |
| if (routeMap && routeMap.map && routeMap.map.invalidateSize) routeMap.map.invalidateSize(); | |
| renderCurrentPlan(); | |
| }, 80); | |
| }, | |
| }); | |
| demoData = window.FSR.createDemoDataController({ | |
| SF: SF, | |
| getCurrentPlan: function () { return currentPlan; }, | |
| onCatalog: function () { if (renderer) renderer.renderApiGuide(); }, | |
| onError: reportBootstrapError, | |
| onLoadingChange: updateSolveActionAvailability, | |
| onPlan: handleDemoPlanLoaded, | |
| utils: utils, | |
| }); | |
| app.insertBefore(demoData.el, layout.bootstrapNotice.nextSibling); | |
| routeMap = SF.map.create({ container: 'fsr-map', center: DEFAULT_CENTER, zoom: 13 }); | |
| renderer = window.FSR.createRenderer({ | |
| SF: SF, | |
| apiGuideContainer: layout.apiGuideContainer, | |
| dayEnd: DAY_END, | |
| dayStart: DAY_START, | |
| getFocusedRouteId: function () { return focusedRouteId; }, | |
| getDemoCatalog: function () { return demoData.getCatalog(); }, | |
| getSelectedDemoId: function () { return demoData.getSelectedId(); }, | |
| onFocusRoute: focusRoute, | |
| routeCards: layout.routeCards, | |
| routeMap: routeMap, | |
| summaryContainer: layout.summaryContainer, | |
| tablesContainer: layout.tablesContainer, | |
| timelineContainer: layout.timelineContainer, | |
| }); | |
| var analysisModal = SF.createModal({ title: 'Score Analysis', width: '760px' }); | |
| var solver = SF.createSolver({ | |
| backend: backend, | |
| statusBar: statusBar, | |
| onProgress: function (meta) { syncLifecycleMarkers(meta); }, | |
| onPauseRequested: function (meta) { syncLifecycleMarkers(meta); }, | |
| onSolution: function (snapshot, meta) { | |
| renderSnapshot(snapshot, meta); | |
| syncLifecycleMarkers(meta); | |
| }, | |
| onPaused: function (snapshot, meta) { | |
| renderSnapshot(snapshot, meta); | |
| syncLifecycleMarkers(meta); | |
| }, | |
| onResumed: function (meta) { syncLifecycleMarkers(meta); }, | |
| onCancelled: function (snapshot, meta) { | |
| renderSnapshot(snapshot, meta); | |
| syncLifecycleMarkers(meta); | |
| }, | |
| onComplete: function (snapshot, meta) { | |
| renderSnapshot(snapshot, meta); | |
| syncLifecycleMarkers(meta); | |
| }, | |
| onFailure: function (message, meta, snapshot, analysis) { | |
| renderSnapshot(snapshot, meta); | |
| if (analysis) lastAnalysis = analysis; | |
| console.error('Solver job failed:', message); | |
| syncLifecycleMarkers(meta); | |
| }, | |
| onAnalysis: function (analysis) { | |
| lastAnalysis = analysis; | |
| syncLifecycleMarkers(); | |
| }, | |
| onError: function (message) { | |
| console.error('Solver lifecycle failed:', message); | |
| syncLifecycleMarkers(); | |
| }, | |
| }); | |
| routeGeometry = window.FSR.createRouteGeometryController({ | |
| solver: solver, | |
| utils: utils, | |
| onClearFocus: function () { focusedRouteId = null; }, | |
| onRoutesChange: function (routes) { | |
| currentRoutes = routes; | |
| renderCurrentPlan(); | |
| }, | |
| }); | |
| renderer.renderApiGuide(); | |
| updateSolveActionAvailability(); | |
| demoData.bootstrap(); | |
| window.addEventListener('beforeunload', function () { renderer.destroy(); }); | |
| function loadAndSolve() { | |
| if (solver.isRunning() || solver.getLifecycleState() === 'PAUSED' || !canSolve()) return; | |
| routeGeometry.invalidate(); | |
| cleanupTerminalJob() | |
| .then(function () { return demoData.resolvePlan(); }) | |
| .then(function (data) { return solver.start(data); }) | |
| .then(function () { syncLifecycleMarkers(); }) | |
| .catch(function (err) { console.error('Solve start failed:', err); }); | |
| } | |
| function pauseSolve() { | |
| solver.pause().then(function () { syncLifecycleMarkers(); }).catch(function (err) { console.error('Pause failed:', err); }); | |
| } | |
| function resumeSolve() { | |
| solver.resume().then(function () { syncLifecycleMarkers(); }).catch(function (err) { console.error('Resume failed:', err); }); | |
| } | |
| function cancelSolve() { | |
| solver.cancel().then(function () { syncLifecycleMarkers(); }).catch(function (err) { console.error('Cancel failed:', err); }); | |
| } | |
| function openAnalysis() { | |
| var jobId = solver.getJobId(); | |
| if (jobId == null || jobId === '') return; | |
| solver.analyzeSnapshot() | |
| .then(function (analysis) { | |
| lastAnalysis = analysis; | |
| analysisModal.setBody(renderer.buildAnalysisBody(analysis)); | |
| analysisModal.open(); | |
| }) | |
| .catch(function (err) { console.error('Analysis failed:', err); }); | |
| } | |
| function cleanupTerminalJob() { | |
| var state = solver.getLifecycleState(); | |
| var jobId = solver.getJobId(); | |
| if (jobId == null || jobId === '' || state === 'IDLE' || state === 'PAUSED' || solver.isRunning()) { | |
| return Promise.resolve(null); | |
| } | |
| return solver.delete().then(function () { | |
| lastAnalysis = null; | |
| syncLifecycleMarkers(); | |
| }); | |
| } | |
| function renderSnapshot(snapshot, meta) { | |
| var identity = routeGeometry.identityFrom(meta); | |
| if (snapshot && snapshot.solution) { | |
| routeGeometry.setPlanIdentity(identity); | |
| renderPlan(snapshot.solution); | |
| } | |
| routeGeometry.load(identity); | |
| } | |
| function handleDemoPlanLoaded(plan) { | |
| lastAnalysis = null; | |
| routeGeometry.invalidate(); | |
| clearBootstrapError(); | |
| renderPlan(plan); | |
| renderer.renderApiGuide(); | |
| syncLifecycleMarkers(); | |
| } | |
| function renderPlan(plan) { | |
| currentPlan = utils.clonePlan(plan); | |
| if (focusedRouteId && !hasRoute(currentPlan, focusedRouteId)) focusedRouteId = null; | |
| renderCurrentPlan(); | |
| } | |
| function renderCurrentPlan() { | |
| if (renderer && currentPlan) renderer.renderAll(currentPlan, currentRoutes); | |
| } | |
| function focusRoute(routeId) { | |
| focusedRouteId = focusedRouteId === routeId ? null : routeId; | |
| renderCurrentPlan(); | |
| } | |
| function hasRoute(plan, routeId) { | |
| return (plan.technician_routes || []).some(function (route, idx) { | |
| return routeKey(route, idx) === routeId; | |
| }); | |
| } | |
| function routeKey(route, idx) { | |
| return String(route.id || route.technician_name || ('route-' + idx)); | |
| } | |
| function canSolve() { | |
| return !bootstrapError && !!currentPlan && !demoData.isLoading(); | |
| } | |
| function reportBootstrapError(err) { | |
| bootstrapError = err && err.message ? err.message : String(err || 'unknown error'); | |
| layout.bootstrapNotice.textContent = 'Demo data bootstrap failed: ' + bootstrapError; | |
| layout.bootstrapNotice.style.display = ''; | |
| app.dataset.bootstrapError = 'true'; | |
| updateSolveActionAvailability(); | |
| console.error('Demo data bootstrap failed:', err); | |
| } | |
| function clearBootstrapError() { | |
| bootstrapError = null; | |
| layout.bootstrapNotice.textContent = ''; | |
| layout.bootstrapNotice.style.display = 'none'; | |
| delete app.dataset.bootstrapError; | |
| } | |
| function updateSolveActionAvailability() { | |
| var solveButton = utils.findHeaderButton(layout.header, 'Solve'); | |
| if (!solveButton) return; | |
| var disabled = !canSolve(); | |
| solveButton.disabled = disabled; | |
| solveButton.setAttribute('aria-disabled', disabled ? 'true' : 'false'); | |
| solveButton.title = disabled | |
| ? (bootstrapError ? 'Bergamo road data could not be loaded.' : 'Loading Bergamo demo data...') | |
| : ''; | |
| } | |
| function syncLifecycleMarkers(meta) { | |
| var jobId = solver.getJobId(); | |
| var snapshotRevision = solver.getSnapshotRevision(); | |
| var lifecycleState = meta && meta.lifecycleState ? meta.lifecycleState : solver.getLifecycleState(); | |
| if (jobId) app.dataset.jobId = String(jobId); | |
| else delete app.dataset.jobId; | |
| if (snapshotRevision != null) app.dataset.snapshotRevision = String(snapshotRevision); | |
| else delete app.dataset.snapshotRevision; | |
| if (lifecycleState && lifecycleState !== 'IDLE') app.dataset.lifecycleState = lifecycleState; | |
| else delete app.dataset.lifecycleState; | |
| updateSolveActionAvailability(); | |
| } | |
| })(); | |