2026-02-05 12:03:22 +00:00
|
|
|
import { html, css, cssManager } from '@design.estate/dees-element';
|
|
|
|
|
import '@design.estate/dees-wcctools/demotools';
|
|
|
|
|
import type { DeesGeoMap } from './dees-geo-map.js';
|
2026-02-05 17:50:45 +00:00
|
|
|
import { getSpeedPresets, type TSimulationSpeed } from './geo-map.mock-gps.js';
|
2026-02-05 12:03:22 +00:00
|
|
|
|
|
|
|
|
export const demoFunc = () => html`
|
|
|
|
|
<style>
|
|
|
|
|
${css`
|
|
|
|
|
.demo-container {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 24px;
|
|
|
|
|
padding: 24px;
|
|
|
|
|
max-width: 1400px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.demo-section {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.demo-title {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.demo-description {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: ${cssManager.bdTheme('#666', '#999')};
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.map-wrapper {
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dees-geo-map {
|
|
|
|
|
height: 500px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-log {
|
|
|
|
|
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
|
|
|
|
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
max-height: 200px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-entry {
|
|
|
|
|
padding: 4px 0;
|
|
|
|
|
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
|
|
|
|
color: ${cssManager.bdTheme('#555', '#aaa')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-entry:last-child {
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-type {
|
|
|
|
|
color: ${cssManager.bdTheme('#0066cc', '#66b3ff')};
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.controls-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-button {
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
border: 1px solid ${cssManager.bdTheme('#ccc', '#444')};
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
background: ${cssManager.bdTheme('#fff', '#2a2a2a')};
|
|
|
|
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.15s ease;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 17:50:45 +00:00
|
|
|
.control-button:hover:not(:disabled) {
|
2026-02-05 12:03:22 +00:00
|
|
|
background: ${cssManager.bdTheme('#f0f0f0', '#333')};
|
|
|
|
|
border-color: ${cssManager.bdTheme('#999', '#666')};
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 17:50:45 +00:00
|
|
|
.control-button:disabled {
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-button.active {
|
|
|
|
|
background: ${cssManager.bdTheme('#0066cc', '#0084ff')};
|
|
|
|
|
color: #fff;
|
|
|
|
|
border-color: transparent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-button.primary {
|
|
|
|
|
background: ${cssManager.bdTheme('#0066cc', '#0084ff')};
|
|
|
|
|
color: #fff;
|
|
|
|
|
border-color: transparent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-button.primary:hover:not(:disabled) {
|
|
|
|
|
background: ${cssManager.bdTheme('#0055aa', '#0073e6')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-button.danger {
|
|
|
|
|
background: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
|
|
|
|
color: #fff;
|
|
|
|
|
border-color: transparent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-button.danger:hover:not(:disabled) {
|
|
|
|
|
background: ${cssManager.bdTheme('#b91c1c', '#dc2626')};
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 12:03:22 +00:00
|
|
|
.feature-display {
|
|
|
|
|
background: ${cssManager.bdTheme('#f9f9f9', '#1e1e1e')};
|
|
|
|
|
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.feature-json {
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
max-height: 300px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.locations-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.location-button {
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#444')};
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
background: ${cssManager.bdTheme('#fff', '#2a2a2a')};
|
|
|
|
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
text-align: center;
|
|
|
|
|
transition: all 0.15s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.location-button:hover {
|
|
|
|
|
background: ${cssManager.bdTheme('#0066cc', '#0084ff')};
|
|
|
|
|
color: #fff;
|
|
|
|
|
border-color: transparent;
|
|
|
|
|
}
|
2026-02-05 17:50:45 +00:00
|
|
|
|
|
|
|
|
/* Simulation Panel Styles */
|
|
|
|
|
.simulation-section {
|
|
|
|
|
background: ${cssManager.bdTheme('#f0f9ff', '#1a2633')};
|
|
|
|
|
border: 1px solid ${cssManager.bdTheme('#bae6fd', '#1e3a5f')};
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.simulation-controls-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: auto 1fr auto;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-playback {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-speed {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-speed-label {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: ${cssManager.bdTheme('#666', '#999')};
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-speed-select {
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
border: 1px solid ${cssManager.bdTheme('#ccc', '#444')};
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
background: ${cssManager.bdTheme('#fff', '#2a2a2a')};
|
|
|
|
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
min-width: 150px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-voice {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-voice-checkbox {
|
|
|
|
|
width: 18px;
|
|
|
|
|
height: 18px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-voice-label {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
user-select: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-progress {
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-progress-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-progress-label {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: ${cssManager.bdTheme('#666', '#999')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-progress-value {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-progress-bar {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 8px;
|
|
|
|
|
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-progress-fill {
|
|
|
|
|
height: 100%;
|
|
|
|
|
background: ${cssManager.bdTheme('#0066cc', '#0084ff')};
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
transition: width 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-status {
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-status.idle {
|
|
|
|
|
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
|
|
|
|
|
color: ${cssManager.bdTheme('#666', '#999')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-status.running {
|
|
|
|
|
background: rgba(34, 197, 94, 0.15);
|
|
|
|
|
color: ${cssManager.bdTheme('#166534', '#86efac')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-status.paused {
|
|
|
|
|
background: rgba(245, 158, 11, 0.15);
|
|
|
|
|
color: ${cssManager.bdTheme('#92400e', '#fcd34d')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-status.completed {
|
|
|
|
|
background: rgba(59, 130, 246, 0.15);
|
|
|
|
|
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sim-status.no-route {
|
|
|
|
|
background: rgba(239, 68, 68, 0.15);
|
|
|
|
|
color: ${cssManager.bdTheme('#991b1b', '#fca5a5')};
|
|
|
|
|
}
|
2026-02-05 12:03:22 +00:00
|
|
|
`}
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
<div class="demo-container">
|
|
|
|
|
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
|
|
|
|
const map = elementArg.querySelector('dees-geo-map') as DeesGeoMap;
|
|
|
|
|
const eventLog = elementArg.querySelector('#event-log') as HTMLElement;
|
|
|
|
|
const featureJson = elementArg.querySelector('#feature-json') as HTMLElement;
|
|
|
|
|
|
2026-02-05 17:50:45 +00:00
|
|
|
// Simulation state
|
|
|
|
|
let simulationState = {
|
|
|
|
|
isRunning: false,
|
|
|
|
|
isPaused: false,
|
|
|
|
|
progress: 0,
|
|
|
|
|
hasRoute: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updateSimulationUI = () => {
|
|
|
|
|
const startBtn = elementArg.querySelector('#sim-start') as HTMLButtonElement;
|
|
|
|
|
const pauseBtn = elementArg.querySelector('#sim-pause') as HTMLButtonElement;
|
|
|
|
|
const stopBtn = elementArg.querySelector('#sim-stop') as HTMLButtonElement;
|
|
|
|
|
const progressFill = elementArg.querySelector('#sim-progress-fill') as HTMLElement;
|
|
|
|
|
const progressValue = elementArg.querySelector('#sim-progress-value') as HTMLElement;
|
|
|
|
|
const statusEl = elementArg.querySelector('#sim-status') as HTMLElement;
|
|
|
|
|
|
|
|
|
|
if (!startBtn) return;
|
|
|
|
|
|
|
|
|
|
// Update button states
|
|
|
|
|
startBtn.disabled = !simulationState.hasRoute || (simulationState.isRunning && !simulationState.isPaused);
|
|
|
|
|
pauseBtn.disabled = !simulationState.isRunning || simulationState.isPaused;
|
|
|
|
|
stopBtn.disabled = !simulationState.isRunning && !simulationState.isPaused;
|
|
|
|
|
|
|
|
|
|
// Update progress
|
|
|
|
|
if (progressFill) {
|
|
|
|
|
progressFill.style.width = `${simulationState.progress}%`;
|
|
|
|
|
}
|
|
|
|
|
if (progressValue) {
|
|
|
|
|
progressValue.textContent = `${simulationState.progress.toFixed(1)}%`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update status
|
|
|
|
|
if (statusEl) {
|
|
|
|
|
statusEl.className = 'sim-status';
|
|
|
|
|
if (!simulationState.hasRoute) {
|
|
|
|
|
statusEl.className += ' no-route';
|
|
|
|
|
statusEl.textContent = 'Calculate a route first to enable simulation';
|
|
|
|
|
} else if (simulationState.isRunning && !simulationState.isPaused) {
|
|
|
|
|
statusEl.className += ' running';
|
|
|
|
|
statusEl.textContent = 'Simulation running...';
|
|
|
|
|
} else if (simulationState.isPaused) {
|
|
|
|
|
statusEl.className += ' paused';
|
|
|
|
|
statusEl.textContent = 'Simulation paused';
|
|
|
|
|
} else if (simulationState.progress >= 100) {
|
|
|
|
|
statusEl.className += ' completed';
|
|
|
|
|
statusEl.textContent = 'Simulation completed!';
|
|
|
|
|
} else {
|
|
|
|
|
statusEl.className += ' idle';
|
|
|
|
|
statusEl.textContent = 'Ready to simulate';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-05 12:03:22 +00:00
|
|
|
const addLogEntry = (type: string, message: string) => {
|
|
|
|
|
const entry = document.createElement('div');
|
|
|
|
|
entry.className = 'event-entry';
|
|
|
|
|
entry.innerHTML = `<span class="event-type">${type}</span>: ${message}`;
|
|
|
|
|
eventLog.insertBefore(entry, eventLog.firstChild);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updateFeatureDisplay = () => {
|
|
|
|
|
if (map && featureJson) {
|
|
|
|
|
featureJson.textContent = JSON.stringify(map.getGeoJson(), null, 2);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (map) {
|
|
|
|
|
map.addEventListener('map-ready', () => {
|
|
|
|
|
addLogEntry('ready', 'Map initialized successfully');
|
2026-02-05 17:50:45 +00:00
|
|
|
updateSimulationUI();
|
2026-02-05 12:03:22 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
map.addEventListener('draw-change', (e: CustomEvent) => {
|
|
|
|
|
addLogEntry('change', `${e.detail.type} - ${e.detail.ids.length} feature(s) affected`);
|
|
|
|
|
updateFeatureDisplay();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
map.addEventListener('draw-finish', (e: CustomEvent) => {
|
|
|
|
|
addLogEntry('finish', `${e.detail.context.mode} completed (id: ${e.detail.id})`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
map.addEventListener('map-move', (e: CustomEvent) => {
|
|
|
|
|
console.log('Map moved:', e.detail);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
map.addEventListener('address-selected', (e: CustomEvent) => {
|
|
|
|
|
addLogEntry('address', `Selected: ${e.detail.address.substring(0, 50)}...`);
|
|
|
|
|
console.log('Address selected:', e.detail);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
map.addEventListener('route-calculated', (e: CustomEvent) => {
|
|
|
|
|
const { route, mode } = e.detail;
|
|
|
|
|
const distKm = (route.distance / 1000).toFixed(1);
|
|
|
|
|
const durationMin = Math.round(route.duration / 60);
|
|
|
|
|
addLogEntry('route', `${mode}: ${distKm} km, ${durationMin} min`);
|
|
|
|
|
console.log('Route calculated:', e.detail);
|
2026-02-05 17:50:45 +00:00
|
|
|
|
|
|
|
|
// Update simulation state
|
|
|
|
|
simulationState.hasRoute = true;
|
|
|
|
|
simulationState.progress = 0;
|
|
|
|
|
updateSimulationUI();
|
|
|
|
|
|
|
|
|
|
// Create/update the simulator with the new route
|
|
|
|
|
const simulator = map.createMockGPSSimulator();
|
|
|
|
|
simulator.setRoute(route);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
map.addEventListener('guidance-event', (e: CustomEvent) => {
|
|
|
|
|
const event = e.detail;
|
|
|
|
|
addLogEntry('guidance', `${event.type}: ${event.instruction || 'Step ' + event.stepIndex}`);
|
|
|
|
|
console.log('Guidance event:', event);
|
|
|
|
|
|
|
|
|
|
// Update progress
|
|
|
|
|
const simulator = map.getMockGPSSimulator();
|
|
|
|
|
if (simulator) {
|
|
|
|
|
simulationState.progress = simulator.getProgress();
|
|
|
|
|
updateSimulationUI();
|
|
|
|
|
}
|
2026-02-05 12:03:22 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 17:50:45 +00:00
|
|
|
// Set up simulation controls
|
|
|
|
|
const setupSimulationControls = () => {
|
|
|
|
|
const startBtn = elementArg.querySelector('#sim-start') as HTMLButtonElement;
|
|
|
|
|
const pauseBtn = elementArg.querySelector('#sim-pause') as HTMLButtonElement;
|
|
|
|
|
const stopBtn = elementArg.querySelector('#sim-stop') as HTMLButtonElement;
|
|
|
|
|
const speedSelect = elementArg.querySelector('#sim-speed') as HTMLSelectElement;
|
|
|
|
|
const voiceCheckbox = elementArg.querySelector('#sim-voice') as HTMLInputElement;
|
|
|
|
|
|
|
|
|
|
if (startBtn && map) {
|
|
|
|
|
startBtn.addEventListener('click', () => {
|
|
|
|
|
let simulator = map.getMockGPSSimulator();
|
|
|
|
|
if (!simulator) {
|
|
|
|
|
simulator = map.createMockGPSSimulator();
|
|
|
|
|
const route = map.getNavigationState()?.route;
|
|
|
|
|
if (route) {
|
|
|
|
|
simulator.setRoute(route);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Start guidance
|
|
|
|
|
map.startGuidance();
|
|
|
|
|
simulator.start();
|
|
|
|
|
|
|
|
|
|
simulationState.isRunning = true;
|
|
|
|
|
simulationState.isPaused = false;
|
|
|
|
|
updateSimulationUI();
|
|
|
|
|
addLogEntry('simulation', 'Started');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pauseBtn && map) {
|
|
|
|
|
pauseBtn.addEventListener('click', () => {
|
|
|
|
|
const simulator = map.getMockGPSSimulator();
|
|
|
|
|
if (simulator) {
|
|
|
|
|
simulator.pause();
|
|
|
|
|
simulationState.isPaused = true;
|
|
|
|
|
updateSimulationUI();
|
|
|
|
|
addLogEntry('simulation', 'Paused');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (stopBtn && map) {
|
|
|
|
|
stopBtn.addEventListener('click', () => {
|
|
|
|
|
map.stopGuidance();
|
|
|
|
|
simulationState.isRunning = false;
|
|
|
|
|
simulationState.isPaused = false;
|
|
|
|
|
simulationState.progress = 0;
|
|
|
|
|
updateSimulationUI();
|
|
|
|
|
addLogEntry('simulation', 'Stopped');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (speedSelect && map) {
|
|
|
|
|
speedSelect.addEventListener('change', () => {
|
|
|
|
|
const speed = speedSelect.value as TSimulationSpeed;
|
|
|
|
|
const simulator = map.getMockGPSSimulator();
|
|
|
|
|
if (simulator) {
|
|
|
|
|
simulator.setSpeed(speed);
|
|
|
|
|
addLogEntry('simulation', `Speed changed to ${speedSelect.options[speedSelect.selectedIndex].text}`);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (voiceCheckbox && map) {
|
|
|
|
|
// Set initial state
|
|
|
|
|
voiceCheckbox.checked = true;
|
|
|
|
|
|
|
|
|
|
voiceCheckbox.addEventListener('change', () => {
|
|
|
|
|
map.setVoiceEnabled(voiceCheckbox.checked);
|
|
|
|
|
addLogEntry('simulation', `Voice ${voiceCheckbox.checked ? 'enabled' : 'disabled'}`);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-05 12:03:22 +00:00
|
|
|
// Set up navigation buttons
|
|
|
|
|
const locations: Record<string, [number, number]> = {
|
|
|
|
|
paris: [2.3522, 48.8566],
|
|
|
|
|
london: [-0.1276, 51.5074],
|
|
|
|
|
newyork: [-74.006, 40.7128],
|
|
|
|
|
tokyo: [139.6917, 35.6895],
|
|
|
|
|
sydney: [151.2093, -33.8688],
|
|
|
|
|
rio: [-43.1729, -22.9068],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Object.entries(locations).forEach(([name, coords]) => {
|
|
|
|
|
const btn = elementArg.querySelector(`#nav-${name}`) as HTMLButtonElement;
|
|
|
|
|
if (btn && map) {
|
|
|
|
|
btn.addEventListener('click', () => map.flyTo(coords, 13));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Set up control buttons
|
|
|
|
|
const clearBtn = elementArg.querySelector('#btn-clear') as HTMLButtonElement;
|
|
|
|
|
const fitBtn = elementArg.querySelector('#btn-fit') as HTMLButtonElement;
|
|
|
|
|
const downloadBtn = elementArg.querySelector('#btn-download') as HTMLButtonElement;
|
|
|
|
|
const loadBtn = elementArg.querySelector('#btn-load') as HTMLButtonElement;
|
|
|
|
|
|
|
|
|
|
if (clearBtn && map) {
|
|
|
|
|
clearBtn.addEventListener('click', () => {
|
|
|
|
|
map.clearAllFeatures();
|
|
|
|
|
updateFeatureDisplay();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fitBtn && map) {
|
|
|
|
|
fitBtn.addEventListener('click', () => map.fitToFeatures());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (downloadBtn && map) {
|
|
|
|
|
downloadBtn.addEventListener('click', () => {
|
|
|
|
|
const geojson = map.getGeoJson();
|
|
|
|
|
const blob = new Blob([JSON.stringify(geojson, null, 2)], { type: 'application/json' });
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = 'features.geojson';
|
|
|
|
|
a.click();
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (loadBtn && map) {
|
|
|
|
|
loadBtn.addEventListener('click', () => {
|
|
|
|
|
map.loadGeoJson({
|
|
|
|
|
type: 'FeatureCollection',
|
|
|
|
|
features: [
|
|
|
|
|
{
|
|
|
|
|
type: 'Feature',
|
|
|
|
|
properties: { mode: 'polygon' },
|
|
|
|
|
geometry: {
|
|
|
|
|
type: 'Polygon',
|
|
|
|
|
coordinates: [[
|
|
|
|
|
[8.675, 50.115],
|
|
|
|
|
[8.690, 50.115],
|
|
|
|
|
[8.690, 50.105],
|
|
|
|
|
[8.675, 50.105],
|
|
|
|
|
[8.675, 50.115],
|
|
|
|
|
]],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
updateFeatureDisplay();
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-02-05 17:50:45 +00:00
|
|
|
|
|
|
|
|
// Initialize simulation controls after a tick
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
setupSimulationControls();
|
|
|
|
|
updateSimulationUI();
|
|
|
|
|
}, 100);
|
2026-02-05 12:03:22 +00:00
|
|
|
}}>
|
|
|
|
|
<div class="demo-section">
|
|
|
|
|
<h2 class="demo-title">Interactive Map with Drawing Tools</h2>
|
|
|
|
|
<p class="demo-description">
|
|
|
|
|
Click on the drawing tools in the toolbar to create shapes on the map.
|
|
|
|
|
Use the Select tool to edit, move, or delete shapes. All features are
|
|
|
|
|
rendered using terra-draw with MapLibre GL JS.
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<div class="map-wrapper">
|
|
|
|
|
<dees-geo-map
|
|
|
|
|
.center=${[8.6821, 50.1109] as [number, number]}
|
|
|
|
|
.zoom=${12}
|
|
|
|
|
.showSearch=${true}
|
|
|
|
|
.showNavigation=${true}
|
|
|
|
|
></dees-geo-map>
|
|
|
|
|
</div>
|
2026-02-05 15:07:33 +00:00
|
|
|
|
|
|
|
|
<p class="demo-description" style="margin-top: 12px; font-size: 12px; opacity: 0.7;">
|
|
|
|
|
<strong>Traffic:</strong> To enable live traffic, set the <code>trafficApiKey</code> property with your HERE API key
|
|
|
|
|
(free tier: 250k requests/month at <a href="https://developer.here.com" target="_blank" style="color: inherit;">developer.here.com</a>).
|
|
|
|
|
</p>
|
2026-02-05 12:03:22 +00:00
|
|
|
</div>
|
|
|
|
|
|
2026-02-05 17:50:45 +00:00
|
|
|
<div class="demo-section simulation-section">
|
|
|
|
|
<h2 class="demo-title">GPS Simulation & Voice Navigation</h2>
|
|
|
|
|
<p class="demo-description">
|
|
|
|
|
Calculate a route using the navigation panel, then use these controls to simulate GPS movement
|
|
|
|
|
along the route with voice-guided turn-by-turn instructions.
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<div class="simulation-controls-grid">
|
|
|
|
|
<div class="sim-playback">
|
|
|
|
|
<button class="control-button primary" id="sim-start" disabled>
|
|
|
|
|
▶ Start
|
|
|
|
|
</button>
|
|
|
|
|
<button class="control-button" id="sim-pause" disabled>
|
|
|
|
|
⏸ Pause
|
|
|
|
|
</button>
|
|
|
|
|
<button class="control-button danger" id="sim-stop" disabled>
|
|
|
|
|
⏹ Stop
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="sim-speed">
|
|
|
|
|
<span class="sim-speed-label">Speed:</span>
|
|
|
|
|
<select class="sim-speed-select" id="sim-speed">
|
|
|
|
|
${getSpeedPresets().map(preset => html`
|
|
|
|
|
<option value="${preset.id}" ?selected=${preset.id === 'city'}>
|
|
|
|
|
${preset.name} (${preset.kmh} km/h)
|
|
|
|
|
</option>
|
|
|
|
|
`)}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="sim-voice">
|
|
|
|
|
<input type="checkbox" id="sim-voice" class="sim-voice-checkbox" checked />
|
|
|
|
|
<label for="sim-voice" class="sim-voice-label">Voice Guidance</label>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="sim-progress">
|
|
|
|
|
<div class="sim-progress-header">
|
|
|
|
|
<span class="sim-progress-label">Route Progress</span>
|
|
|
|
|
<span class="sim-progress-value" id="sim-progress-value">0%</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="sim-progress-bar">
|
|
|
|
|
<div class="sim-progress-fill" id="sim-progress-fill" style="width: 0%"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="sim-status no-route" id="sim-status">
|
|
|
|
|
Calculate a route first to enable simulation
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-05 12:03:22 +00:00
|
|
|
<div class="demo-section">
|
|
|
|
|
<h2 class="demo-title">Quick Navigation</h2>
|
|
|
|
|
<div class="locations-grid">
|
|
|
|
|
<button class="location-button" id="nav-paris">Paris</button>
|
|
|
|
|
<button class="location-button" id="nav-london">London</button>
|
|
|
|
|
<button class="location-button" id="nav-newyork">New York</button>
|
|
|
|
|
<button class="location-button" id="nav-tokyo">Tokyo</button>
|
|
|
|
|
<button class="location-button" id="nav-sydney">Sydney</button>
|
|
|
|
|
<button class="location-button" id="nav-rio">Rio</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="demo-section">
|
|
|
|
|
<h2 class="demo-title">Controls</h2>
|
|
|
|
|
<div class="controls-row">
|
|
|
|
|
<button class="control-button" id="btn-clear">Clear All Features</button>
|
|
|
|
|
<button class="control-button" id="btn-fit">Fit to Features</button>
|
|
|
|
|
<button class="control-button" id="btn-download">Download GeoJSON</button>
|
|
|
|
|
<button class="control-button" id="btn-load">Load Sample Data</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="demo-section">
|
|
|
|
|
<h2 class="demo-title">Event Log</h2>
|
|
|
|
|
<div class="event-log">
|
|
|
|
|
<div id="event-log">
|
|
|
|
|
<div class="event-entry"><span class="event-type">init</span>: Waiting for map...</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="demo-section">
|
|
|
|
|
<h2 class="demo-title">Current Features (GeoJSON)</h2>
|
|
|
|
|
<div class="feature-display">
|
|
|
|
|
<pre class="feature-json" id="feature-json">{ "type": "FeatureCollection", "features": [] }</pre>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</dees-demowrapper>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|