diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..8b97913 --- /dev/null +++ b/changelog.md @@ -0,0 +1,17 @@ +# Changelog + +## 2026-02-05 - 1.1.0 - feat(geo-map) +add live traffic visualization and traffic-aware routing with pluggable providers and UI integration + +- Introduce TrafficController and traffic layer rendering (auto-refresh, zoom gating, legend). +- Add traffic providers: HereTrafficProvider (HERE API) and ValhallaTrafficProvider (self-hosted) with ITrafficProvider interface and config types. +- Integrate traffic-aware routing into NavigationController: fetch and render traffic-aware routes, show congestion level and delay in the navigation panel. +- Add toolbar/header UI: traffic toggle button, traffic legend, new icons, overlay grid and header toolbar layout; update demo and readme.hints.md with usage and provider examples. +- Add new source files: geo-map.traffic.ts and geo-map.traffic.providers.ts; export types from index.ts and update icons and styles (trafficStyles, headerToolbarStyles). +- Update component styles (overlay grid, toolbar, traffic legend) and refactor parts of dees-geo-map for modular controllers. +- Include new header toolbar artwork assets and update demo text; bump devDependencies: @git.zone/tswatch ^3.1.0 and @types/node ^25.2.1. + +## 2026-02-05 - 1.0.0 - initial +Initial release. + +- Initial commit: project scaffold and first files \ No newline at end of file diff --git a/header-toolbar-fixed.png b/header-toolbar-fixed.png new file mode 100644 index 0000000..9ad0b33 Binary files /dev/null and b/header-toolbar-fixed.png differ diff --git a/header-toolbar-full.png b/header-toolbar-full.png new file mode 100644 index 0000000..0f63bd6 Binary files /dev/null and b/header-toolbar-full.png differ diff --git a/header-toolbar-layout.png b/header-toolbar-layout.png new file mode 100644 index 0000000..0f63bd6 Binary files /dev/null and b/header-toolbar-layout.png differ diff --git a/header-toolbar-native.png b/header-toolbar-native.png new file mode 100644 index 0000000..0ea4217 Binary files /dev/null and b/header-toolbar-native.png differ diff --git a/header-toolbar-v3.png b/header-toolbar-v3.png new file mode 100644 index 0000000..b08c601 Binary files /dev/null and b/header-toolbar-v3.png differ diff --git a/map-layout-test.png b/map-layout-test.png new file mode 100644 index 0000000..a3f965f Binary files /dev/null and b/map-layout-test.png differ diff --git a/map-no-toolbar-test.png b/map-no-toolbar-test.png new file mode 100644 index 0000000..1a3a85b Binary files /dev/null and b/map-no-toolbar-test.png differ diff --git a/package.json b/package.json index b48745c..6a48921 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,9 @@ "@git.zone/tsbuild": "^4.1.2", "@git.zone/tsbundle": "^2.8.3", "@git.zone/tstest": "^3.1.8", - "@git.zone/tswatch": "^3.0.1", + "@git.zone/tswatch": "^3.1.0", "@push.rocks/projectinfo": "^5.0.2", - "@types/node": "^25.2.0" + "@types/node": "^25.2.1" }, "files": [ "ts/**/*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 112fd2c..683d8ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,14 +40,14 @@ importers: specifier: ^3.1.8 version: 3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3) '@git.zone/tswatch': - specifier: ^3.0.1 - version: 3.0.1(@tiptap/pm@2.27.2) + specifier: ^3.1.0 + version: 3.1.0(@tiptap/pm@2.27.2) '@push.rocks/projectinfo': specifier: ^5.0.2 version: 5.0.2 '@types/node': - specifier: ^25.2.0 - version: 25.2.0 + specifier: ^25.2.1 + version: 25.2.1 packages: @@ -263,6 +263,9 @@ packages: '@cloudflare/workers-types@4.20260203.0': resolution: {integrity: sha512-XD2uglpGbVppjXXLuAdalKkcTi/i4TyQSx0w/ijJbvrR1Cfm7zNkxtvFBNy3tBNxZOiFIJtw5bszifQB1eow6A==} + '@cloudflare/workers-types@4.20260205.0': + resolution: {integrity: sha512-LTnpvcodmiuMwxmbrO2Fd0+Avbm2UVLLJxT8J2pRWPfoM44gmbIecXwOPZmDAMeadKWrBsQ+B0sloQAhUu5fpA==} + '@configvault.io/interfaces@1.0.17': resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==} @@ -486,8 +489,8 @@ packages: resolution: {integrity: sha512-nmiLGeOkKMkLDyIk5BUBLx5ExskFbKHKlPdrWCARPVFkU4cAAiuIyJWVfLwISoS0TO/zSInLqArPwIc76yvaNw==} hasBin: true - '@git.zone/tswatch@3.0.1': - resolution: {integrity: sha512-vrAkKM5ff/e1BLNkrIRXnTIkMyjl/uW49c1cYaw2nYGloM6/wT1FSwYjwh6BcDkHIYMnzS30SOy9jSYRptW/iw==} + '@git.zone/tswatch@3.1.0': + resolution: {integrity: sha512-R2ZI+j1OKVgd0zTbtGtJjyt7r2kF0Z4nl8neolHuQL+jpr16i2NHVfVK92uIeeZDnJSqo5vf7Syt0XeQ4rz2HA==} hasBin: true '@happy-dom/global-registrator@15.11.7': @@ -1029,6 +1032,9 @@ packages: '@push.rocks/taskbuffer@3.5.0': resolution: {integrity: sha512-Y9WwIEIyp6oVFdj06j84tfrZIvjhbMb3DF52rYxlTeYLk3W7RPhSg1bGPCbtkXWeKdBrSe37V90BkOG7Qq8Pqg==} + '@push.rocks/taskbuffer@4.2.0': + resolution: {integrity: sha512-ttoBe5y/WXkAo5/wSMcC/Y4Zbyw4XG8kwAsEaqnAPCxa3M9MI1oV/yM1e9gU1IH97HVPidzbTxRU5/PcHDdUsg==} + '@push.rocks/webrequest@3.0.37': resolution: {integrity: sha512-fLN7kP6GeHFxE4UH4r9C9pjcQb0QkJxHeAMwXvbOqB9hh0MFNKhtGU7GoaTn8SVRGRMPc9UqZVNwo6u5l8Wn0A==} @@ -1748,11 +1754,11 @@ packages: '@types/node-forge@1.3.14': resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} - '@types/node@22.19.8': - resolution: {integrity: sha512-ebO/Yl+EAvVe8DnMfi+iaAyIqYdK0q/q0y0rw82INWEKJOBe6b/P3YWE8NW7oOlF/nXFNrHwhARrN/hdgDkraA==} + '@types/node@22.19.9': + resolution: {integrity: sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==} - '@types/node@25.2.0': - resolution: {integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==} + '@types/node@25.2.1': + resolution: {integrity: sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==} '@types/ping@0.4.4': resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==} @@ -4114,7 +4120,7 @@ snapshots: '@api.global/typedrequest': 3.2.5 '@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1) - '@cloudflare/workers-types': 4.20260203.0 + '@cloudflare/workers-types': 4.20260205.0 '@design.estate/dees-catalog': 3.42.0(@tiptap/pm@2.27.2) '@design.estate/dees-comms': 1.0.30 '@push.rocks/lik': 6.2.2 @@ -4689,6 +4695,8 @@ snapshots: '@cloudflare/workers-types@4.20260203.0': {} + '@cloudflare/workers-types@4.20260205.0': {} + '@configvault.io/interfaces@1.0.17': dependencies: '@api.global/typedrequest-interfaces': 3.0.19 @@ -5021,7 +5029,7 @@ snapshots: - utf-8-validate - vue - '@git.zone/tswatch@3.0.1(@tiptap/pm@2.27.2)': + '@git.zone/tswatch@3.1.0(@tiptap/pm@2.27.2)': dependencies: '@api.global/typedserver': 8.3.0(@tiptap/pm@2.27.2) '@git.zone/tsbundle': 2.8.3 @@ -5037,7 +5045,7 @@ snapshots: '@push.rocks/smartlog-destination-local': 9.0.2 '@push.rocks/smartshell': 3.3.0 '@push.rocks/smartwatch': 6.3.0 - '@push.rocks/taskbuffer': 3.5.0 + '@push.rocks/taskbuffer': 4.2.0 transitivePeerDependencies: - '@nuxt/kit' - '@swc/helpers' @@ -5070,7 +5078,7 @@ snapshots: '@inquirer/figures': 1.0.15 '@inquirer/type': 2.0.0 '@types/mute-stream': 0.0.4 - '@types/node': 22.19.8 + '@types/node': 22.19.9 '@types/wrap-ansi': 3.0.0 ansi-escapes: 4.3.2 cli-width: 4.1.0 @@ -6215,6 +6223,22 @@ snapshots: - supports-color - vue + '@push.rocks/taskbuffer@4.2.0': + dependencies: + '@design.estate/dees-element': 2.1.6 + '@push.rocks/lik': 6.2.2 + '@push.rocks/smartdelay': 3.0.5 + '@push.rocks/smartlog': 3.1.10 + '@push.rocks/smartpromise': 4.2.3 + '@push.rocks/smartrx': 3.0.10 + '@push.rocks/smarttime': 4.1.1 + '@push.rocks/smartunique': 3.0.9 + transitivePeerDependencies: + - '@nuxt/kit' + - react + - supports-color + - vue + '@push.rocks/webrequest@3.0.37': dependencies: '@push.rocks/smartdelay': 3.0.5 @@ -6942,27 +6966,27 @@ snapshots: '@types/bn.js@5.2.0': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.1 '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 25.2.0 + '@types/node': 25.2.1 '@types/buffer-json@2.0.3': {} '@types/clean-css@4.2.11': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.1 source-map: 0.6.1 '@types/connect@3.4.38': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.1 '@types/cors@2.8.19': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.1 '@types/debug@4.1.12': dependencies: @@ -6970,7 +6994,7 @@ snapshots: '@types/dns-packet@5.6.5': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.1 '@types/elliptic@6.4.18': dependencies: @@ -6978,7 +7002,7 @@ snapshots: '@types/express-serve-static-core@5.1.1': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.1 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -6991,19 +7015,19 @@ snapshots: '@types/from2@2.3.6': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.1 '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 - '@types/node': 25.2.0 + '@types/node': 25.2.1 '@types/geojson@7946.0.16': {} '@types/glob@8.1.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 25.2.0 + '@types/node': 25.2.1 '@types/hast@3.0.4': dependencies: @@ -7025,7 +7049,7 @@ snapshots: '@types/jsonfile@6.1.4': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.1 '@types/linkify-it@5.0.0': {} @@ -7048,17 +7072,17 @@ snapshots: '@types/mute-stream@0.0.4': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.1 '@types/node-forge@1.3.14': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.1 - '@types/node@22.19.8': + '@types/node@22.19.9': dependencies: undici-types: 6.21.0 - '@types/node@25.2.0': + '@types/node@25.2.1': dependencies: undici-types: 7.16.0 @@ -7076,12 +7100,12 @@ snapshots: '@types/send@1.2.1': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.1 '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 25.2.0 + '@types/node': 25.2.1 '@types/supercluster@7.1.3': dependencies: @@ -7091,11 +7115,11 @@ snapshots: '@types/tar-stream@3.1.4': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.1 '@types/through2@2.0.41': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.1 '@types/trusted-types@2.0.7': {} @@ -7121,11 +7145,11 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.1 '@types/yauzl@2.10.3': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.1 optional: true '@ungap/structured-clone@1.3.0': {} @@ -7559,7 +7583,7 @@ snapshots: engine.io@6.6.4: dependencies: '@types/cors': 2.8.19 - '@types/node': 25.2.0 + '@types/node': 25.2.1 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 diff --git a/readme.hints.md b/readme.hints.md index 2781dc9..92f1bb3 100644 --- a/readme.hints.md +++ b/readme.hints.md @@ -23,6 +23,9 @@ Geospatial web components library using MapLibre GL JS for map rendering and ter | `showSearch` | `boolean` | `false` | Show address search input | | `showNavigation` | `boolean` | `false` | Show A-to-B navigation panel | | `navigationMode` | `'driving' \| 'walking' \| 'cycling'` | `'driving'` | Transport mode for routing | +| `showTraffic` | `boolean` | `false` | Enable traffic layer visualization | +| `trafficApiKey` | `string` | `''` | HERE API key for traffic data | +| `trafficProvider` | `ITrafficProvider` | `null` | Custom traffic data provider | ### Drawing Tools (TDrawTool) - `point` - Draw points @@ -41,6 +44,7 @@ Geospatial web components library using MapLibre GL JS for map rendering and ter - `draw-finish` - Fired when a shape is completed - `address-selected` - Fired when a search result is selected - `route-calculated` - Fired when a navigation route is calculated (includes route, startPoint, endPoint, mode) +- `traffic-updated` - Fired when traffic data is refreshed ### Public Methods - `getFeatures()` - Get all drawn features @@ -57,11 +61,19 @@ Geospatial web components library using MapLibre GL JS for map rendering and ter - `setNavigationStart(coords, address?)` - Set navigation start point - `setNavigationEnd(coords, address?)` - Set navigation end point - `clearNavigation()` - Clear all navigation state +- `enableTraffic()` - Enable traffic visualization +- `disableTraffic()` - Disable traffic visualization +- `toggleTraffic()` - Toggle traffic visualization +- `refreshTraffic()` - Refresh traffic data +- `setTrafficProvider(provider)` - Set custom traffic provider +- `supportsTrafficRouting()` - Check if traffic-aware routing is available +- `getTrafficController()` - Get the TrafficController instance ### Context Menu Right-click on the map to access a context menu with the following options: - **Drag to Draw** - Toggle between drag mode (click-drag for circles/rectangles) and two-click mode - **Globe View** - Toggle between globe (3D sphere) and Mercator (flat) projection +- **Show Traffic** - Toggle traffic layer (requires configured traffic provider) - **Clear All Features** - Remove all drawn features from the map - **Fit to Features** - Zoom and pan to show all drawn features @@ -71,6 +83,45 @@ The navigation panel (`showNavigation={true}`) provides A-to-B routing using OSR - **Point selection**: Type an address or click on the map - **Route display**: Blue line overlay with turn-by-turn directions - **API**: Uses free OSRM API (https://router.project-osrm.org) with fair-use rate limit +- **Traffic-aware routing**: When a traffic provider is configured, shows duration with/without traffic + +### Traffic Feature +Live traffic visualization with pluggable provider architecture: + +#### HERE Traffic Provider (Recommended) +```typescript +// Using API key property + + +// Or programmatically +import { HereTrafficProvider } from '@design.estate/dees-catalog-geo'; +const map = document.querySelector('dees-geo-map'); +const provider = new HereTrafficProvider(); +provider.configure({ apiKey: 'YOUR_HERE_API_KEY' }); +map.setTrafficProvider(provider); +map.enableTraffic(); +``` + +**Free Tier**: 250,000 transactions/month (no credit card required) +Sign up at: https://developer.here.com + +#### Valhalla Traffic Provider (Self-Hosted) +```typescript +import { ValhallaTrafficProvider } from '@design.estate/dees-catalog-geo'; +const provider = new ValhallaTrafficProvider(); +provider.configure({ + serverUrl: 'https://your-valhalla-server.com', + trafficDataUrl: 'https://your-traffic-data-endpoint.com' // optional +}); +map.setTrafficProvider(provider); +``` + +#### Traffic Color Legend +- 🟢 Green - Free flow +- 🟡 Yellow - Light congestion +- 🟠 Orange - Moderate congestion +- 🔴 Red - Heavy congestion +- 🔴 Dark Red - Severe/stopped ## Development - `pnpm install` - Install dependencies @@ -90,11 +141,13 @@ ts_web/ ├── index.ts └── dees-geo-map/ ├── index.ts # Exports main + modules - ├── dees-geo-map.ts # Main component (~550 lines) + ├── dees-geo-map.ts # Main component ├── dees-geo-map.demo.ts # Demo function - ├── geo-map.icons.ts # Icon SVG definitions (~60 lines) - ├── geo-map.search.ts # SearchController class (~180 lines) - └── geo-map.navigation.ts # NavigationController class (~530 lines) + ├── geo-map.icons.ts # Icon SVG definitions + ├── geo-map.search.ts # SearchController class + ├── geo-map.navigation.ts # NavigationController class + ├── geo-map.traffic.ts # TrafficController class + └── geo-map.traffic.providers.ts # Traffic provider implementations ``` ## Modular Architecture @@ -117,6 +170,18 @@ Contains all SVG icon definitions as a `GEO_MAP_ICONS` record and a `renderIcon( - Map click mode for point selection - Turn-by-turn directions rendering - Route overlay on map +- Traffic-aware routing integration (shows congestion level and delay) + +### geo-map.traffic.ts +`TrafficController` class for live traffic visualization: +- Traffic layer rendering with color-coded congestion +- Auto-refresh logic with configurable interval +- Supports pluggable traffic providers + +### geo-map.traffic.providers.ts +Traffic data provider implementations: +- `HereTrafficProvider` - HERE Traffic API v7 (freemium) +- `ValhallaTrafficProvider` - Self-hosted Valhalla server ### Usage of Controllers ```typescript @@ -142,6 +207,37 @@ const nav = new NavigationController({ - Terra-draw requires the separate maplibre-gl-adapter package - The component uses Shadow DOM for style encapsulation +### UI Layout + +The component uses a header toolbar above the map for a cleaner layout: + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ HEADER TOOLBAR │ +│ [Draw Tools] | [Search Bar] | [Nav Toggle] [Traffic] [Zoom +/-] │ +├──────────────────────────────────────────────────────────────────────┤ +│ │ +│ [Navigation] MAP │ +│ (toggleable) │ +│ │ +│ [Traffic Legend] [Feature Count] │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +**Header Toolbar Sections:** +- **Left**: Draw tools (Point, Line, Polygon, Rectangle, Circle, Freehand, Select, Clear) +- **Center**: Search bar (expandable width) +- **Right**: Navigation toggle, Traffic toggle, Zoom in/out buttons + +**Map Overlays:** +- Navigation panel: Toggleable overlay on top-left of map +- Traffic legend: Bottom-left overlay (when traffic enabled) +- Feature count: Bottom-left overlay (when features exist) + +**Z-index hierarchy:** +- `z-index: 20` - Dropdowns (search results, nav search results) +- `z-index: 5` - Map overlays (navigation panel, traffic legend, feature count) + ### Shadow DOM & Terra-Draw Drawing Fix Terra-draw's event listeners normally intercept map events through MapLibre's canvas element. In Shadow DOM contexts, these events are scoped locally and don't propagate correctly, causing terra-draw handlers to fail while MapLibre's drag handlers continue working. diff --git a/search-results-test.png b/search-results-test.png new file mode 100644 index 0000000..bea3a64 Binary files /dev/null and b/search-results-test.png differ diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 1a80d10..c2ed6ed 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -1,9 +1,8 @@ /** - * This file is auto-generated by git.zone build tools. - * Do not modify manually. + * autocreated commitinfo by @push.rocks/commitinfo */ export const commitinfo = { name: '@design.estate/dees-catalog-geo', - version: '1.0.0', - description: 'A geospatial web components library with MapLibre GL JS maps and terra-draw drawing tools', -}; + version: '1.1.0', + description: 'A geospatial web components library with MapLibre GL JS maps and terra-draw drawing tools' +} diff --git a/ts_web/elements/00componentstyles.ts b/ts_web/elements/00componentstyles.ts index 824afde..b67c0d7 100644 --- a/ts_web/elements/00componentstyles.ts +++ b/ts_web/elements/00componentstyles.ts @@ -25,6 +25,9 @@ export const geoComponentStyles = css` */ export const mapContainerStyles = css` .map-container { + --geo-overlay-inset: 12px; + --geo-overlay-gap: 8px; + position: relative; width: 100%; height: 100%; @@ -37,6 +40,58 @@ export const mapContainerStyles = css` position: absolute; inset: 0; } + + /* Overlay grid for UI elements */ + .map-overlay { + position: absolute; + inset: var(--geo-overlay-inset); + pointer-events: none; + z-index: 5; + + display: grid; + grid-template-columns: auto 1fr auto; + grid-template-rows: auto 1fr auto; + grid-template-areas: + "top-left . top-right" + ". . ." + "bottom-left . bottom-right"; + gap: var(--geo-overlay-gap); + } + + .map-overlay > * { + pointer-events: auto; + } + + .overlay-top-left { + grid-area: top-left; + display: flex; + align-items: flex-start; + gap: var(--geo-overlay-gap); + } + + .overlay-top-right { + grid-area: top-right; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: var(--geo-overlay-gap); + } + + .overlay-bottom-left { + grid-area: bottom-left; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--geo-overlay-gap); + } + + .overlay-bottom-right { + grid-area: bottom-right; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: var(--geo-overlay-gap); + } `; /** @@ -44,10 +99,6 @@ export const mapContainerStyles = css` */ export const toolbarStyles = css` .toolbar { - position: absolute; - top: 12px; - left: 12px; - z-index: 10; display: flex; flex-direction: column; gap: 4px; @@ -57,6 +108,7 @@ export const toolbarStyles = css` border-radius: 8px; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); + align-self: flex-start; } .toolbar-group { @@ -111,15 +163,11 @@ export const toolbarStyles = css` */ export const searchStyles = css` .search-container { - position: absolute; - top: 12px; - right: 12px; - z-index: 10; width: 280px; + position: relative; } .search-input-wrapper { - position: relative; display: flex; align-items: center; background: var(--geo-toolbar-bg, rgba(30, 30, 30, 0.9)); @@ -127,7 +175,6 @@ export const searchStyles = css` border-radius: 8px; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); - overflow: hidden; } .search-input-wrapper:focus-within { @@ -224,6 +271,7 @@ export const searchStyles = css` border-radius: 8px; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); + z-index: 20; } .search-results:empty { @@ -274,10 +322,6 @@ export const searchStyles = css` */ export const navigationStyles = css` .navigation-panel { - position: absolute; - top: 12px; - left: 60px; - z-index: 10; width: 300px; background: var(--geo-toolbar-bg, rgba(30, 30, 30, 0.95)); border: 1px solid var(--geo-toolbar-border, rgba(255, 255, 255, 0.1)); @@ -285,6 +329,7 @@ export const navigationStyles = css` backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); overflow: hidden; + align-self: flex-start; } .nav-header { @@ -484,7 +529,7 @@ export const navigationStyles = css` background: var(--geo-toolbar-bg, rgba(30, 30, 30, 0.98)); border: 1px solid var(--geo-toolbar-border, rgba(255, 255, 255, 0.1)); border-radius: 6px; - z-index: 100; + z-index: 20; } .nav-search-result { @@ -686,4 +731,330 @@ export const navigationStyles = css` color: rgba(255, 255, 255, 0.5); font-size: 12px; } + + /* Traffic-aware route info */ + .nav-traffic-info { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: rgba(255, 152, 0, 0.1); + border-radius: 6px; + margin: 8px 12px; + } + + .nav-traffic-info.low { + background: rgba(0, 200, 83, 0.1); + } + + .nav-traffic-info.moderate { + background: rgba(255, 235, 59, 0.15); + } + + .nav-traffic-info.heavy { + background: rgba(255, 152, 0, 0.15); + } + + .nav-traffic-info.severe { + background: rgba(244, 67, 54, 0.15); + } + + .nav-traffic-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + } + + .nav-traffic-indicator.low { + background: #00c853; + } + + .nav-traffic-indicator.moderate { + background: #ffeb3b; + } + + .nav-traffic-indicator.heavy { + background: #ff9800; + } + + .nav-traffic-indicator.severe { + background: #f44336; + } + + .nav-traffic-text { + font-size: 12px; + color: rgba(255, 255, 255, 0.8); + } + + .nav-traffic-delay { + font-size: 11px; + color: rgba(255, 255, 255, 0.5); + margin-left: auto; + } +`; + +/** + * Traffic control styles + */ +/** + * Header toolbar styles for toolbar above map + */ +export const headerToolbarStyles = css` + .geo-component { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + } + + .header-toolbar { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 12px; + background: var(--geo-toolbar-bg, rgba(30, 30, 30, 0.95)); + border-bottom: 1px solid var(--geo-toolbar-border, rgba(255, 255, 255, 0.1)); + flex-shrink: 0; + min-height: 52px; + position: relative; + z-index: 10; + } + + .toolbar-left { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; + } + + .toolbar-center { + flex: 1; + display: flex; + justify-content: center; + min-width: 200px; + } + + .toolbar-right { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + } + + .header-toolbar .toolbar-divider { + width: 1px; + height: 24px; + background: rgba(255, 255, 255, 0.15); + margin: 0 4px; + } + + /* Header toolbar button styles */ + .header-toolbar .tool-button { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + padding: 0; + border: none; + border-radius: 6px; + background: transparent; + color: var(--geo-text, #fff); + cursor: pointer; + transition: background-color 0.15s ease, color 0.15s ease; + } + + .header-toolbar .tool-button:hover { + background: var(--geo-tool-hover, rgba(255, 255, 255, 0.1)); + } + + .header-toolbar .tool-button.active { + background: var(--geo-tool-active, #0084ff); + color: #fff; + } + + .header-toolbar .tool-button:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .header-toolbar .tool-button svg { + width: 18px; + height: 18px; + } + + /* Search container in header */ + .header-toolbar .search-container { + width: 100%; + max-width: 300px; + } + + /* Map container takes remaining space */ + .geo-component .map-container { + flex: 1; + position: relative; + min-height: 0; + } +`; + +/** + * Traffic control styles + */ +export const trafficStyles = css` + .traffic-control { + display: flex; + align-items: center; + gap: 8px; + } + + .traffic-toggle-btn { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + padding: 0; + border: none; + border-radius: 8px; + background: var(--geo-toolbar-bg, rgba(30, 30, 30, 0.9)); + border: 1px solid var(--geo-toolbar-border, rgba(255, 255, 255, 0.1)); + color: var(--geo-text, #fff); + cursor: pointer; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease; + } + + .traffic-toggle-btn:hover:not(:disabled) { + background: var(--geo-tool-hover, rgba(255, 255, 255, 0.15)); + } + + .traffic-toggle-btn.active { + background: var(--geo-tool-active, #0084ff); + border-color: var(--geo-tool-active, #0084ff); + color: #fff; + } + + .traffic-toggle-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .traffic-toggle-btn svg { + width: 20px; + height: 20px; + } + + .traffic-loading-indicator { + position: absolute; + bottom: 2px; + right: 2px; + display: flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + background: var(--geo-toolbar-bg, rgba(30, 30, 30, 0.9)); + border-radius: 50%; + } + + .traffic-loading-indicator svg { + width: 10px; + height: 10px; + animation: spin 1s linear infinite; + } + + .traffic-status { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + background: var(--geo-toolbar-bg, rgba(30, 30, 30, 0.9)); + border: 1px solid var(--geo-toolbar-border, rgba(255, 255, 255, 0.1)); + border-radius: 6px; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + } + + .traffic-status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: #00c853; + animation: pulse 2s ease-in-out infinite; + } + + @keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } + } + + .traffic-status-text { + font-size: 11px; + color: rgba(255, 255, 255, 0.7); + } + + .traffic-error { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + color: #f44336; + cursor: help; + } + + .traffic-error svg { + width: 16px; + height: 16px; + } + + /* Traffic Legend */ + .traffic-legend { + padding: 10px 12px; + background: var(--geo-toolbar-bg, rgba(30, 30, 30, 0.9)); + border: 1px solid var(--geo-toolbar-border, rgba(255, 255, 255, 0.1)); + border-radius: 8px; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + } + + .traffic-legend-title { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(255, 255, 255, 0.5); + margin-bottom: 8px; + } + + .traffic-legend-items { + display: flex; + flex-direction: column; + gap: 4px; + } + + .traffic-legend-item { + display: flex; + align-items: center; + gap: 8px; + font-size: 11px; + color: rgba(255, 255, 255, 0.8); + } + + .traffic-legend-color { + width: 16px; + height: 4px; + border-radius: 2px; + } + + .traffic-legend-updated { + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid rgba(255, 255, 255, 0.1); + font-size: 10px; + color: rgba(255, 255, 255, 0.4); + } `; diff --git a/ts_web/elements/00group-map/dees-geo-map/dees-geo-map.demo.ts b/ts_web/elements/00group-map/dees-geo-map/dees-geo-map.demo.ts index 4704d4d..6c2a712 100644 --- a/ts_web/elements/00group-map/dees-geo-map/dees-geo-map.demo.ts +++ b/ts_web/elements/00group-map/dees-geo-map/dees-geo-map.demo.ts @@ -274,6 +274,11 @@ export const demoFunc = () => html` .showNavigation=${true} > + +

