blackopsrepl's picture
feat(fsr): add snapshot-scoped route geometry
ae32abe
/* 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();
}
})();