The School Route Mapper uses a comprehensive event-driven architecture for inter-component communication. All components emit and listen for events to maintain loose coupling.
// Feature lifecycle events
document.addEventListener('drawingTool:featureAdded', (event) => {
// event.detail: { feature, tool }
});
document.addEventListener('drawingTool:featureModified', (event) => {
// event.detail: { feature, changes, tool }
});
document.addEventListener('drawingTool:featureDeleted', (event) => {
// event.detail: { featureId, feature, tool }
});
document.addEventListener('drawingTool:selectionChanged', (event) => {
// event.detail: { selectedFeatures, previousSelection }
});
// Tool state events
document.addEventListener('drawingTool:toolChanged', (event) => {
// event.detail: { tool, previousTool }
});
document.addEventListener('drawingTool:categoryChanged', (event) => {
// event.detail: { categoryId, category }
});
// POI lifecycle events
document.addEventListener('poi:poiAdded', (event) => {
// event.detail: { poi }
});
document.addEventListener('poi:poiUpdated', (event) => {
// event.detail: { poi, changes }
});
document.addEventListener('poi:poiDeleted', (event) => {
// event.detail: { poiId, poi }
});
// POI interaction events
document.addEventListener('poi:poiSelected', (event) => {
// event.detail: { poi }
});
document.addEventListener('poi:dialogOpened', (event) => {
// event.detail: { poi, mode } // mode: 'create' | 'edit'
});
document.addEventListener('poi:dialogClosed', (event) => {
// event.detail: { saved, poi }
});
// Project operations
document.addEventListener('storage:projectSaved', (event) => {
// event.detail: { project, timestamp }
});
document.addEventListener('storage:projectLoaded', (event) => {
// event.detail: { project, source } // source: 'localStorage' | 'import'
});
document.addEventListener('storage:projectImported', (event) => {
// event.detail: { project, filename, warnings }
});
document.addEventListener('storage:projectExported', (event) => {
// event.detail: { project, filename, format }
});
// Autosave events
document.addEventListener('storage:autosaveTriggered', (event) => {
// event.detail: { project, timestamp }
});
document.addEventListener('storage:autosaveCompleted', (event) => {
// event.detail: { success, project, timestamp }
});
// Error events
document.addEventListener('storage:error', (event) => {
// event.detail: { message, error, operation }
});
// Viewport changes
document.addEventListener('canvas:viewportChanged', (event) => {
// event.detail: { viewport, zoom, center }
});
// Mouse interactions
document.addEventListener('canvas:click', (event) => {
// event.detail: { world, screen, button, ctrlKey, shiftKey }
});
document.addEventListener('canvas:mousemove', (event) => {
// event.detail: { world, screen, deltaX, deltaY }
});
// Grid events
document.addEventListener('canvas:gridChanged', (event) => {
// event.detail: { spacing, origin, visible }
});
class BaseComponent {
constructor() {
this.eventListeners = new Map();
}
on(eventType, listener) {
if (!this.eventListeners.has(eventType)) {
this.eventListeners.set(eventType, []);
}
this.eventListeners.get(eventType).push(listener);
}
off(eventType, listener) {
const listeners = this.eventListeners.get(eventType);
if (listeners) {
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
}
}
emit(eventType, data) {
const listeners = this.eventListeners.get(eventType);
if (listeners) {
listeners.forEach(listener => listener(data));
}
// Also emit as custom DOM event
const event = new CustomEvent(`${this.constructor.name.toLowerCase()}:${eventType}`, {
detail: data
});
document.dispatchEvent(event);
}
}
class SchoolRouteMapperApp {
constructor() {
this.version = '1.0.0';
this.project = null;
this.canvasGrid = null;
this.drawingTools = null;
this.poiManager = null;
this.storage = null;
this.uiControls = null;
this.isInitialized = false;
this.currentMode = 'edit'; // 'edit' | 'navigate'
}
// Lifecycle Methods
async initialize() { /* ... */ }
destroy() { /* ... */ }
// Project Management
newProject(title = 'New School Map') { /* ... */ }
async loadProject(project) { /* ... */ }
saveProject() { /* ... */ }
exportProject(filename = null) { /* ... */ }
loadDemo() { /* ... */ }
// State Management
getState() {
return {
version: this.version,
initialized: this.isInitialized,
mode: this.currentMode,
project: this.project,
hasUnsavedChanges: this.storage?.isDirty() || false
};
}
// Utility Methods
getContentBounds() { /* ... */ }
render() { /* ... */ }
showError(message) { /* ... */ }
checkBrowserCompatibility() { /* ... */ }
}
// Project Factory Functions
createEmptyProject(title = "New School Map") -> Project
createDemoProject() -> Project
// Validation Functions
validateProject(data) -> {
valid: boolean,
errors: string[],
warnings: string[]
}
migrateProject(data) -> Project
// Feature Factory Functions
createPolyline(categoryId, points, properties = {}) -> PolylineFeature
createPOI(name, type, position, properties = {}) -> POIFeature
// Utility Functions
generateId(prefix = '') -> string
deepClone(obj) -> any
calculateBounds(features) -> BoundingBox | null
findFeaturesNear(features, point, tolerance = 0.5) -> FoundFeature[]
snapToGrid(point, spacing, origin = [0, 0]) -> [number, number]
getVisibleGridPoints(viewport, spacing, origin = [0, 0]) -> [number, number][]
updateProjectMeta(project, updates) -> Project
normalizeColor(color) -> string
normalizeDashPattern(dash) -> number[]
class CategoryManager {
constructor(categories = []) {
this.categories = [...categories];
}
// CRUD Operations
getCategory(id) -> Category | undefined
addCategory(category) -> Category
updateCategory(id, updates) -> Category | null
removeCategory(id) -> Category | null
// Query Methods
getAllCategories() -> Category[]
getWalkableCategories() -> Category[]
getBlockedCategories() -> Category[]
// Validation
validateCategory(category) -> ValidationResult
}
class CanvasGrid extends BaseComponent {
constructor(canvas) {
super();
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.viewport = { x: 0, y: 0, zoom: 20, width: 0, height: 0 };
this.grid = { spacing: 0.5, origin: [0, 0], visible: true };
this.mouse = { world: [0, 0], screen: [0, 0] };
this.background = { image: null, transform: {...}, opacity: 0.35 };
}
// Coordinate Transformation
screenToWorld(screenX, screenY) -> [number, number]
worldToScreen(worldX, worldY) -> [number, number]
// Viewport Management
setViewport(x, y, zoom) -> void
fitToContent(bounds, padding = 50) -> void
centerOn(worldX, worldY) -> void
// Grid Management
setGridSpacing(spacing) -> void
setGridOrigin(origin) -> void
setGridVisible(visible) -> void
getSnapPosition(worldPos = this.mouse.world) -> [number, number]
// Background Management
setBackgroundImage(image) -> Promise<void>
setBackgroundTransform(transform) -> void
setBackgroundOpacity(opacity) -> void
clearBackground() -> void
// Rendering
draw() -> void
clear() -> void
resize() -> void
// Event Handling
setupEventListeners() -> void
handleMouseMove(event) -> void
handleMouseDown(event) -> void
handleMouseUp(event) -> void
handleWheel(event) -> void
handleKeyDown(event) -> void
}
class DrawingTools extends BaseComponent {
constructor(canvasGrid, project) {
super();
this.canvasGrid = canvasGrid;
this.project = project;
this.categoryManager = new CategoryManager(project.categories);
this.currentTool = 'select';
this.currentCategory = null;
this.selectedFeatures = [];
this.isDrawing = false;
}
// Tool Management
setTool(tool) -> void // 'select' | 'polyline' | 'erase' | 'poi' | 'pan'
getCurrentTool() -> string
// Category Management
setCategory(categoryId) -> void
getCurrentCategory() -> Category | null
// Selection Management
selectFeature(feature, addToSelection = false) -> void
deselectFeature(feature) -> void
clearSelection() -> void
getSelectedFeatures() -> Feature[]
// Feature Operations
deleteSelected() -> void
moveSelected(deltaX, deltaY) -> void
duplicateSelected() -> Feature[]
// Drawing Operations
startDrawing(worldPos) -> void
continueDrawing(worldPos) -> void
finishDrawing() -> Feature | null
cancelDrawing() -> void
// Hit Testing
hitTest(worldPos, tolerance = 0.5) -> Feature | null
getFeatureAtPosition(worldPos, tolerance = 0.5) -> Feature | null
// Rendering Integration
render(ctx) -> void
renderSelection(ctx) -> void
renderPreview(ctx) -> void
}
class POIManager extends BaseComponent {
constructor(canvasGrid, project) {
super();
this.canvasGrid = canvasGrid;
this.project = project;
this.selectedPOI = null;
this.hoveredPOI = null;
this.editingPOI = null;
}
// POI Operations
placePOI(position, name = 'New POI', type = 'other', properties = {}) -> POI
updatePOI(poiId, updates) -> POI | null
deletePOI(poiId) -> boolean
// Query Methods
getAllPOIs() -> POI[]
getPOIsByType(type) -> POI[]
getPOIsByLevel(level) -> POI[]
findPOINear(position, tolerance = 1.0) -> POI[]
// UI Integration
showPOIDialog(poi = null) -> void
hidePOIDialog() -> void
// Selection Management
selectPOI(poi) -> void
deselectPOI() -> void
getSelectedPOI() -> POI | null
// Rendering
render(ctx) -> void
renderPOI(ctx, poi) -> void
getPOIColor(type) -> string
getPOIIcon(type) -> string
}
class StorageManager extends BaseComponent {
constructor() {
super();
this.storageKey = 'schoolRouteMapper';
this.autosaveKey = `${this.storageKey}_autosave`;
this.historyKey = `${this.storageKey}_history`;
this.settingsKey = `${this.storageKey}_settings`;
this.maxHistoryItems = 10;
this.autosaveInterval = 30000;
this.currentProject = null;
this.hasUnsavedChanges = false;
}
// Project Persistence
saveProject(project, updateMeta = true) -> boolean
loadProject() -> Project | null
// File Operations
exportProject(project, filename = null) -> boolean
importProject(file) -> Promise<ImportResult>
setupFileInput(input) -> void
setupDragAndDrop(element) -> void
// Autosave Management
startAutosave(project) -> void
stopAutosave() -> void
markDirty() -> void
isDirty() -> boolean
// History Management
saveToHistory(project) -> void
getHistory() -> HistoryEntry[]
loadFromHistory(historyId) -> Project | null
clearHistory() -> void
// Settings Management
getSettings() -> Settings
saveSettings(settings) -> void
resetSettings() -> void
// Utility Methods
getStorageUsage() -> StorageInfo
clearAllData() -> void
}
interface Project {
schemaVersion: number;
meta: ProjectMetadata;
background: BackgroundConfig;
categories: Category[];
features: Features;
}
interface ProjectMetadata {
title: string;
created: string; // ISO 8601 timestamp
updated: string; // ISO 8601 timestamp
unit: 'm' | 'ft'; // measurement unit
grid: GridConfig;
render: RenderConfig;
description?: string;
author?: string;
version?: string;
}
interface GridConfig {
spacing: number; // grid spacing in units
origin: [number, number]; // grid origin point
}
interface RenderConfig {
theme: 'light' | 'dark';
showGrid?: boolean;
showCoordinates?: boolean;
highQuality?: boolean;
}
interface BackgroundConfig {
source: string | null; // data URL or null
transform: Transform;
opacity: number; // 0-1
}
interface Transform {
x: number; // translation X
y: number; // translation Y
scale: number; // scale factor
rotation: number; // rotation in degrees
}
interface Category {
id: string; // unique identifier
name: string; // display name
color: string; // hex color code
dash: number[]; // line dash pattern
walkability: WalkabilityType;
// Optional routing properties
buffer?: number; // minimum clearance
speedFactor?: number; // movement speed multiplier
costFactor?: number; // pathfinding cost multiplier
gateWidth?: number; // passage width for gates
}
type WalkabilityType =
| 'blocked'
| 'gate'
| 'normal'
| 'fast'
| 'slow'
| 'slower'
| 'discouraged';
interface Features {
polylines: PolylineFeature[];
pois: POIFeature[];
}
interface PolylineFeature {
id: string;
category: string; // category ID
points: [number, number][]; // world coordinates
properties: PolylineProperties;
}
interface PolylineProperties {
level: number; // floor level
description?: string;
tags?: string[];
[key: string]: any; // extensible properties
}
interface POIFeature {
id: string;
name: string;
type: POIType;
pos: [number, number]; // world coordinates
properties: POIProperties;
}
interface POIProperties {
level: number; // floor level
tags: string[];
description?: string;
capacity?: number;
accessible?: boolean;
[key: string]: any; // extensible properties
}
type POIType =
| 'room'
| 'entrance'
| 'stairs'
| 'elevator'
| 'restroom'
| 'office'
| 'exit'
| 'emergency-exit'
| 'cafeteria'
| 'library'
| 'gym'
| 'auditorium'
| 'lab'
| 'computer-lab'
| 'nurse'
| 'admin'
| 'parking'
| 'bike-rack'
| 'bus-stop'
| 'other';
interface BoundingBox {
minX: number;
minY: number;
maxX: number;
maxY: number;
width: number;
height: number;
centerX: number;
centerY: number;
}
interface Viewport {
x: number; // world X offset
y: number; // world Y offset
zoom: number; // zoom level (pixels per unit)
width: number; // viewport width in pixels
height: number; // viewport height in pixels
}
interface Settings {
theme: 'light' | 'dark';
gridSpacing: number;
autosaveEnabled: boolean;
routingResolution: number;
defaultBuffer: number;
turnPenalty: number;
splineTension: number;
animationSpeed: number;
highQualityRendering: boolean;
}
interface HistoryEntry {
id: string;
timestamp: string;
title: string;
project: Project;
size: number; // serialized size in bytes
}
interface ImportResult {
project: Project;
warnings: string[];
migrated: boolean;
}
interface StorageInfo {
used: number; // bytes used
available: number; // bytes available
quota: number; // total quota
projects: number; // number of projects
history: number; // number of history entries
}
interface ValidationRule {
field: string;
type: 'required' | 'type' | 'range' | 'format' | 'custom';
message: string;
validator?: (value: any) => boolean;
}
// Example validation rules
const PROJECT_VALIDATION_RULES: ValidationRule[] = [
{
field: 'schemaVersion',
type: 'required',
message: 'Schema version is required'
},
{
field: 'meta.title',
type: 'required',
message: 'Project title is required'
},
{
field: 'categories',
type: 'type',
message: 'Categories must be an array',
validator: (value) => Array.isArray(value)
}
];
const DEFAULT_SETTINGS = {
// Visual preferences
theme: 'light', // 'light' | 'dark'
highQualityRendering: true, // enable anti-aliasing
// Grid configuration
gridSpacing: 0.5, // meters
showGrid: true, // show grid by default
snapToGrid: true, // enable grid snapping
// Storage settings
autosaveEnabled: true, // enable automatic saves
autosaveInterval: 30000, // milliseconds
maxHistoryItems: 10, // autosave history limit
// Routing parameters
routingResolution: 0.25, // pathfinding grid resolution (meters)
defaultBuffer: 0.3, // obstacle clearance (meters)
turnPenalty: 0.3, // cost for direction changes
// Animation settings
animationSpeed: 1.0, // route animation speed multiplier
// Spline settings
splineTension: 0.5, // curve tension (0-1)
splineResolution: 0.1, // curve point density
// Performance settings
maxFeatures: 10000, // feature count warning threshold
maxPolylinePoints: 1000, // polyline complexity limit
renderDistance: 1000 // meters beyond viewport to render
};
const THEME_DEFINITIONS = {
light: {
// Background colors
'--bg-primary': '#ffffff',
'--bg-secondary': '#f8fafc',
'--bg-tertiary': '#f1f5f9',
// Text colors
'--text-primary': '#1e293b',
'--text-secondary': '#64748b',
'--text-muted': '#94a3b8',
// Border colors
'--border-color': '#e2e8f0',
// Accent colors
'--accent-primary': '#3b82f6',
'--accent-hover': '#2563eb',
'--success': '#10b981',
'--warning': '#f59e0b',
'--danger': '#ef4444',
// Grid colors
'--grid-color': '#94a3b8',
'--grid-alpha': 0.5
},
dark: {
// Background colors
'--bg-primary': '#0f172a',
'--bg-secondary': '#1e293b',
'--bg-tertiary': '#334155',
// Text colors
'--text-primary': '#f1f5f9',
'--text-secondary': '#cbd5e1',
'--text-muted': '#64748b',
// Border colors
'--border-color': '#475569',
// Accent colors
'--accent-primary': '#60a5fa',
'--accent-hover': '#3b82f6',
'--success': '#34d399',
'--warning': '#fbbf24',
'--danger': '#f87171',
// Grid colors
'--grid-color': '#64748b',
'--grid-alpha': 0.3
}
};
const PATHFINDING_CONFIG = {
// Grid settings
resolution: 0.25, // meters per grid cell
padding: 5, // meters of padding around content
// Algorithm parameters
algorithm: 'astar', // 'astar' | 'dijkstra' | 'bfs'
heuristic: 'euclidean', // 'euclidean' | 'manhattan'
allowDiagonal: true, // allow diagonal movement
// Cost factors
defaultCost: 1.0, // base movement cost
diagonalCost: 1.414, // diagonal movement multiplier
turnPenalty: 0.3, // cost for direction changes
// Obstacle handling
defaultBuffer: 0.3, // minimum clearance from obstacles
bufferResolution: 0.1, // buffer calculation precision
// Performance limits
maxIterations: 100000, // algorithm iteration limit
timeout: 5000 // milliseconds before timeout
};
const CATEGORY_PRESETS = {
// Structure categories
walls: {
external: {
color: '#1f2937',
walkability: 'blocked',
buffer: 0.5,
dash: []
},
internal: {
color: '#4b5563',
walkability: 'blocked',
buffer: 0.4,
dash: []
},
temporary: {
color: '#9ca3af',
walkability: 'blocked',
buffer: 0.3,
dash: [4, 4]
}
},
// Access categories
openings: {
door: {
color: '#22c55e',
walkability: 'gate',
gateWidth: 1.0,
dash: [2, 3]
},
window: {
color: '#06b6d4',
walkability: 'blocked',
buffer: 0.2,
dash: [1, 2]
},
opening: {
color: '#84cc16',
walkability: 'normal',
dash: []
}
},
// Circulation categories
movement: {
corridor: {
color: '#e5e7eb',
walkability: 'normal',
dash: []
},
stairs: {
color: '#10b981',
walkability: 'slow',
speedFactor: 0.6,
dash: [1, 2]
},
elevator: {
color: '#0ea5e9',
walkability: 'slow',
speedFactor: 0.7,
dash: [4, 2]
},
ramp: {
color: '#14b8a6',
walkability: 'normal',
speedFactor: 0.9,
dash: []
}
},
// Outdoor categories
exterior: {
path: {
color: '#2563eb',
walkability: 'fast',
speedFactor: 1.05,
dash: []
},
road: {
color: '#7c3aed',
walkability: 'discouraged',
costFactor: 1.8,
dash: [2, 6]
},
grass: {
color: '#84cc16',
walkability: 'slower',
speedFactor: 0.8,
dash: [1, 5]
}
}
};
class BasePlugin {
constructor(app) {
this.app = app;
this.name = 'BasePlugin';
this.version = '1.0.0';
this.enabled = false;
}
// Lifecycle methods
initialize() { /* Override */ }
activate() { this.enabled = true; }
deactivate() { this.enabled = false; }
destroy() { /* Override */ }
// Extension points
onProjectLoaded(project) { /* Override */ }
onFeatureAdded(feature) { /* Override */ }
onRender(ctx) { /* Override */ }
// Utility methods
registerTool(toolName, toolClass) { /* ... */ }
registerCategory(categoryDef) { /* ... */ }
addMenuItem(menu, item) { /* ... */ }
}
class CustomTool extends BaseTool {
constructor(drawingTools) {
super(drawingTools);
this.name = 'custom';
this.cursor = 'crosshair';
}
activate() {
super.activate();
// Tool-specific setup
}
deactivate() {
super.deactivate();
// Tool-specific cleanup
}
handleClick(worldPos, event) {
// Handle click events
}
handleMouseMove(worldPos, event) {
// Handle mouse movement
}
render(ctx) {
// Custom rendering
}
}
// Register custom tool
app.drawingTools.registerTool('custom', CustomTool);
const CUSTOM_POI_TYPES = {
'science-lab': {
name: 'Science Laboratory',
color: '#8b5cf6',
icon: '🧪',
category: 'academic',
properties: {
capacity: { type: 'number', required: true },
equipment: { type: 'array', default: [] },
safetyLevel: { type: 'string', enum: ['low', 'medium', 'high'] }
}
}
};
// Register custom POI type
app.poiManager.registerPOIType('science-lab', CUSTOM_POI_TYPES['science-lab']);
class CustomRenderLayer {
constructor(canvasGrid) {
this.canvasGrid = canvasGrid;
this.visible = true;
this.zIndex = 10;
}
render(ctx) {
if (!this.visible) return;
// Custom rendering logic
ctx.save();
// ... rendering code ...
ctx.restore();
}
hitTest(worldPos) {
// Return feature at position or null
return null;
}
}
// Register custom layer
app.canvasGrid.addRenderLayer(new CustomRenderLayer(app.canvasGrid));
const CUSTOM_STYLES = {
polyline: {
selectedStroke: '#ff6b6b',
selectedWidth: 3,
hoverStroke: '#4ecdc4',
hoverWidth: 2
},
poi: {
selectedFill: '#ff6b6b',
selectedStroke: '#ffffff',
hoverFill: '#4ecdc4',
textFont: '12px Arial',
textColor: '#333333'
},
grid: {
majorColor: '#cccccc',
minorColor: '#eeeeee',
majorWidth: 1,
minorWidth: 0.5,
majorSpacing: 5 // major lines every 5 grid units
}
};
// Apply custom styles
app.canvasGrid.setStyles(CUSTOM_STYLES);
class SchoolRouteMapperError extends Error {
constructor(message, code, context = {}) {
super(message);
this.name = 'SchoolRouteMapperError';
this.code = code;
this.context = context;
this.timestamp = new Date().toISOString();
}
}
// Error codes
const ERROR_CODES = {
// Initialization errors
INIT_CANVAS_NOT_FOUND: 'INIT_001',
INIT_BROWSER_INCOMPATIBLE: 'INIT_002',
INIT_MODULE_LOAD_FAILED: 'INIT_003',
// Data errors
DATA_VALIDATION_FAILED: 'DATA_001',
DATA_MIGRATION_FAILED: 'DATA_002',
DATA_CORRUPTION: 'DATA_003',
// Storage errors
STORAGE_QUOTA_EXCEEDED: 'STOR_001',
STORAGE_ACCESS_DENIED: 'STOR_002',
STORAGE_IMPORT_FAILED: 'STOR_003',
STORAGE_EXPORT_FAILED: 'STOR_004',
// Rendering errors
RENDER_CANVAS_ERROR: 'REND_001',
RENDER_BACKGROUND_LOAD_FAILED: 'REND_002',
// Pathfinding errors
PATH_NO_SOLUTION: 'PATH_001',
PATH_INVALID_START: 'PATH_002',
PATH_INVALID_END: 'PATH_003',
PATH_TIMEOUT: 'PATH_004'
};
class ErrorRecoveryManager {
constructor(app) {
this.app = app;
this.recoveryStrategies = new Map();
this.setupDefaultStrategies();
}
setupDefaultStrategies() {
// Storage quota exceeded
this.recoveryStrategies.set('STOR_001', async (error) => {
// Clear old history entries
await this.app.storage.clearOldHistory();
// Compress current project
await this.app.storage.compressProject();
// Retry operation
return { retry: true };
});
// Canvas rendering error
this.recoveryStrategies.set('REND_001', async (error) => {
// Reduce rendering quality
this.app.canvasGrid.setHighQuality(false);
// Clear and reinitialize canvas
this.app.canvasGrid.clear();
this.app.canvasGrid.resize();
return { retry: true };
});
// Pathfinding timeout
this.recoveryStrategies.set('PATH_004', async (error) => {
// Reduce pathfinding resolution
const newResolution = Math.min(error.context.resolution * 2, 1.0);
this.app.pathfinder.setResolution(newResolution);
return { retry: true, message: 'Retrying with lower precision' };
});
}
async handleError(error) {
const strategy = this.recoveryStrategies.get(error.code);
if (strategy) {
try {
const result = await strategy(error);
return result;
} catch (recoveryError) {
console.error('Error recovery failed:', recoveryError);
return { retry: false };
}
}
return { retry: false };
}
}
class GlobalErrorHandler {
constructor(app) {
this.app = app;
this.errorHistory = [];
this.maxErrorHistory = 50;
this.setupErrorHandlers();
}
setupErrorHandlers() {
// Unhandled JavaScript errors
window.addEventListener('error', (event) => {
this.handleError(new SchoolRouteMapperError(
event.message,
'JS_RUNTIME_ERROR',
{
filename: event.filename,
line: event.lineno,
column: event.colno,
stack: event.error?.stack
}
));
});
// Unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
this.handleError(new SchoolRouteMapperError(
event.reason?.message || 'Unhandled promise rejection',
'PROMISE_REJECTION',
{
reason: event.reason,
stack: event.reason?.stack
}
));
});
// Application-specific errors
document.addEventListener('schoolRouteMapper:error', (event) => {
this.handleError(event.detail.error);
});
}
handleError(error) {
// Add to error history
this.errorHistory.unshift({
error,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href,
appState: this.app.getState()
});
// Limit history size
if (this.errorHistory.length > this.maxErrorHistory) {
this.errorHistory = this.errorHistory.slice(0, this.maxErrorHistory);
}
// Log error
console.error('Application error:', error);
// Attempt recovery
if (this.app.errorRecovery) {
this.app.errorRecovery.handleError(error);
}
// Show user notification for critical errors
if (this.isCriticalError(error)) {
this.showErrorNotification(error);
}
}
isCriticalError(error) {
const criticalCodes = [
'INIT_CANVAS_NOT_FOUND',
'INIT_BROWSER_INCOMPATIBLE',
'DATA_CORRUPTION',
'STORAGE_ACCESS_DENIED'
];
return criticalCodes.includes(error.code);
}
showErrorNotification(error) {
if (this.app.uiControls) {
this.app.uiControls.showNotification({
type: 'error',
title: 'Application Error',
message: error.message,
actions: [
{ label: 'Retry', action: () => window.location.reload() },
{ label: 'Report', action: () => this.reportError(error) }
]
});
}
}
reportError(error) {
// Create error report
const report = {
error: {
message: error.message,
code: error.code,
context: error.context,
timestamp: error.timestamp
},
environment: {
userAgent: navigator.userAgent,
url: window.location.href,
timestamp: new Date().toISOString()
},
application: this.app.getState()
};
// Generate downloadable report
const blob = new Blob([JSON.stringify(report, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `error-report-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
}
}
This API reference provides comprehensive technical documentation for developers working with or extending the School Route Mapper application.