+ Traffic: To enable live traffic, set the trafficApiKey property with your HERE API key + (free tier: 250k requests/month at developer.here.com). +

diff --git a/ts_web/elements/00group-map/dees-geo-map/dees-geo-map.ts b/ts_web/elements/00group-map/dees-geo-map/dees-geo-map.ts index d034bcf..7692430 100644 --- a/ts_web/elements/00group-map/dees-geo-map/dees-geo-map.ts +++ b/ts_web/elements/00group-map/dees-geo-map/dees-geo-map.ts @@ -10,7 +10,7 @@ import { css, } from '@design.estate/dees-element'; import { DeesContextmenu } from '@design.estate/dees-catalog'; -import { geoComponentStyles, mapContainerStyles, toolbarStyles, searchStyles, navigationStyles } from '../../00componentstyles.js'; +import { geoComponentStyles, mapContainerStyles, toolbarStyles, searchStyles, navigationStyles, trafficStyles, headerToolbarStyles } from '../../00componentstyles.js'; // MapLibre imports import maplibregl from 'maplibre-gl'; @@ -33,6 +33,8 @@ import { TerraDrawMapLibreGLAdapter } from 'terra-draw-maplibre-gl-adapter'; import { renderIcon } from './geo-map.icons.js'; import { SearchController, type INominatimResult, type IAddressSelectedEvent } from './geo-map.search.js'; import { NavigationController, type TNavigationMode, type INavigationState, type IRouteCalculatedEvent } from './geo-map.navigation.js'; +import { TrafficController } from './geo-map.traffic.js'; +import { HereTrafficProvider, type ITrafficProvider } from './geo-map.traffic.providers.js'; // Re-export types for external consumers export type { INominatimResult, IAddressSelectedEvent } from './geo-map.search.js'; @@ -44,6 +46,7 @@ export type { IOSRMStep, IRouteCalculatedEvent, } from './geo-map.navigation.js'; +export type { ITrafficProvider, ITrafficFlowData, ITrafficAwareRoute } from './geo-map.traffic.providers.js'; export type TDrawTool = 'polygon' | 'rectangle' | 'point' | 'linestring' | 'circle' | 'freehand' | 'select' | 'static'; @@ -114,6 +117,16 @@ export class DeesGeoMap extends DeesElement { @property({ type: String }) accessor navigationMode: TNavigationMode = 'driving'; + // Traffic properties + @property({ type: Boolean }) + accessor showTraffic: boolean = false; + + @property({ type: Object }) + accessor trafficProvider: ITrafficProvider | null = null; + + @property({ type: String }) + accessor trafficApiKey: string = ''; + // ─── State ────────────────────────────────────────────────────────────────── @state() @@ -125,9 +138,13 @@ export class DeesGeoMap extends DeesElement { @state() private accessor isMapReady: boolean = false; + @state() + private accessor isNavigationOpen: boolean = true; + // Controllers private searchController: SearchController | null = null; private navigationController: NavigationController | null = null; + private trafficController: TrafficController | null = null; // ─── Styles ───────────────────────────────────────────────────────────────── @@ -138,6 +155,8 @@ export class DeesGeoMap extends DeesElement { toolbarStyles, searchStyles, navigationStyles, + trafficStyles, + headerToolbarStyles, css` :host { display: block; @@ -163,51 +182,7 @@ export class DeesGeoMap extends DeesElement { color: rgba(255, 255, 255, 0.8); } - .toolbar { - user-select: none; - } - - .toolbar-title { - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - color: rgba(255, 255, 255, 0.5); - padding: 0 4px 4px; - } - - .tool-button { - position: relative; - } - - .tool-button::after { - content: attr(title); - position: absolute; - left: calc(100% + 8px); - top: 50%; - transform: translateY(-50%); - padding: 4px 8px; - background: rgba(0, 0, 0, 0.8); - color: #fff; - font-size: 12px; - white-space: nowrap; - border-radius: 4px; - opacity: 0; - visibility: hidden; - pointer-events: none; - transition: opacity 0.15s ease, visibility 0.15s ease; - } - - .tool-button:hover::after { - opacity: 1; - visibility: visible; - } - .feature-count { - position: absolute; - bottom: 12px; - left: 12px; - z-index: 10; padding: 6px 12px; background: rgba(30, 30, 30, 0.9); border: 1px solid rgba(255, 255, 255, 0.1); @@ -217,22 +192,6 @@ export class DeesGeoMap extends DeesElement { font-size: 12px; color: rgba(255, 255, 255, 0.7); } - - .zoom-controls { - position: absolute; - bottom: 12px; - right: 12px; - z-index: 10; - display: flex; - flex-direction: column; - gap: 4px; - padding: 4px; - background: rgba(30, 30, 30, 0.9); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 8px; - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - } `, ]; @@ -269,6 +228,23 @@ export class DeesGeoMap extends DeesElement { if (changedProperties.has('navigationMode') && this.navigationController) { this.navigationController.navigationMode = this.navigationMode; } + // Traffic property changes + if (changedProperties.has('showTraffic') && this.trafficController) { + if (this.showTraffic) { + this.trafficController.enable(); + } else { + this.trafficController.disable(); + } + } + if (changedProperties.has('trafficProvider') && this.trafficController && this.trafficProvider) { + this.trafficController.setProvider(this.trafficProvider); + } + if (changedProperties.has('trafficApiKey') && this.trafficController && this.trafficApiKey) { + // Auto-configure HERE provider if API key is provided + const hereProvider = new HereTrafficProvider(); + hereProvider.configure({ apiKey: this.trafficApiKey }); + this.trafficController.setProvider(hereProvider); + } } // ─── Controller Initialization ────────────────────────────────────────────── @@ -312,8 +288,30 @@ export class DeesGeoMap extends DeesElement { }, onRequestUpdate: () => this.requestUpdate(), getMap: () => this.map, + // Connect traffic controller for traffic-aware routing + getTrafficRoute: async (start, end, mode) => { + if (this.trafficController?.supportsTrafficRouting()) { + return this.trafficController.fetchRouteWithTraffic(start, end, mode); + } + return null; + }, }); this.navigationController.navigationMode = this.navigationMode; + + // Initialize traffic controller + this.trafficController = new TrafficController({ + onRequestUpdate: () => this.requestUpdate(), + getMap: () => this.map, + }); + + // Configure traffic provider if API key or provider is set + if (this.trafficProvider) { + this.trafficController.setProvider(this.trafficProvider); + } else if (this.trafficApiKey) { + const hereProvider = new HereTrafficProvider(); + hereProvider.configure({ apiKey: this.trafficApiKey }); + this.trafficController.setProvider(hereProvider); + } } // ─── Map Initialization ───────────────────────────────────────────────────── @@ -342,6 +340,12 @@ export class DeesGeoMap extends DeesElement { this.map!.setProjection({ type: this.projection }); this.initializeTerraDraw(); + + // Enable traffic if configured + if (this.showTraffic && this.trafficController) { + this.trafficController.enable(); + } + this.dispatchEvent(new CustomEvent('map-ready', { detail: { map: this.map } })); }); @@ -353,6 +357,11 @@ export class DeesGeoMap extends DeesElement { zoom: this.map?.getZoom(), }, })); + + // Refresh traffic data when map moves + if (this.trafficController) { + this.trafficController.handleMapMoveEnd(); + } }); // Handle clicks for navigation point selection @@ -672,6 +681,61 @@ export class DeesGeoMap extends DeesElement { return this.navigationController?.navigationState ?? null; } + // ─── Traffic Public Methods ──────────────────────────────────────────────── + + /** + * Enable traffic visualization + */ + public enableTraffic(): void { + this.showTraffic = true; + this.trafficController?.enable(); + } + + /** + * Disable traffic visualization + */ + public disableTraffic(): void { + this.showTraffic = false; + this.trafficController?.disable(); + } + + /** + * Toggle traffic visualization + */ + public toggleTraffic(): void { + this.showTraffic = !this.showTraffic; + this.trafficController?.toggle(); + } + + /** + * Refresh traffic data + */ + public async refreshTraffic(): Promise { + await this.trafficController?.refresh(); + } + + /** + * Set traffic provider + */ + public setTrafficProvider(provider: ITrafficProvider): void { + this.trafficProvider = provider; + this.trafficController?.setProvider(provider); + } + + /** + * Check if traffic-aware routing is available + */ + public supportsTrafficRouting(): boolean { + return this.trafficController?.supportsTrafficRouting() ?? false; + } + + /** + * Get traffic controller for advanced usage + */ + public getTrafficController(): TrafficController | null { + return this.trafficController; + } + // ─── Private Methods ──────────────────────────────────────────────────────── private ensureMaplibreCssLoaded() { @@ -690,8 +754,9 @@ export class DeesGeoMap extends DeesElement { this.draw.stop(); this.draw = null; } - // Clean up navigation controller + // Clean up controllers this.navigationController?.cleanup(); + this.trafficController?.cleanup(); if (this.map) { this.map.remove(); @@ -723,6 +788,8 @@ export class DeesGeoMap extends DeesElement { private handleMapContextMenu(e: MouseEvent) { e.preventDefault(); + const hasTrafficProvider = this.trafficController?.provider?.isConfigured ?? false; + DeesContextmenu.openContextMenuWithOptions(e, [ { name: this.dragToDraw ? '✓ Drag to Draw' : 'Drag to Draw', @@ -739,6 +806,19 @@ export class DeesGeoMap extends DeesElement { }, }, { divider: true }, + { + name: this.showTraffic ? '✓ Show Traffic' : 'Show Traffic', + iconName: 'lucide:traffic-cone', + action: async () => { + if (hasTrafficProvider) { + this.toggleTraffic(); + } else { + console.warn('[dees-geo-map] No traffic provider configured. Set trafficApiKey or trafficProvider property.'); + } + }, + disabled: !hasTrafficProvider, + }, + { divider: true }, { name: 'Clear All Features', iconName: 'lucide:trash2', @@ -752,26 +832,88 @@ export class DeesGeoMap extends DeesElement { ]); } + private toggleNavigation(): void { + this.isNavigationOpen = !this.isNavigationOpen; + } + // ─── Render ───────────────────────────────────────────────────────────────── public render(): TemplateResult { const featureCount = this.draw?.getSnapshot().length || 0; + const hasTrafficProvider = this.trafficController?.provider?.isConfigured ?? false; + const showTrafficControls = Boolean(hasTrafficProvider || this.trafficApiKey || this.trafficProvider); return html` -
this.handleMapContextMenu(e)}> -
+
+ + ${this.renderHeaderToolbar(showTrafficControls)} - ${this.showToolbar ? this.renderToolbar() : ''} - ${this.showSearch && this.searchController ? this.searchController.render() : ''} - ${this.showNavigation && this.navigationController ? this.navigationController.render() : ''} + +
this.handleMapContextMenu(e)}> +
- ${featureCount > 0 ? html` -
- ${featureCount} feature${featureCount !== 1 ? 's' : ''} +
+ +
+ ${this.showNavigation && this.isNavigationOpen && this.navigationController + ? this.navigationController.render() + : ''} +
+ + +
+ + +
+ ${this.showTraffic && this.trafficController ? this.trafficController.renderLegend() : ''} + ${featureCount > 0 ? html` +
+ ${featureCount} feature${featureCount !== 1 ? 's' : ''} +
+ ` : ''} +
+ + +
- ` : ''} +
+
+ `; + } -
+ private renderHeaderToolbar(showTrafficControls: boolean): TemplateResult { + return html` +
+ +
+ ${this.showToolbar ? html` + ${this.renderDrawTools()} +
+ ` : ''} +
+ + +
+ ${this.showSearch && this.searchController + ? this.searchController.render() + : ''} +
+ + +
+ ${this.showNavigation ? html` + + ` : ''} + ${showTrafficControls && this.trafficController + ? this.trafficController.render() + : ''} +
- `)} -
- -
-
Edit
- -
- - -
-
+ ${tools.map(tool => html` + + `)} +
+ + `; } -} + + } diff --git a/ts_web/elements/00group-map/dees-geo-map/geo-map.icons.ts b/ts_web/elements/00group-map/dees-geo-map/geo-map.icons.ts index 9ab71fd..48e25fe 100644 --- a/ts_web/elements/00group-map/dees-geo-map/geo-map.icons.ts +++ b/ts_web/elements/00group-map/dees-geo-map/geo-map.icons.ts @@ -36,6 +36,11 @@ export const GEO_MAP_ICONS: Record = { clock: html``, ruler: html``, error: html``, + + // Traffic + traffic: html``, + trafficLight: html``, + congestion: html``, }; /** diff --git a/ts_web/elements/00group-map/dees-geo-map/geo-map.navigation.ts b/ts_web/elements/00group-map/dees-geo-map/geo-map.navigation.ts index 15b60c9..6785214 100644 --- a/ts_web/elements/00group-map/dees-geo-map/geo-map.navigation.ts +++ b/ts_web/elements/00group-map/dees-geo-map/geo-map.navigation.ts @@ -2,6 +2,7 @@ import { html, type TemplateResult } from '@design.estate/dees-element'; import maplibregl from 'maplibre-gl'; import { renderIcon } from './geo-map.icons.js'; import { type INominatimResult } from './geo-map.search.js'; +import type { ITrafficAwareRoute } from './geo-map.traffic.providers.js'; // ─── Navigation/Routing Types ──────────────────────────────────────────────── @@ -39,6 +40,7 @@ export interface INavigationState { startAddress: string; endAddress: string; route: IOSRMRoute | null; + trafficRoute: ITrafficAwareRoute | null; isLoading: boolean; error: string | null; } @@ -57,6 +59,12 @@ export interface INavigationControllerCallbacks { onRouteCalculated: (event: IRouteCalculatedEvent) => void; onRequestUpdate: () => void; getMap: () => maplibregl.Map | null; + /** Optional callback to fetch traffic-aware route */ + getTrafficRoute?: ( + start: [number, number], + end: [number, number], + mode: TNavigationMode + ) => Promise; } /** @@ -71,6 +79,7 @@ export class NavigationController { startAddress: '', endAddress: '', route: null, + trafficRoute: null, isLoading: false, error: null, }; @@ -154,20 +163,30 @@ export class NavigationController { ...this.navigationState, isLoading: true, error: null, + trafficRoute: null, }; this.callbacks.onRequestUpdate(); try { - const route = await this.fetchRoute(startPoint, endPoint, this.navigationMode); + // Fetch both regular route and traffic-aware route in parallel + const [route, trafficRoute] = await Promise.all([ + this.fetchRoute(startPoint, endPoint, this.navigationMode), + this.callbacks.getTrafficRoute + ? this.callbacks.getTrafficRoute(startPoint, endPoint, this.navigationMode) + : Promise.resolve(null), + ]); if (route) { this.navigationState = { ...this.navigationState, route, + trafficRoute, isLoading: false, }; - this.renderRouteOnMap(route); + // Use traffic route geometry if available, otherwise use regular route + const routeToRender = trafficRoute || route; + this.renderRouteOnMap(routeToRender); // Dispatch route-calculated event this.callbacks.onRouteCalculated({ @@ -178,7 +197,7 @@ export class NavigationController { }); // Fit map to route bounds - this.fitToRoute(route); + this.fitToRoute(routeToRender); } } catch (error) { this.navigationState = { @@ -244,6 +263,7 @@ export class NavigationController { startAddress: '', endAddress: '', route: null, + trafficRoute: null, isLoading: false, error: null, }; @@ -679,6 +699,19 @@ export class NavigationController { return icons[key] || icons[type] || '➡'; } + /** + * Get congestion label for display + */ + public getCongestionLabel(level: 'low' | 'moderate' | 'heavy' | 'severe'): string { + const labels = { + low: 'Light traffic', + moderate: 'Moderate traffic', + heavy: 'Heavy traffic', + severe: 'Severe congestion', + }; + return labels[level]; + } + /** * Format step instruction for display */ @@ -754,13 +787,14 @@ export class NavigationController { /** * Render the navigation panel + * @param extraClass - Optional CSS class to add to the panel for positioning */ - public render(): TemplateResult { + public render(extraClass?: string): TemplateResult { const { route, isLoading, error, startPoint, endPoint } = this.navigationState; const canCalculate = startPoint && endPoint && !isLoading; return html` -