The School Route Mapper requires no build process and can run directly in any modern web browser.
# Option 1: Simple file server
cd route/
python3 -m http.server 8000
# Open http://localhost:8000 in browser
# Option 2: Node.js http-server
npx http-server route/ -p 8000
# Option 3: PHP built-in server
cd route/
php -S localhost:8000
# If using Git
git clone <repository-url>
cd school-route-mapper/route
# Or download and extract ZIP file
unzip school-route-mapper.zip
cd school-route-mapper/route
# Python 3.x
python3 -m http.server 8000
# Python 2.x (if needed)
python -m SimpleHTTPServer 8000
# Install http-server globally
npm install -g http-server
# Serve from route directory
http-server -p 8000 -c-1
# Alternative: use npx (no global install)
npx http-server -p 8000 -c-1
# PHP 5.4+
php -S localhost:8000
# With specific document root
php -S localhost:8000 -t ./
index.html// Access application instance in console
window.schoolRouteMapper
// Check application state
window.schoolRouteMapper.getState()
// Access components
window.schoolRouteMapper.canvasGrid
window.schoolRouteMapper.drawingTools
window.schoolRouteMapper.poiManager
Create debug.html for development:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>School Route Mapper - Debug</title>
<link rel="stylesheet" href="styles/main.css">
<link rel="stylesheet" href="styles/themes.css">
<style>
.debug-panel {
position: fixed;
top: 10px;
right: 10px;
background: rgba(0,0,0,0.8);
color: white;
padding: 10px;
border-radius: 5px;
font-family: monospace;
font-size: 12px;
z-index: 1000;
}
</style>
</head>
<body>
<!-- Debug panel -->
<div class="debug-panel" id="debug-panel">
<div>FPS: <span id="fps">0</span></div>
<div>Features: <span id="feature-count">0</span></div>
<div>Zoom: <span id="zoom-level">0</span></div>
<div>Mouse: <span id="mouse-pos">0, 0</span></div>
</div>
<!-- Include all normal content from index.html -->
<!-- ... -->
<script type="module">
import app from './js/app.js';
// Debug information updates
let frameCount = 0;
let lastTime = performance.now();
function updateDebugInfo() {
const now = performance.now();
const deltaTime = now - lastTime;
frameCount++;
if (deltaTime >= 1000) {
const fps = Math.round(frameCount * 1000 / deltaTime);
document.getElementById('fps').textContent = fps;
frameCount = 0;
lastTime = now;
}
if (app.isInitialized) {
const state = app.getState();
const project = state.project;
if (project) {
const featureCount =
project.features.polylines.length +
project.features.pois.length;
document.getElementById('feature-count').textContent = featureCount;
}
if (app.canvasGrid) {
const zoom = app.canvasGrid.viewport.zoom.toFixed(1);
document.getElementById('zoom-level').textContent = zoom + 'x';
const mouse = app.canvasGrid.mouse.world;
const mouseText = `${mouse[0].toFixed(2)}, ${mouse[1].toFixed(2)}`;
document.getElementById('mouse-pos').textContent = mouseText;
}
}
requestAnimationFrame(updateDebugInfo);
}
updateDebugInfo();
</script>
</body>
</html>
Create test.html for basic testing:
<!DOCTYPE html>
<html>
<head>
<title>School Route Mapper - Tests</title>
<style>
.test-result { margin: 5px 0; padding: 5px; border-radius: 3px; }
.pass { background: #d4edda; color: #155724; }
.fail { background: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<h1>School Route Mapper Tests</h1>
<div id="test-results"></div>
<script type="module">
import { createEmptyProject, validateProject, CategoryManager } from './js/data-model.js';
class TestRunner {
constructor() {
this.results = [];
}
test(name, fn) {
try {
fn();
this.results.push({ name, status: 'pass' });
} catch (error) {
this.results.push({ name, status: 'fail', error: error.message });
}
}
render() {
const container = document.getElementById('test-results');
container.innerHTML = this.results.map(result => `
<div class="test-result ${result.status}">
${result.status.toUpperCase()}: ${result.name}
${result.error ? `<br>Error: ${result.error}` : ''}
</div>
`).join('');
}
}
const runner = new TestRunner();
// Data model tests
runner.test('Create empty project', () => {
const project = createEmptyProject('Test Project');
if (!project.meta.title === 'Test Project') {
throw new Error('Project title not set correctly');
}
});
runner.test('Validate valid project', () => {
const project = createEmptyProject();
const result = validateProject(project);
if (!result.valid) {
throw new Error('Valid project failed validation');
}
});
runner.test('Category manager operations', () => {
const manager = new CategoryManager();
const category = {
id: 'test',
name: 'Test Category',
color: '#ff0000',
walkability: 'normal'
};
manager.addCategory(category);
const retrieved = manager.getCategory('test');
if (!retrieved || retrieved.name !== 'Test Category') {
throw new Error('Category not added/retrieved correctly');
}
});
// Add more tests as needed
runner.render();
</script>
</body>
</html>
# CSS minification
npm install -g clean-css-cli
cleancss -o styles/main.min.css styles/main.css styles/themes.css
# JavaScript minification
npm install -g terser
find js/ -name "*.js" -exec terser {} -o {}.min -m \;
# Pre-compress files for nginx
find . -name "*.js" -o -name "*.css" -o -name "*.html" -o -name "*.json" | \
xargs gzip -k -9
server {
listen 80;
server_name your-domain.com;
root /path/to/route/;
index index.html;
# Enable gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
gzip_min_length 1000;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Security headers
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
# MIME type for .schoolmap.json files
location ~* \.schoolmap\.json$ {
add_header Content-Type application/json;
}
# Fallback for SPA-like behavior
location / {
try_files $uri $uri/ /index.html;
}
}
# Enable compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
</IfModule>
# Cache control
<IfModule mod_expires.c>
ExpiresActive on
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/jpg "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
</IfModule>
# Security headers
<IfModule mod_headers.c>
Header always set X-Content-Type-Options nosniff
Header always set X-Frame-Options DENY
Header always set X-XSS-Protection "1; mode=block"
</IfModule>
# MIME type for .schoolmap.json
AddType application/json .schoolmap.json
# Enable HTTPS redirect
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</IfModule>
# .github/workflows/deploy.yml
name: Deploy to GitHub Pages
on:
push:
branches: [ main ]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies (if using build tools)
run: npm ci
working-directory: ./route
- name: Build (if needed)
run: npm run build
working-directory: ./route
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: $
publish_dir: ./route
# netlify.toml
[build]
publish = "route/"
command = "echo 'No build required'"
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
X-Content-Type-Options = "nosniff"
[[headers]]
for = "*.js"
[headers.values]
Cache-Control = "public, max-age=31536000"
[[headers]]
for = "*.css"
[headers.values]
Cache-Control = "public, max-age=31536000"
# Redirects for SPA behavior (if needed)
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
conditions = {Role = ["admin"]}
{
"version": 2,
"builds": [
{
"src": "route/**",
"use": "@vercel/static"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "/route/$1"
}
],
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "X-XSS-Protection",
"value": "1; mode=block"
}
]
}
]
}
# Upload to S3
aws s3 sync route/ s3://your-bucket-name/ --delete
# Configure bucket for static website hosting
aws s3 website s3://your-bucket-name --index-document index.html
# Create CloudFront distribution (optional)
aws cloudfront create-distribution --distribution-config file://cloudfront-config.json
/js/ directory browsing// CloudFlare Page Rules
// Rule 1: Cache static assets
// URL: yourdomain.com/*.js, yourdomain.com/*.css
// Settings:
// - Cache Level: Cache Everything
// - Edge Cache TTL: 1 year
// - Browser Cache TTL: 1 year
// Rule 2: Security headers
// URL: yourdomain.com/*
// Settings:
// - Security Level: Medium
// - Browser Integrity Check: On
# .env (if using build tools)
NODE_ENV=production
PUBLIC_URL=https://yourdomain.com
ENABLE_DEBUGGING=false
ANALYTICS_ID=your-analytics-id
// config.js
const CONFIG = {
production: {
debug: false,
analytics: {
enabled: true,
trackingId: 'GA-XXXXXXXX-X'
},
features: {
fileExport: true,
backgroundImages: true,
demo: true
},
performance: {
autosaveInterval: 30000,
maxHistoryItems: 10,
highQualityRendering: true
}
},
development: {
debug: true,
analytics: {
enabled: false
},
features: {
fileExport: true,
backgroundImages: true,
demo: true,
debugPanel: true
},
performance: {
autosaveInterval: 10000,
maxHistoryItems: 50,
highQualityRendering: true
}
}
};
// Auto-detect environment
const environment = window.location.hostname === 'localhost' ? 'development' : 'production';
window.APP_CONFIG = CONFIG[environment];
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
connect-src 'self';
font-src 'self';
object-src 'none';
media-src 'self';
child-src 'none';
">
<meta http-equiv="Permissions-Policy" content="
camera=(),
microphone=(),
geolocation=(),
payment=(),
usb=()
">
#!/bin/bash
# school-deploy.sh
# Configuration
DOMAIN="schoolmaps.yourdomain.edu"
BACKUP_DIR="/backup/schoolmaps"
DEPLOY_DIR="/var/www/schoolmaps"
# Create backup
echo "Creating backup..."
tar -czf "$BACKUP_DIR/backup-$(date +%Y%m%d-%H%M%S).tar.gz" "$DEPLOY_DIR"
# Deploy new version
echo "Deploying new version..."
rsync -av --delete route/ "$DEPLOY_DIR/"
# Set permissions
chown -R www-data:www-data "$DEPLOY_DIR"
chmod -R 644 "$DEPLOY_DIR"
find "$DEPLOY_DIR" -type d -exec chmod 755 {} \;
# Test deployment
echo "Testing deployment..."
curl -f "https://$DOMAIN" > /dev/null && echo "✓ Site accessible" || echo "✗ Site not accessible"
echo "Deployment complete!"
// manifest.json
{
"name": "School Route Mapper",
"short_name": "RouteMapper",
"description": "Interactive school campus mapping tool",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3b82f6",
"orientation": "any",
"icons": [
{
"src": "icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"categories": ["education", "navigation", "productivity"],
"lang": "en",
"scope": "/"
}
// sw.js - Basic service worker for PWA
const CACHE_NAME = 'school-route-mapper-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles/main.css',
'/styles/themes.css',
'/js/app.js',
'/js/data-model.js',
'/js/canvas-grid.js',
'/js/drawing-tools.js',
'/js/poi.js',
'/js/storage.js',
'/js/ui-controls.js',
'/js/spline.js',
'/js/demo-data.js',
'/js/pathfinding-worker.js'
];
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
return fetch(event.request);
}
)
);
});
FROM nginx:alpine
# Copy application files
COPY route/ /usr/share/nginx/html/
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Add security headers
RUN echo 'add_header X-Content-Type-Options nosniff;' >> /etc/nginx/conf.d/security.conf && \
echo 'add_header X-Frame-Options DENY;' >> /etc/nginx/conf.d/security.conf && \
echo 'add_header X-XSS-Protection "1; mode=block";' >> /etc/nginx/conf.d/security.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
version: '3.8'
services:
school-route-mapper:
build: .
ports:
- "80:80"
volumes:
- ./route:/usr/share/nginx/html:ro
restart: unless-stopped
environment:
- NGINX_ENVSUBST_TEMPLATE_SUFFIX=.template
labels:
- "traefik.enable=true"
- "traefik.http.routers.schoolmaps.rule=Host(`schoolmaps.yourdomain.com`)"
- "traefik.http.services.schoolmaps.loadbalancer.server.port=80"
networks:
default:
external:
name: web
# Check console for errors
# 1. Open browser developer tools (F12)
# 2. Check Console tab for error messages
# 3. Check Network tab for failed requests
# Common causes:
# - CORS issues (need proper web server)
# - Missing files (check file paths)
# - Browser compatibility (check browser version)
# Error: "Failed to resolve module specifier"
# Solution: Ensure you're serving from a web server, not file:// protocol
# Error: "Unexpected token '<'"
# Solution: Check that .js files are served with correct MIME type
# Add to .htaccess or server config:
AddType application/javascript .js
// Debug performance in browser console
console.time('render');
app.render();
console.timeEnd('render');
// Check memory usage
console.log('Memory usage:', performance.memory);
// Monitor frame rate
let lastTime = 0;
let frameCount = 0;
function checkFPS() {
const now = performance.now();
frameCount++;
if (now - lastTime >= 1000) {
console.log('FPS:', frameCount);
frameCount = 0;
lastTime = now;
}
requestAnimationFrame(checkFPS);
}
checkFPS();
// Check localStorage quota
function checkStorageQuota() {
if ('storage' in navigator && 'estimate' in navigator.storage) {
navigator.storage.estimate().then(estimate => {
console.log('Storage quota:', estimate.quota);
console.log('Storage usage:', estimate.usage);
console.log('Storage available:', estimate.quota - estimate.usage);
});
}
}
checkStorageQuota();
// Clear storage if needed
localStorage.removeItem('schoolRouteMapper');
localStorage.removeItem('schoolRouteMapper_autosave');
localStorage.removeItem('schoolRouteMapper_history');
// Safari requires user interaction for file downloads
// Ensure export is triggered by user click
// Safari canvas memory limits
// Reduce canvas size if needed
const maxCanvasSize = 4096; // Safari limit
if (canvas.width > maxCanvasSize || canvas.height > maxCanvasSize) {
// Scale down canvas
}
/* Prevent zoom on input focus */
input, select, textarea {
font-size: 16px; /* Minimum to prevent zoom */
}
/* Handle viewport units on mobile */
.full-height {
height: 100vh;
height: -webkit-fill-available; /* Safari mobile fix */
}
<!-- IE compatibility (if needed) -->
<!--[if IE]>
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<p style="background: #ffebee; padding: 20px; margin: 20px; border: 1px solid #f44336;">
This application requires a modern browser. Please upgrade to Chrome, Firefox, Safari, or Edge.
</p>
<![endif]-->
class PerformanceMonitor {
constructor() {
this.metrics = [];
this.isRecording = false;
}
start() {
this.isRecording = true;
this.startTime = performance.now();
}
mark(label) {
if (!this.isRecording) return;
this.metrics.push({
label,
timestamp: performance.now(),
memory: performance.memory ? performance.memory.usedJSHeapSize : 0
});
}
stop() {
this.isRecording = false;
return this.metrics;
}
generateReport() {
console.table(this.metrics);
// Memory usage over time
const memoryData = this.metrics.map(m => m.memory);
console.log('Memory usage (bytes):', {
min: Math.min(...memoryData),
max: Math.max(...memoryData),
avg: memoryData.reduce((a, b) => a + b, 0) / memoryData.length
});
}
}
// Usage
const monitor = new PerformanceMonitor();
monitor.start();
monitor.mark('App initialized');
// ... app operations ...
monitor.mark('Features loaded');
monitor.stop();
monitor.generateReport();
// Dynamic imports for large features
async function loadPathfindingWorker() {
const { PathfindingEngine } = await import('./js/pathfinding-worker.js');
return new PathfindingEngine();
}
// Load only when needed
document.getElementById('calculate-route-btn').addEventListener('click', async () => {
if (!this.pathfinder) {
this.pathfinder = await loadPathfindingWorker();
}
// ... route calculation
});
# Optimize PNG images
pngquant --quality=65-80 --ext .png --force icons/*.png
# Convert to WebP
for file in icons/*.png; do
cwebp -q 80 "$file" -o "${file%.png}.webp"
done
# Use in HTML with fallback
<picture>
<source srcset="icon.webp" type="image/webp">
<img src="icon.png" alt="Icon">
</picture>
/* Use efficient selectors */
.feature-selected { /* Good */ }
div.feature.selected { /* Less efficient */ }
/* Minimize reflows */
.canvas-container {
will-change: transform; /* Hint to browser */
contain: layout style paint; /* CSS containment */
}
/* Use hardware acceleration */
.animated-element {
transform: translateZ(0); /* Force GPU acceleration */
}
// Use requestAnimationFrame for animations
function animate() {
// Update animation
requestAnimationFrame(animate);
}
// Debounce expensive operations
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
const debouncedResize = debounce(() => {
app.canvasGrid.resize();
}, 250);
window.addEventListener('resize', debouncedResize);
// Service Worker caching strategy
const CACHE_STRATEGIES = {
// App shell - cache first
appShell: ['/index.html', '/js/', '/styles/'],
// Static assets - cache first with update
static: ['/icons/', '/fonts/'],
// User data - network first
userData: ['/api/'],
// Background images - cache with fallback
images: ['/uploads/', '/backgrounds/']
};
# nginx.conf - Advanced compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
application/json
application/javascript
application/xml
text/css
text/javascript
text/plain
text/xml;
# Brotli compression (if available)
brotli on;
brotli_comp_level 6;
brotli_types
application/json
application/javascript
text/css
text/javascript
text/plain;
// Sanitize user inputs
function sanitizeInput(input) {
if (typeof input !== 'string') return '';
return input
.replace(/[<>]/g, '') // Remove potential HTML
.trim()
.substring(0, 1000); // Limit length
}
// Validate coordinates
function validateCoordinate(coord) {
const num = parseFloat(coord);
return !isNaN(num) && isFinite(num) && Math.abs(num) < 1000000;
}
// Validate imported files
function validateImportedFile(file) {
// Check file size (max 10MB)
if (file.size > 10 * 1024 * 1024) {
throw new Error('File too large');
}
// Check file type
if (!file.name.endsWith('.json') && !file.name.endsWith('.schoolmap.json')) {
throw new Error('Invalid file type');
}
// Validate JSON structure
try {
const data = JSON.parse(content);
const validation = validateProject(data);
if (!validation.valid) {
throw new Error('Invalid project structure');
}
} catch (e) {
throw new Error('Invalid JSON format');
}
}
// Escape HTML in user content
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Use when displaying user content
poiNameElement.innerHTML = escapeHtml(poi.name);
// Local-only storage notice
const PRIVACY_NOTICE = `
This application stores all data locally in your browser.
No information is sent to external servers.
Your map data remains private and under your control.
`;
// Optional analytics (with consent)
function initializeAnalytics() {
if (window.APP_CONFIG.analytics.enabled) {
const consent = localStorage.getItem('analytics-consent');
if (consent === 'granted') {
// Initialize analytics
} else {
// Show consent dialog
showAnalyticsConsentDialog();
}
}
}
This deployment guide provides comprehensive instructions for setting up the School Route Mapper in various environments. Choose the deployment method that best fits your infrastructure and requirements.