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`
+