# Technical Specification: Map Badge Card Enhanced Features **Created**: 2025-11-19 **Planner**: @planner **Status**: Draft → Ready for Implementation ## Overview This technical specification details the implementation of four enhanced features for the Home Assistant Map Badge Card: 1. **Adjustable Marker Size** - Configurable marker presets (small/medium/large) via UI 2. **Speed Calculation** - Real-time speed tracking using position history with Haversine formula 3. **Popup Follow Fix** - Ensure info popups remain anchored to moving markers 4. **Predicted Activity** - Speed-based activity inference as alternative to sensor data ### Objectives - [ ] Enable users to customize marker appearance with three size presets - [ ] Calculate and display accurate travel speed in entity popups - [ ] Fix popup positioning bugs for moving entities across both map providers - [ ] Provide fallback activity detection based on movement patterns - [ ] Maintain 100% backward compatibility with existing configurations - [ ] Ensure consistent behavior across Leaflet and Google Maps providers ### Success Criteria - [ ] Marker sizes render correctly at 36px, 48px, and 64px dimensions - [ ] Speed calculations show < 10% error compared to GPS accuracy - [ ] Popups track marker movement without reposition lag - [ ] Activity prediction matches documented thresholds (1, 7, 25 km/h) - [ ] All new configuration options appear in UI editor - [ ] Existing cards continue functioning without configuration changes - [ ] Unit test coverage ≥ 80% for new calculation logic - [ ] Manual testing confirms functionality on both map providers ## Architecture ### Component Structure ``` Map Badge Card System ├── Main Component (map-badge-card.js) │ └── ConfigManager (config-manager.js) │ ├── EntityDataFetcher (entity-data-fetcher.js) ← ADD: Speed calc & history │ ├── IframeMessenger (iframe-messenger.js) ← UPDATE: Pass marker_size │ └── Editor UI Layer (editor-ui.js, editor-handlers.js) ← UPDATE: New controls └── Map Renderer (map-badge-v2.html) ← UPDATE: Dynamic sizes, popup tracking ├── Leaflet Implementation └── Google Maps Implementation ``` ### Technology Stack - **Language**: JavaScript ES6 Modules - **Framework**: Home Assistant Custom Card (Legacy API) - **Map Libraries**: Leaflet 1.x, Google Maps JavaScript API - **Key Algorithms**: Haversine formula for distance calculation - **Browser APIs**: PostMessage for iframe communication ### Design Patterns 1. **Configuration Cascade**: Default → User Config → Runtime Override - Prevents cascading re-renders by batching config updates - Maintains backward compatibility with optional fields 2. **Position History Ring Buffer**: Fixed-size array (5 entries) per entity - Prevents memory leaks from infinite history growth - Provides sufficient data points for accurate speed averaging 3. **Data Transformation Pipeline**: Raw HA State → Cached Entity Data → Map Parameters - Centralizes calculation logic in EntityDataFetcher - Ensures consistent data format across consumers 4. **Provider Abstraction**: Unified interface for Leaflet/Google Maps - Minimizes provider-specific code duplication - Enables feature parity across implementations ## Implementation Phases ### Phase 1: Foundation (Data Structures & Configuration) **Objective**: Establish constants, config validation, and parameter passing infrastructure **Assigned to**: @coder-mid **Tasks**: 1. **Update Constants** (`src/constants.js`) - Add `MARKER_SIZES` object with small/medium/large presets (marker width, badge size, popup offset) - Add `ACTIVITY_THRESHOLDS` object defining speed boundaries (still, walking, cycling, vehicle) - Extend `DEFAULT_CONFIG` with `marker_size`, `use_predicted_activity`, `activity_source` - **Complexity**: Simple (data definition only) - **Dependencies**: None - **Deliverables**: Updated constants file with 3 new exports 2. **Update ConfigManager** (`src/config-manager.js`) - Extend `setConfig()` to validate and store new marker/activity configuration - Add marker_size to `buildIframeParams()` return value - Add boolean flag tracking for config change detection - **Complexity**: Simple (config pass-through, no logic changes) - **Dependencies**: Phase 1.1 - **Deliverables**: Modified config manager with new parameter handling ### Phase 2: Core Features (Calculation & Rendering) **Objective**: Implement speed calculation engine and dynamic marker sizing **Assigned to**: @coder-senior **Tasks**: 3. **Enhance EntityDataFetcher** (`src/entity-data-fetcher.js`) - Add `_positionHistory` Map (entityId → PositionEntry[]) - Implement `_calculateHaversineDistance()` (returns meters) - Implement `calculateSpeed()` (returns {speed_kmh, speed_mph, time_diff}) - Implement `_updatePositionHistory()` with ring buffer logic (max 5 entries) - Implement `predictActivity()` (returns string: 'still', 'walking', 'on_bicycle', 'in_vehicle') - Modify `fetchEntities()` to calculate/store speed, update history - Modify `prepareEntityData()` to include speed and predicted_activity fields - **Complexity**: Medium (requires mathematical accuracy + caching) - **Dependencies**: Phase 1.1 - **Deliverables**: Enhanced data fetcher with calculation methods 4. **Update Map HTML Rendering** (`src/map-badge-v2.html`) - Parse `marker_size` URL parameter on load - Create CSS variables: `--marker-size`, `--badge-size`, `--popup-offset` - Update `.custom-marker-wrapper` CSS to use variables - Modify Leaflet icon creation to use dynamic `iconSize`, `iconAnchor`, `popupAnchor` - Modify Google Maps `CustomMarker.draw()` to use dynamic dimensions - Ensure marker CSS class names remain consistent - **Complexity**: Medium (requires CSS/JS synchronization) - **Dependencies**: Phase 2.1, Phase 1.2 - **Deliverables**: Map renderer supporting 3 size presets ### Phase 3: UI Integration (Editor Controls) **Objective**: Add configuration controls to visual editor **Assigned to**: @coder-junior **Tasks**: 5. **Add Marker Size Control** (`src/editor-ui.js`) - Extend `_generateAppearanceSection()` with `` - Include three `` entries: small (36px), medium (48px), large (64px) - Add descriptive helper text below dropdown - **Complexity**: Simple (HTML template modification) - **Dependencies**: Phase 2.2 - **Deliverables**: Editor UI with marker size dropdown 6. **Add Activity Source Control** (`src/editor-ui.js`) - Extend appearance section with `` - Include two options: 'Activity Sensor' and 'Speed-Based Prediction' - Add multi-line helper text explaining speed thresholds - **Complexity**: Simple (HTML template modification) - **Dependencies**: Phase 3.1 - **Deliverables**: Editor UI with activity source selector 7. **Attach Editor Event Handlers** (`src/editor-handlers.js`) - Add `_attachMarkerSizeListener()` method - Add `_attachActivitySourceListener()` method - Register both in `attachListeners()` call - Ensure handlers use `onChange()` callback properly - **Complexity**: Simple (event listener pattern) - **Dependencies**: Phase 3.2 - **Deliverables**: Functional UI controls with state management ### Phase 4: Popup & Display Logic **Objective**: Fix popup anchoring and enhance content with speed data **Assigned to**: @coder-mid **Tasks**: 8. **Fix Popup Following Behavior** (`src/map-badge-v2.html`) - **Leaflet**: Ensure `marker.update()` called when popup open during position change - **Google Maps**: Store InfoWindow reference per marker, call `setPosition()` on update - Add position change detection to trigger popup reposition - Prevent popup flicker during rapid updates - **Complexity**: Medium (requires timing/animation testing) - **Dependencies**: Phase 2.2 - **Deliverables**: Anchored popups that track marker movement 9. **Enhance Popup HTML** (`src/map-badge-v2.html`) - Modify `createPopupHTML()` to accept `speedData` parameter - Add speed display row with icon when `speedData.speed_kmh !== null` - Format speed as "X.X km/h" (one decimal place) - Position speed below state, above activity in visual hierarchy - **Complexity**: Medium (requires styling + conditional rendering) - **Dependencies**: Phase 4.1 - **Deliverables**: Richer popup content with speed information 10. **Implement Activity Selection Logic** (`src/entity-data-fetcher.js`) - Add `_getActivityToUse(data, config)` helper method - Respect `config.activity_source` when determining displayed activity - Return predicted activity when `speed_predicted` selected and speed exists - Fallback to sensor activity otherwise - Modify `prepareEntityData()` to use activity selection logic - **Complexity**: Simple (conditional logic) - **Dependencies**: Phase 4.2 - **Deliverables**: Configurable activity source behavior ### Phase 5: Integration & Dynamic Updates **Objective**: Enable runtime config changes and ensure system-wide consistency **Assigned to**: @coder-mid **Tasks**: 11. **Extend IframeMessenger** (`src/iframe-messenger.js`) - Add `markerSize` parameter to `sendConfigUpdate()` - Include marker_size in PostMessage payload - Maintain backward compatibility (default to 'medium') - **Complexity**: Simple (parameter pass-through) - **Dependencies**: Phase 4.3 - **Deliverables**: Messenger supporting marker size propagation 12. **Update Config Change Detection** (`src/map-badge-card.js`) - Add `'marker_size'` to `visualPropsChanged` check array - Pass `config.marker_size` to `_messenger.sendConfigUpdate()` - Ensure config reload triggers re-render with new marker size - **Complexity**: Simple (property tracking) - **Dependencies**: Phase 5.1 - **Deliverables**: Main card component integrated with new features 13. **Handle Runtime Marker Size Changes** (`src/map-badge-v2.html`) - Listen for `marker_size` in config-update PostMessage - Update CSS variables when new size received - Trigger `updateAllMarkers()` to re-render with new dimensions - Preserve popup open/close state during re-render toggle - **Complexity**: Simple (event handler + batch update) - **Dependencies**: Phase 5.2 - **Deliverables**: Live marker size adjustment without reload ## API Contracts ### Configuration Schema **Location**: User config passed to `setConfig()` **New Properties**: | Name | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `marker_size` | string | No | `'medium'` | Marker preset: 'small', 'medium', 'large' | | `activity_source` | string | No | `'sensor'` | Activity mode: 'sensor' or 'speed_predicted' | | `use_predicted_activity` | boolean | No | `false` | (Deprecated) Backward compatibility flag | **Example Valid Configurations**: ```yaml # Minimal config (uses defaults) type: custom:map-badge-card entities: - person: person.john # Marker size only type: custom:map-badge-card marker_size: large google_api_key: YOUR_KEY # Full feature set type: custom:map-badge-card marker_size: medium activity_source: speed_predicted entities: - person: person.john activity: sensor.john_phone_activity ``` ### Iframe Parameters **Location**: URL query string passed to `map-badge-v2.html` **New Parameters**: | Parameter | Type | Example | Description | |-----------|------|---------|-------------| | `marker_size` | string | `marker_size=large` | Selected size preset | **Complete URL Example**: ``` map-badge-v2.html?marker_size=medium&dark_mode=false&theme_color=%23ff9800&... ``` ### PostMessage API (Config Updates) **Location**: Communication from main card to iframe **Enhanced Message Structure**: ```typescript interface ConfigUpdateMessage { type: 'config-update'; zones: ZoneConfig[]; activities: ActivityConfig[]; marker_border_radius: number; badge_border_radius: number; marker_size: string; // NEW: Size preset timestamp: number; } ``` **Sending Example**: ```javascript // From map-badge-card.js _messenger.sendConfigUpdate( config.zones, config.activities, config.marker_border_radius, config.badge_border_radius, config.marker_size // New parameter ); ``` **Receiving Example**: ```javascript // In map-badge-v2.html if (event.data.type === 'config-update') { if (event.data.marker_size) { applyMarkerSize(event.data.marker_size); } } ``` ## Data Models ### Marker Size Presets **Location**: `src/constants.js` ```javascript interface MarkerSize { marker: number; // Base marker diameter (pixels) badge: number; // Badge diameter (pixels) popupOffset: number; // Popup anchor offset (pixels, negative) } export const MARKER_SIZES: Record = { small: { marker: 36, badge: 15, popupOffset: -52 }, medium: { marker: 48, badge: 20, popupOffset: -68 }, large: { marker: 64, badge: 24, popupOffset: -88 } }; ``` **Usage**: - Leaflet: `iconSize = [marker, marker + 14]` - Google Maps: `div.style.width = marker + 'px'` - CSS: `--marker-size: ${marker}px` ### Activity Thresholds **Location**: `src/constants.js` ```javascript interface ActivityThresholds { still: number; // km/h threshold for "still" (upper bound) walking: number; // km/h threshold for "on_bicycle" (upper bound) cycling: number; // km/h threshold for "in_vehicle" (upper bound) vehicle: number; // km/h threshold for "vehicle" (unused, for parity) } export const ACTIVITY_THRESHOLDS: ActivityThresholds = { still: 1, // < 1 km/h → 'still' walking: 7, // 1-7 km/h → 'walking' cycling: 25, // 7-25 km/h → 'on_bicycle' vehicle: 25 // > 25 km/h → 'in_vehicle' }; ``` **Activity Mapping**: ```javascript function predictActivity(speedKmh: number): string { if (speedKmh < ACTIVITY_THRESHOLDS.still) return 'still'; if (speedKmh < ACTIVITY_THRESHOLDS.walking) return 'walking'; if (speedKmh < ACTIVITY_THRESHOLDS.cycling) return 'on_bicycle'; return 'in_vehicle'; } ``` ### Position History Entry **Location**: Internal to `EntityDataFetcher` ```typescript interface PositionEntry { latitude: number; longitude: number; timestamp: number; // Unix timestamp in milliseconds } // Storage structure private _positionHistory: Map; ``` **History Management**: - Maximum 5 entries per entity (ring buffer) - Oldest entries removed automatically - New entries appended with current timestamp - Speed calculation requires ≥ 2 entries ### Speed Calculation Result **Location**: Calculated in `EntityDataFetcher`, passed to map ```typescript interface SpeedData { speed_kmh: number; // Speed in kilometers per hour speed_mph: number; // Speed in miles per hour time_diff: number; // Time difference in seconds distance_m?: number; // Optional: distance in meters accuracy?: number; // Optional: GPS accuracy if available } ``` **Calculation Formula**: ```javascript const distance = haversine(prevLat, prevLon, currLat, currLon); // meters const timeDiff = (currTime - prevTime) / 1000; // seconds const speedKmh = (distance / 1000) / (timeDiff / 3600); // km/h ``` ### Entity Data Output **Location**: `EntityDataFetcher.prepareEntityData()` return value ```typescript interface EntityDataMap { [entityId: string]: { state: string; attributes: { friendly_name: string; latitude: number; longitude: number; entity_picture?: string; // ... other HA attributes }; activity: string; // Selected activity ('sensor' or 'speed_predicted') speed: SpeedData | null; // Calculated speed (null if insufficient history) predicted_activity: string | null; // Calculated activity (null if no speed) zone: string | null; zone_color: string | null; }; } ``` **Activity Selection Logic**: ```javascript const activity = config.activity_source === 'speed_predicted' && speedData ? predictActivity(speedData.speed_kmh) : sensorActivity; ``` ## Testing Requirements ### Unit Tests (Target: 80% coverage) **Location**: New test files or existing test structure 1. **Haversine Distance Calculation** ```javascript // Test: Known locations with expected distances test('haversine: New York to London', () => { const distance = haversine(40.7128, -74.0060, 51.5074, -0.1278); expect(distance).toBeCloseTo(5570000, -4); // ~5570km }); // Edge: Same location → distance = 0 // Edge: Antipodal points → half circumference ``` 2. **Speed Calculation** ```javascript // Test: Simulated position history test('speed: 100m in 10 seconds', () => { const history = [ { lat: 0, lon: 0, timestamp: 0 }, { lat: 0.0009, lon: 0, timestamp: 10000 } // ~100m north ]; const speed = calculateSpeed('person.test', history[1]); expect(speed.speed_kmh).toBeCloseTo(36, 1); // 100m/10s = 36 km/h }); // Edge: Single history entry → returns null // Edge: Zero time difference → returns null // Edge: Negative coordinates → still calculates correctly ``` 3. **Activity Prediction** ```javascript // Test: All threshold boundaries test('predictActivity: walking threshold', () => { expect(predictActivity(0.5)).toBe('still'); expect(predictActivity(1)).toBe('still'); // Edge case expect(predictActivity(1.1)).toBe('walking'); expect(predictActivity(6.9)).toBe('walking'); expect(predictActivity(7)).toBe('walking'); // Edge case expect(predictActivity(7.1)).toBe('on_bicycle'); expect(predictActivity(24.9)).toBe('on_bicycle'); expect(predictActivity(25)).toBe('on_bicycle'); // Edge case expect(predictActivity(25.1)).toBe('in_vehicle'); }); // Edge: Negative speed → should not occur, but handle gracefully // Edge: Null/undefined speed → returns null ``` 4. **Marker Size CSS Application** ```javascript // Test: CSS variable setting test('applyMarkerSize: small preset', () => { applyMarkerSize('small'); expect(document.documentElement.style.getPropertyValue('--marker-size')).toBe('36px'); }); // Edge: Invalid preset → defaults to medium // Edge: Empty string → defaults to medium ``` ### Integration Tests **Location**: Test card loading with configuration 1. **Config Propagation Flow** ```javascript // Test: marker_size flows through entire system test('config propagation: marker_size', async () => { const config = { marker_size: 'large', /* ... */ }; card.setConfig(config); await card.updateComplete; // Verify iframe URL contains marker_size=large expect(iframe.src).toContain('marker_size=large'); // Verify CSS variable set in iframe expect(iframe.contentDocument.documentElement.style.getPropertyValue('--marker-size')).toBe('64px'); }); ``` 2. **Position History Persistence** ```javascript // Test: History maintained across multiple updates test('position history: 5 updates', async () => { const fetcher = new EntityDataFetcher(); // Simulate 5 position updates for (let i = 0; i < 5; i++) { await fetcher.fetchEntities(); await sleep(1000); } // Verify history has 5 entries const history = fetcher._positionHistory['person.test']; expect(history.length).toBe(5); // 6th update should still have 5 (ring buffer) await fetcher.fetchEntities(); expect(history.length).toBe(5); }); ``` 3. **Popup Following Behavior** ```javascript // Test: Markers + markers move → popup stays anchored test('popup following: Leaflet', async () => { const marker = createMarkerOSM(entityData); marker.bindPopup('Test').openPopup(); const initialPopupPos = marker.getPopup().getLatLng(); // Move marker marker.setLatLng([newLat, newLon]); marker.update(); const newPopupPos = marker.getPopup().getLatLng(); expect(newPopupPos.lat).toBeCloseTo(newLat, 6); expect(newPopupPos.lng).toBeCloseTo(newLon, 6); }); // Repeat for Google Maps with InfoWindow.setPosition() ``` 4. **Activity Override Logic** ```javascript // Test: speed_predicted overrides sensor test('activity source: speed_predicted', () => { const config = { activity_source: 'speed_predicted' }; const data = { activity: { state: 'walking' }, speed: { speed_kmh: 50 } }; const result = entityDataFetcher.prepareEntityData(config); expect(result['person.test'].activity).toBe('in_vehicle'); // From speed, not sensor }); ``` ### Manual Testing Scenarios **Environment**: Real Home Assistant instance + mobile device 1. **Marker Size Visualization** - [ ] Configure card with `marker_size: small` → verify 36px markers - [ ] Configure card with `marker_size: medium` → verify 48px markers (default) - [ ] Configure card with `marker_size: large` → verify 64px markers - [ ] Test both Leaflet and Google Maps providers - [ ] Verify badges scale proportionally within markers - [ ] Check popup positioning aligns correctly at all sizes 2. **Speed Calculation Accuracy** - [ ] Walk 100m at constant speed, verify calculated speed matches expected - [ ] Drive at 50 km/h, verify speed display within ±2 km/h - [ ] First card load shows no speed (waiting for history) - [ ] After 2+ position updates, speed appears in popup - [ ] Check speed updates smoothly (not jumping erratically) 3. **Popup Tracking Behavior** - [ ] Open popup, move ~10 meters → popup should move with marker - [ ] Rapid movement (driving) → popup stays anchored without lag - [ ] Switch between map providers → behavior consistent - [ ] Popup close/reopen during movement → re-anchors correctly - [ ] No visual flicker or reposition artifacts 4. **Activity Prediction Validation** - [ ] Stand still (< 1 km/h) → shows 'still' - [ ] Walk slowly (3 km/h) → shows 'walking' - [ ] Cycle (15 km/h) → shows 'on_bicycle' - [ ] Drive (60 km/h) → shows 'in_vehicle' - [ ] Switch between sensor and speed_predicted → activity updates - [ ] Speed-based mode works without activity sensor configured 5. **Configuration Editor UX** - [ ] Marker size dropdown shows preview text with pixel dimensions - [ ] Activity source selector includes helpful threshold explanation - [ ] Changes reflect immediately in preview (if available) - [ ] Configuration YAML updates correctly when UI changed - [ ] Form validation prevents invalid values 6. **Backward Compatibility** - [ ] Existing cards without new config options → work unchanged - [ ] Default values match previous hardcoded behavior - [ ] Migration from old to new config → seamless transition - [ ] No console errors or warnings with legacy configurations ### Performance Requirements - **Speed Calculation**: < 5ms per entity (single haversine calculation) - **Position History**: Memory < 1KB per entity (5 entries × ~24 bytes) - **Popup Updates**: < 16ms frame time (60 FPS target) - **Config Updates**: < 50ms full re-render with new marker size - **Concurrent Entities**: Tested with 10+ entities without degradation ## Dependencies & Risks ### External Dependencies - **Home Assistant Frontend**: Must support existing card API - **Browser Support**: ES6 modules, CSS variables (Chrome 49+, Firefox 31+, Safari 9.1+) - **Map APIs**: Leaflet (bundled), Google Maps (external API key required) - **No new runtime dependencies** (Haversine formula self-implemented) ### Internal Dependencies 1. **ConfigManager must be updated before EntityDataFetcher** - Reason: Default values required for speed calculation logic - Mitigation: Phased implementation with clear ordering 2. **EntityDataFetcher must be complete before Map HTML** - Reason: Map renderer consumes speed/speed/predicted_activity fields - Mitigation: Phase 2 fully completes before Phase 4 begins 3. **Editor UI must be complete before integration testing** - Reason: Manual testing requires UI configuration controls - Mitigation: Phase 3 prioritized early in development cycle ### Risk Assessment #### Risk 1: GPS Jitter Causes Erratic Speed Readings **Likelihood**: Medium **Impact**: Medium (poor user experience) **Mitigation**: - Implement minimum time threshold (5 seconds) between speed calculations - Average across multiple history points (not just last 2) - Add GPS accuracy filtering if available in HA attributes - Display "-- km/h" when accuracy below threshold #### Risk 2: Google Maps InfoWindow Performance Issues **Likelihood**: Low **Impact**: High (UI freezes during movement) **Mitigation**: - Throttle setPosition() calls to max 4x per second - Test with 10+ moving markers simultaneously - Provide fallback to disable auto-follow for power users - Document performance considerations in README #### Risk 3: Position History Lost on Iframe Reload **Likelihood**: High **Impact**: Low-Medium (speed unavailable temporarily) **Mitigation**: - Document that history is main-card-scoped, not persisted across reloads - Reduce iframe reload frequency (already batched in existing code) - Accept temporary speed unavailability as acceptable UX - Consider future enhancement: persist history in localStorage #### Risk 4: Mobile Browser CSS Variable Support **Likelihood**: Low **Impact**: Medium (marker sizing fails on old browsers) **Mitigation**: - Provide JavaScript fallback: direct style manipulation if CSS variables unsupported - Test on iOS Safari 10+, Android Chrome 50+ - Document minimum browser requirements in README - Graceful degradation: stick to medium size if feature detection fails #### Risk 5: Activity Sensor vs Speed Mismatch Confuses Users **Likelihood**: Medium **Impact**: Low (user education issue) **Mitigation**: - Add explicit UI label: "Activity (from speed)" when in predictive mode - Include configuration helper text explaining thresholds - Document in README when to use each mode - Consider debug logging to help users understand selection logic ## Open Questions 1. **Q**: Should speed calculation continue when device is stationary? **A**: Yes, but will return 0 km/h. Helps indicate "still" vs "no data" 2. **Q**: How to handle time sync issues if HA server clock drifts? **A**: Use `state.last_changed` timestamp (UTC from HA), not client time 3. **Q**: Should popup follow be configurable (enable/disable)? **A**: Defer to future enhancement if requested. Current plan: always follow. 4. **Q**: Add imperial (mph) display option? **A**: Calculate both but display km/h only for now. Add config option if users request. 5. **Q**: Persist position history across browser sessions? **A**: Not in scope. History is ephemeral by design (security/privacy consideration). ## Implementation Notes ### Code Quality Standards - **ESLint**: Maintain existing rules - **Formatting**: Match existing 2-space indentation, semicolons - **Comments**: Add JSDoc for new public methods, inline comments for complex calculations - **Naming**: `camelCase` for variables/functions, `PascalCase` for classes, `UPPER_CASE` for constants ### Performance Optimizations - **Memoization**: Cache marker size CSS variable values to avoid repeated calculations - **Batch Updates**: Use requestAnimationFrame for popup position updates if needed - **Early Returns**: Return early in speed calc if insufficient history (avoid math operations) - **Object Pooling**: Reuse PositionEntry objects to reduce GC pressure (if many entities) ### Backward Compatibility - All new config properties optional with sensible defaults - Default `marker_size: 'medium'` matches current 48px hardcoded size - Default `activity_source: 'sensor'` preserves existing behavior - Graceful fallbacks in map renderer if parameters missing - Existing card configs continue working without changes ### Debugging Support Add debug logging (respect existing `debugMode` flag): ```javascript if (this._debugMode) { console.log(`[MapBadge] Speed: ${speedKmh.toFixed(1)} km/h for ${entityId}`); console.log(`[MapBadge] Activity: ${activity} (source: ${config.activity_source})`); } ``` ### Future Extensibility - **Marker Size**: Easy to add 'xlarge' preset (just add to MARKER_SIZES) - **Activity Thresholds**: Could be user-configurable (expose in UI) - **Speed Calculation**: Could add averaging window configuration - **Popup Content**: Speed display could be toggled via config --- ## Status Updates - **2025-11-19**: Technical specification created by @documentator - **2025-11-19**: Ready for implementation phase (pending coder assignment) - **Next**: Phase 1 development to begin upon Vexa delegation **Specification Status**: ✅ **READY FOR IMPLEMENTATION**