@de./sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +4391 -0
- package/dist/allend/Access.d.ts +15 -0
- package/dist/allend/Access.js +164 -0
- package/dist/allend/Access.js.map +1 -0
- package/dist/allend/DAuth/index.d.ts +20 -0
- package/dist/allend/DAuth/index.js +92 -0
- package/dist/allend/DAuth/index.js.map +1 -0
- package/dist/allend/DClient/Client.d.ts +10 -0
- package/dist/allend/DClient/Client.js +144 -0
- package/dist/allend/DClient/Client.js.map +1 -0
- package/dist/allend/DClient/Event.d.ts +18 -0
- package/dist/allend/DClient/Event.js +100 -0
- package/dist/allend/DClient/Event.js.map +1 -0
- package/dist/allend/DClient/Order.d.ts +49 -0
- package/dist/allend/DClient/Order.js +601 -0
- package/dist/allend/DClient/Order.js.map +1 -0
- package/dist/allend/IoTClient.d.ts +29 -0
- package/dist/allend/IoTClient.js +59 -0
- package/dist/allend/IoTClient.js.map +1 -0
- package/dist/allend/MSI/Controls.d.ts +256 -0
- package/dist/allend/MSI/Controls.js +763 -0
- package/dist/allend/MSI/Controls.js.map +1 -0
- package/dist/allend/MSI/Handles.d.ts +88 -0
- package/dist/allend/MSI/Handles.js +445 -0
- package/dist/allend/MSI/Handles.js.map +1 -0
- package/dist/allend/MSI/Plugins.d.ts +22 -0
- package/dist/allend/MSI/Plugins.js +38 -0
- package/dist/allend/MSI/Plugins.js.map +1 -0
- package/dist/allend/MSI/index.d.ts +55 -0
- package/dist/allend/MSI/index.js +155 -0
- package/dist/allend/MSI/index.js.map +1 -0
- package/dist/allend/index.d.ts +12 -0
- package/dist/allend/index.js +18 -0
- package/dist/allend/index.js.map +1 -0
- package/dist/backend/Auth.d.ts +38 -0
- package/dist/backend/Auth.js +286 -0
- package/dist/backend/Auth.js.map +1 -0
- package/dist/baseUrl.d.ts +12 -0
- package/dist/baseUrl.js +16 -0
- package/dist/baseUrl.js.map +1 -0
- package/dist/bundle.js +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/types/access.d.ts +8 -0
- package/dist/types/auth.d.ts +27 -0
- package/dist/types/index.d.ts +281 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/stream.d.ts +20 -0
- package/dist/utils/stream.js +63 -0
- package/dist/utils/stream.js.map +1 -0
- package/package.json +66 -0
- package/src/allend/Access.ts +78 -0
- package/src/allend/DAuth/index.ts +39 -0
- package/src/allend/DClient/Client.ts +76 -0
- package/src/allend/DClient/Event.ts +76 -0
- package/src/allend/DClient/Order.ts +452 -0
- package/src/allend/IoTClient.ts +68 -0
- package/src/allend/MSI/Controls.ts +760 -0
- package/src/allend/MSI/Handles.ts +387 -0
- package/src/allend/MSI/Plugins.ts +52 -0
- package/src/allend/MSI/index.ts +160 -0
- package/src/allend/index.ts +18 -0
- package/src/backend/Auth.ts +201 -0
- package/src/baseUrl.ts +13 -0
- package/src/index.ts +19 -0
- package/src/types/access.d.ts +8 -0
- package/src/types/auth.d.ts +27 -0
- package/src/types/index.d.ts +281 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/stream.ts +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,4391 @@
|
|
|
1
|
+
# De. Platform Developer Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
De. (Dedot) is a supply chain coordination platform that connects all the moving pieces of delivery—logistics companies, commerce platforms, developers, and end users—through one unified system. This documentation covers the complete SDK for integrating De.'s mapping, routing, and logistics coordination capabilities into your applications.
|
|
6
|
+
|
|
7
|
+
**Think of De. as the operating system for logistics** - similar to how Stripe revolutionized payments or how electrical wiring connects all appliances in a house, De. provides the invisible backbone that makes delivery and logistics work seamlessly.
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
1. [Getting Started](#getting-started)
|
|
12
|
+
2. [Architecture Overview](#architecture-overview)
|
|
13
|
+
3. [Installation & Setup](#installation--setup)
|
|
14
|
+
4. [Core Components](#core-components)
|
|
15
|
+
5. [API Reference](#api-reference)
|
|
16
|
+
6. [Code Examples](#code-examples)
|
|
17
|
+
7. [Plugin Development](#plugin-development)
|
|
18
|
+
8. [Advanced Usage Patterns](#advanced-usage-patterns)
|
|
19
|
+
9. [Error Handling](#error-handling)
|
|
20
|
+
10. [Performance Optimization](#performance-optimization)
|
|
21
|
+
11. [Testing](#testing)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Getting Started
|
|
26
|
+
|
|
27
|
+
The De. SDK provides three main modules:
|
|
28
|
+
|
|
29
|
+
- **MSI (Map System Interface)**: Complete map integration with iframe-based communication for visual delivery tracking
|
|
30
|
+
- **DClient**: Client-side operations for orders, events, and customer management
|
|
31
|
+
- **Access**: API authentication and HTTP request management
|
|
32
|
+
|
|
33
|
+
### Who Uses De.?
|
|
34
|
+
|
|
35
|
+
**Logistics Service Providers (LSP)**
|
|
36
|
+
- Carriers, 3PLs, 4PLs, Forwarders, Brokers
|
|
37
|
+
- Get better coordination tools, more business opportunities
|
|
38
|
+
- Real-time fleet tracking and route optimization
|
|
39
|
+
|
|
40
|
+
**Commerce Service Providers (CSP)**
|
|
41
|
+
- Retailers, Manufacturers, E-commerce platforms
|
|
42
|
+
- Seamless delivery integration for customers
|
|
43
|
+
- Multi-carrier coordination without building from scratch
|
|
44
|
+
|
|
45
|
+
**Developers (DEV)**
|
|
46
|
+
- Build delivery features into applications
|
|
47
|
+
- Access comprehensive logistics network
|
|
48
|
+
- Extensible plugin system for custom functionality
|
|
49
|
+
|
|
50
|
+
**End User Service (EUS)**
|
|
51
|
+
- Customers tracking their deliveries
|
|
52
|
+
- Real-time visibility into package location
|
|
53
|
+
- Delivery preference management
|
|
54
|
+
|
|
55
|
+
### Prerequisites
|
|
56
|
+
|
|
57
|
+
- Node.js 14+ or modern browser environment
|
|
58
|
+
- TypeScript 4.0+ (recommended)
|
|
59
|
+
- Valid De. workspace and access token
|
|
60
|
+
- Basic understanding of Promises and async/await
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Architecture Overview
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
┌─────────────────────────────────────────────────────────┐
|
|
68
|
+
│ Your Application │
|
|
69
|
+
│ (E-commerce, Logistics Dashboard, Mobile App, etc.) │
|
|
70
|
+
└────────────────────┬────────────────────────────────────┘
|
|
71
|
+
│
|
|
72
|
+
┌────────────┴───────────────┐
|
|
73
|
+
│ De. SDK │
|
|
74
|
+
├────────────────────────────┤
|
|
75
|
+
│ │
|
|
76
|
+
│ ┌──────────────────────┐ │
|
|
77
|
+
│ │ MSI (Map System) │ │
|
|
78
|
+
│ │ ┌────────────────┐ │ │
|
|
79
|
+
│ │ │ Controls │ │ │ ← Promise-based direct operations
|
|
80
|
+
│ │ ├────────────────┤ │ │
|
|
81
|
+
│ │ │ Handles │ │ │ ← Stream-based high-level API
|
|
82
|
+
│ │ ├────────────────┤ │ │
|
|
83
|
+
│ │ │ Plugins │ │ │ ← Extensibility system
|
|
84
|
+
│ │ └────────────────┘ │ │
|
|
85
|
+
│ └──────────────────────┘ │
|
|
86
|
+
│ │
|
|
87
|
+
│ ┌──────────────────────┐ │
|
|
88
|
+
│ │ DClient │ │
|
|
89
|
+
│ │ - Order │ │ ← Order management
|
|
90
|
+
│ │ - Event │ │ ← Event tracking
|
|
91
|
+
│ │ - Client │ │ ← Customer operations
|
|
92
|
+
│ └──────────────────────┘ │
|
|
93
|
+
│ │
|
|
94
|
+
│ ┌──────────────────────┐ │
|
|
95
|
+
│ │ Access │ │ ← API authentication
|
|
96
|
+
│ └──────────────────────┘ │
|
|
97
|
+
│ │
|
|
98
|
+
│ ┌──────────────────────┐ │
|
|
99
|
+
│ │ Utils │ │ ← Utility functions
|
|
100
|
+
│ └──────────────────────┘ │
|
|
101
|
+
└────────────────────────────┘
|
|
102
|
+
│
|
|
103
|
+
▼
|
|
104
|
+
┌──────────────────┐
|
|
105
|
+
│ Iframe (MSI) │
|
|
106
|
+
│ Map Engine │ ← Isolated rendering environment
|
|
107
|
+
│ (MapBox/Google) │
|
|
108
|
+
└──────────────────┘
|
|
109
|
+
│
|
|
110
|
+
▼
|
|
111
|
+
┌──────────────────┐
|
|
112
|
+
│ De. Platform │
|
|
113
|
+
│ - Logistics API │
|
|
114
|
+
│ - Route Engine │
|
|
115
|
+
│ - Coordination │
|
|
116
|
+
└──────────────────┘
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Component Responsibilities
|
|
120
|
+
|
|
121
|
+
| Component | Purpose | Use Cases |
|
|
122
|
+
|-----------|---------|-----------|
|
|
123
|
+
| **MSI** | Map visualization & interaction | Delivery tracking, route planning, location selection |
|
|
124
|
+
| **Controls** | Direct map manipulation (Promise-based) | Set routes, update markers, control navigation |
|
|
125
|
+
| **Handles** | High-level workflows (Stream-based) | Real-time tracking, fleet management, live updates |
|
|
126
|
+
| **Plugins** | Custom extensions | Analytics, custom markers, third-party integrations |
|
|
127
|
+
| **DClient** | Order & customer management | Create orders, track status, manage clients |
|
|
128
|
+
| **Access** | API communication | Authenticated requests, token management |
|
|
129
|
+
| **Utils** | Helper functions | Distance calculation, formatting, validation |
|
|
130
|
+
|
|
131
|
+
### Communication Flow
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
Your App → Access → De. API → Backend Services
|
|
135
|
+
↓
|
|
136
|
+
Your App → MSI → Iframe → Map Engine
|
|
137
|
+
↓ ↓
|
|
138
|
+
Your App ← Controls/Handles ← Map Events
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Installation & Setup
|
|
144
|
+
|
|
145
|
+
### NPM Installation
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npm install @de./sdk
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### CDN Installation
|
|
152
|
+
|
|
153
|
+
```html
|
|
154
|
+
<script src="https://cdn.dedot.io/sdk/v2/dedot.min.js"></script>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Basic Initialization
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import De from '@de./sdk';
|
|
161
|
+
|
|
162
|
+
// Initialize MSI for map visualization
|
|
163
|
+
const msi = new De.MSI({
|
|
164
|
+
element: 'map-container', // DOM element ID
|
|
165
|
+
accessToken: 'your-access-token',
|
|
166
|
+
env: 'prod', // or 'dev'
|
|
167
|
+
version: 1 // API version (optional)
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Load and get access to all components
|
|
171
|
+
const { controls, handles, plugins } = await msi.load();
|
|
172
|
+
|
|
173
|
+
console.log('De. SDK initialized successfully!');
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Full Stack Setup
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import De from '@de./sdk';
|
|
180
|
+
|
|
181
|
+
// 1. Initialize Access for API operations
|
|
182
|
+
const access = new De.Access({
|
|
183
|
+
workspace: 'your-workspace-id',
|
|
184
|
+
accessToken: 'your-access-token',
|
|
185
|
+
remoteOrigin: window.location.origin,
|
|
186
|
+
env: 'prod', // 'dev' | 'prod'
|
|
187
|
+
version: 1 // API version (optional)
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// 2. Initialize MSI for map interface
|
|
191
|
+
const msi = new De.MSI({
|
|
192
|
+
element: 'map-container',
|
|
193
|
+
accessToken: 'your-access-token',
|
|
194
|
+
env: 'prod',
|
|
195
|
+
version: 1
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// 3. Load MSI components
|
|
199
|
+
const { controls, handles, plugins } = await msi.load();
|
|
200
|
+
|
|
201
|
+
// 4. Initialize DClient for order management
|
|
202
|
+
const { Client, Order, Event } = De.DClient;
|
|
203
|
+
|
|
204
|
+
// Now you have full access to:
|
|
205
|
+
// - controls: Direct map operations
|
|
206
|
+
// - handles: Stream-based high-level API
|
|
207
|
+
// - plugins: Custom extensions
|
|
208
|
+
// - access: Authenticated API calls
|
|
209
|
+
// - DClient: Order/customer management
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Configuration Options
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
interface MapOptions {
|
|
216
|
+
element: string; // Required: DOM element ID
|
|
217
|
+
accessToken: string; // Required: Authentication token
|
|
218
|
+
env?: 'dev' | 'prod'; // Environment (default: 'prod')
|
|
219
|
+
version?: number; // API version (default: 1)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
interface AccessOptions {
|
|
223
|
+
workspace: string; // Required: Workspace identifier
|
|
224
|
+
accessToken: string; // Required: Authentication token
|
|
225
|
+
remoteOrigin?: string; // Optional: Origin for CORS
|
|
226
|
+
env?: 'dev' | 'prod'; // Environment (default: 'prod')
|
|
227
|
+
version?: number; // API version (default: 1)
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Quick Start Example
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// Initialize
|
|
235
|
+
const msi = new De.MSI({
|
|
236
|
+
element: 'map-container',
|
|
237
|
+
accessToken: 'your-token',
|
|
238
|
+
env: 'prod'
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const { controls, handles } = await msi.load();
|
|
242
|
+
|
|
243
|
+
// Set delivery route
|
|
244
|
+
await handles.pickupPoint(
|
|
245
|
+
{ lng: -74.0060, lat: 40.7128 },
|
|
246
|
+
{ label: 'Warehouse A', duration: 5, unit: 'min' }
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
await handles.dropoffPoint(
|
|
250
|
+
{ lng: -73.9855, lat: 40.7580 },
|
|
251
|
+
{ label: 'Customer' }
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// Track delivery vehicle
|
|
255
|
+
const vehicleStream = handles.nearby([{
|
|
256
|
+
id: 'vehicle-1',
|
|
257
|
+
type: 'car',
|
|
258
|
+
status: 'ACTIVE',
|
|
259
|
+
grade: '2H',
|
|
260
|
+
currentLocation: { lng: -74.0050, lat: 40.7200 }
|
|
261
|
+
}]);
|
|
262
|
+
|
|
263
|
+
console.log('Delivery tracking active!');
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Core Components
|
|
269
|
+
|
|
270
|
+
### 1. MSI (Map System Interface)
|
|
271
|
+
|
|
272
|
+
The main orchestrator that embeds the map interface via iframe and provides access to all sub-components.
|
|
273
|
+
|
|
274
|
+
#### Constructor
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
const msi = new De.MSI(options: MapOptions);
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### Key Methods
|
|
281
|
+
|
|
282
|
+
**`load(): Promise<MSIInterface>`**
|
|
283
|
+
|
|
284
|
+
Initializes the iframe, establishes communication, and returns all components.
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
const { controls, handles, plugins } = await msi.load();
|
|
288
|
+
|
|
289
|
+
// Returns:
|
|
290
|
+
interface MSIInterface {
|
|
291
|
+
controls: Controls; // Direct map operations
|
|
292
|
+
handles: Handles; // Stream-based workflows
|
|
293
|
+
plugins: Plugins; // Extension system
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**`isReady(): boolean`**
|
|
298
|
+
|
|
299
|
+
Check if MSI is loaded and ready for operations.
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
if (msi.isReady()) {
|
|
303
|
+
await controls.getCurrentLocation();
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**`plugin<T>(name: string, fn: Plugin<T>)`**
|
|
308
|
+
|
|
309
|
+
Register custom plugins before loading MSI.
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
// Register plugin
|
|
313
|
+
msi.plugin('deliveryAnalytics', (hooks, options) => {
|
|
314
|
+
return {
|
|
315
|
+
trackDelivery: (orderId) => {
|
|
316
|
+
// Implementation
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Use after loading
|
|
322
|
+
const { plugins } = await msi.load();
|
|
323
|
+
const analytics = plugins.use('deliveryAnalytics');
|
|
324
|
+
await analytics.trackDelivery('order-123');
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
#### Events
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// Listen to MSI lifecycle events
|
|
331
|
+
msi.on('ready', () => {
|
|
332
|
+
console.log('MSI is ready for interaction');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
msi.on('error', (error: Error) => {
|
|
336
|
+
console.error('MSI error:', error);
|
|
337
|
+
// Handle initialization errors
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
msi.on('loaded', (channel: IOF) => {
|
|
341
|
+
console.log('Iframe loaded and communication established');
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
#### Best Practices
|
|
346
|
+
|
|
347
|
+
1. **Always wait for load** before using controls/handles
|
|
348
|
+
2. **Handle errors** during initialization
|
|
349
|
+
3. **Register plugins** before calling `load()`
|
|
350
|
+
4. **Clean up** on component unmount
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
// React example
|
|
354
|
+
useEffect(() => {
|
|
355
|
+
const msi = new De.MSI(config);
|
|
356
|
+
let api: MSIInterface;
|
|
357
|
+
|
|
358
|
+
msi.on('error', handleError);
|
|
359
|
+
|
|
360
|
+
msi.load()
|
|
361
|
+
.then(loadedApi => {
|
|
362
|
+
api = loadedApi;
|
|
363
|
+
// Use controls, handles, plugins
|
|
364
|
+
})
|
|
365
|
+
.catch(handleError);
|
|
366
|
+
|
|
367
|
+
return () => {
|
|
368
|
+
// Cleanup
|
|
369
|
+
api?.handles.removeListeners();
|
|
370
|
+
};
|
|
371
|
+
}, []);
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
### 2. Controls
|
|
377
|
+
|
|
378
|
+
Promise-based API for direct map manipulation. All operations return Promises with 12-second timeout protection.
|
|
379
|
+
|
|
380
|
+
#### Location Services
|
|
381
|
+
|
|
382
|
+
**`getCurrentLocation(): Promise<RTLocation | null>`**
|
|
383
|
+
|
|
384
|
+
Get user's current GPS location with accuracy, heading, and speed.
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
const location = await controls.getCurrentLocation();
|
|
388
|
+
console.log(location);
|
|
389
|
+
// {
|
|
390
|
+
// latitude: 40.7128,
|
|
391
|
+
// longitude: -74.0060,
|
|
392
|
+
// accuracy: 10, // meters
|
|
393
|
+
// heading: 180, // degrees (0-360)
|
|
394
|
+
// speed: 15 // meters/second
|
|
395
|
+
// }
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
**`pinCurrentLocation(): Promise<Coordinates | null>`**
|
|
399
|
+
|
|
400
|
+
Display user's location on map with a marker.
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
const coords = await controls.pinCurrentLocation();
|
|
404
|
+
// Returns: [latitude, longitude] or null
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**`trackLiveLocation(): Promise<void>`**
|
|
408
|
+
|
|
409
|
+
Start continuous location tracking.
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
// Configure tracking
|
|
413
|
+
await controls.setLiveLocationOptions({
|
|
414
|
+
accuracy: 'high', // 'high' | 'medium' | 'low'
|
|
415
|
+
updateInterval: 5000, // milliseconds
|
|
416
|
+
showAccuracyCircle: true
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Start tracking
|
|
420
|
+
await controls.trackLiveLocation();
|
|
421
|
+
|
|
422
|
+
// Stop when done
|
|
423
|
+
await controls.untrackLiveLocation();
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
#### Map Styling
|
|
427
|
+
|
|
428
|
+
**`setMapStyle(style: MapLayerStyle): Promise<void>`**
|
|
429
|
+
|
|
430
|
+
Change map appearance.
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
await controls.setMapStyle('dark');
|
|
434
|
+
// Options: 'streets' | 'outdoors' | 'light' | 'dark' | 'satellite'
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
#### Search & Geocoding
|
|
438
|
+
|
|
439
|
+
**`searchQuery(input: string): Promise<string[]>`**
|
|
440
|
+
|
|
441
|
+
Search for places or addresses.
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
const suggestions = await controls.searchQuery('coffee shops near me');
|
|
445
|
+
// Returns: ['Starbucks - 123 Main St', 'Dunkin - 456 Park Ave', ...]
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
**`searchSelect(index: number): Promise<SearchPlace | null>`**
|
|
449
|
+
|
|
450
|
+
Get details of a search result.
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
const place = await controls.searchSelect(0);
|
|
454
|
+
console.log(place);
|
|
455
|
+
// {
|
|
456
|
+
// name: 'Starbucks',
|
|
457
|
+
// location: { lng: -73.985428, lat: 40.748817 },
|
|
458
|
+
// address: '123 Main St, New York, NY'
|
|
459
|
+
// }
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**`resolvePlace(name: string): Promise<Coordinates | null>`**
|
|
463
|
+
|
|
464
|
+
Get coordinates from place name (geocoding).
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
const coords = await controls.resolvePlace('Empire State Building');
|
|
468
|
+
// Returns: { lng: -73.985428, lat: 40.748817 }
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
**`resolveCoordinates(coords: Coordinates): Promise<any | null>`**
|
|
472
|
+
|
|
473
|
+
Get place details from coordinates (reverse geocoding).
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
const place = await controls.resolveCoordinates({ lng: -74.0060, lat: 40.7128 });
|
|
477
|
+
console.log(place);
|
|
478
|
+
// {
|
|
479
|
+
// name: 'Lower Manhattan',
|
|
480
|
+
// address: 'Manhattan, New York, NY',
|
|
481
|
+
// postalCode: '10007',
|
|
482
|
+
// ...
|
|
483
|
+
// }
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
#### Drag & Pick Location
|
|
487
|
+
|
|
488
|
+
**`enableDragPickLocation(initialLocation?: Coordinates): Promise<void>`**
|
|
489
|
+
|
|
490
|
+
Enable interactive location picker.
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
// Enable with initial position
|
|
494
|
+
await controls.enableDragPickLocation({ lng: -74.0060, lat: 40.7128 });
|
|
495
|
+
|
|
496
|
+
// Listen for picked location
|
|
497
|
+
controls.on('pick:location', (location) => {
|
|
498
|
+
console.log('User picked:', location.coordinates);
|
|
499
|
+
console.log('Point:', location.point);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Disable when done
|
|
503
|
+
await controls.disableDragPickLocation();
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
**`setDragPickContent(type: DragPickContentType, content: DragPickContent): Promise<void>`**
|
|
507
|
+
|
|
508
|
+
Customize the drag picker content display.
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
// Show duration
|
|
512
|
+
await controls.setDragPickContent('duration', {
|
|
513
|
+
time: 15,
|
|
514
|
+
unit: 'min'
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Show distance
|
|
518
|
+
await controls.setDragPickContent('distance', {
|
|
519
|
+
distance: 5.2,
|
|
520
|
+
unit: 'km'
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// Show preloader
|
|
524
|
+
await controls.setDragPickContent('preloader', {
|
|
525
|
+
preloader: true
|
|
526
|
+
});
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
#### Route Management
|
|
530
|
+
|
|
531
|
+
**`setRoute(journey: Journey): Promise<void>`**
|
|
532
|
+
|
|
533
|
+
Create a complete route with origin, waypoints, and destination.
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
await controls.setRoute({
|
|
537
|
+
routeId: 'delivery-route-1',
|
|
538
|
+
origin: {
|
|
539
|
+
coords: { lng: -74.0060, lat: 40.7128 },
|
|
540
|
+
caption: {
|
|
541
|
+
label: 'Warehouse',
|
|
542
|
+
duration: 5,
|
|
543
|
+
unit: 'min'
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
destination: {
|
|
547
|
+
coords: { lng: -73.9855, lat: 40.7580 },
|
|
548
|
+
caption: {
|
|
549
|
+
label: 'Customer',
|
|
550
|
+
duration: 15,
|
|
551
|
+
unit: 'min'
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
waypoints: [
|
|
555
|
+
{
|
|
556
|
+
coords: { lng: -73.9680, lat: 40.7489 },
|
|
557
|
+
caption: { label: 'Stop 1: Package Pickup' },
|
|
558
|
+
index: 0
|
|
559
|
+
}
|
|
560
|
+
],
|
|
561
|
+
options: {
|
|
562
|
+
mode: 'navigation',
|
|
563
|
+
profile: 'driving-traffic',
|
|
564
|
+
unit: 'metric',
|
|
565
|
+
preference: 'TRAFFIC_AWARE',
|
|
566
|
+
animation: 'dot:flow'
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
**Route Options Explained:**
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
interface RouteOptions {
|
|
575
|
+
id?: string | number;
|
|
576
|
+
mode?: 'default' | 'navigation';
|
|
577
|
+
|
|
578
|
+
// Profile determines routing algorithm
|
|
579
|
+
profile?: 'driving-traffic' // Real-time traffic (recommended)
|
|
580
|
+
| 'driving' // Standard driving
|
|
581
|
+
| 'cycling' // Bicycle routes
|
|
582
|
+
| 'biking' // Bike paths
|
|
583
|
+
| 'walking' // Pedestrian routes
|
|
584
|
+
| 'transit'; // Public transport
|
|
585
|
+
|
|
586
|
+
unit?: 'metric' | 'imperial';
|
|
587
|
+
|
|
588
|
+
// Traffic preference
|
|
589
|
+
preference?: 'TRAFFIC_AWARE' // Use real-time traffic
|
|
590
|
+
| 'TRAFFIC_UNAWARE'; // Ignore traffic
|
|
591
|
+
|
|
592
|
+
pointless?: boolean; // Hide waypoint markers
|
|
593
|
+
styles?: any; // Custom route styling
|
|
594
|
+
|
|
595
|
+
// Animated route options
|
|
596
|
+
animation?: 'dot:flow' // Flowing dots
|
|
597
|
+
| 'dot:fade' // Fading dots
|
|
598
|
+
| 'dot:pulse' // Pulsing dots
|
|
599
|
+
| 'dot:directional' // Directional dots
|
|
600
|
+
| 'solid:flow' // Flowing solid line
|
|
601
|
+
| 'solid:fade' // Fading solid line
|
|
602
|
+
| 'solid:pulse' // Pulsing solid line
|
|
603
|
+
| 'solid:directional' // Directional solid line
|
|
604
|
+
| AnimatedRouteCustomOptions;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Custom animation options
|
|
608
|
+
interface AnimatedRouteCustomOptions {
|
|
609
|
+
handler?: new (engine: Engine, path: Coordinates[], rules?: AnimatedRouteRules) => AnimatedRoute;
|
|
610
|
+
method?: string;
|
|
611
|
+
rules?: {
|
|
612
|
+
styles?: any;
|
|
613
|
+
speed?: number; // pixels per frame
|
|
614
|
+
fadeLength?: number; // fade effect length in pixels
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**`setRouteOrigin(routeId: string, point: MapWaypoint): Promise<void>`**
|
|
620
|
+
|
|
621
|
+
Set or update route starting point.
|
|
622
|
+
|
|
623
|
+
```typescript
|
|
624
|
+
await controls.setRouteOrigin('route-1', {
|
|
625
|
+
coords: [40.7128, -74.0060],
|
|
626
|
+
caption: { label: 'Warehouse A' }
|
|
627
|
+
});
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
**`setRouteDestination(routeId: string, point: MapWaypoint): Promise<void>`**
|
|
631
|
+
|
|
632
|
+
Set or update route ending point.
|
|
633
|
+
|
|
634
|
+
```typescript
|
|
635
|
+
await controls.setRouteDestination('route-1', {
|
|
636
|
+
coords: [40.7580, -73.9855],
|
|
637
|
+
caption: { label: 'Customer Delivery' }
|
|
638
|
+
});
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
**`addRouteWaypoint(routeId: string, point: MapWaypoint): Promise<void>`**
|
|
642
|
+
|
|
643
|
+
Add stop to existing route.
|
|
644
|
+
|
|
645
|
+
```typescript
|
|
646
|
+
await controls.addRouteWaypoint('route-1', {
|
|
647
|
+
coords: [40.7489, -73.9680],
|
|
648
|
+
caption: {
|
|
649
|
+
label: 'Additional Stop',
|
|
650
|
+
description: 'Package pickup at retail store'
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
**`updateRouteWaypoint(routeId: string, point: MapWaypoint): Promise<void>`**
|
|
656
|
+
|
|
657
|
+
Update existing waypoint.
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
await controls.updateRouteWaypoint('route-1', {
|
|
661
|
+
index: 0,
|
|
662
|
+
coords: [40.7500, -73.9700],
|
|
663
|
+
caption: { label: 'Updated Stop Location' }
|
|
664
|
+
});
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
**`removeRouteWaypoint(routeId: string, index: number): Promise<void>`**
|
|
668
|
+
|
|
669
|
+
Remove waypoint from route.
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
await controls.removeRouteWaypoint('route-1', 0);
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
**`fitRouteBounds(routeId: string, margin?: number): Promise<void>`**
|
|
676
|
+
|
|
677
|
+
Adjust map view to show entire route.
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
await controls.fitRouteBounds('route-1', 100); // 100px margin
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
**`fitRoutesBounds(options: RoutesFitBoundsOptions): Promise<void>`**
|
|
684
|
+
|
|
685
|
+
Fit multiple routes in view simultaneously.
|
|
686
|
+
|
|
687
|
+
```typescript
|
|
688
|
+
await controls.fitRoutesBounds({
|
|
689
|
+
includes: ['route-1', 'route-2', 'route-3'],
|
|
690
|
+
margin: 80
|
|
691
|
+
});
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
#### Nearby Entities
|
|
695
|
+
|
|
696
|
+
**`showNearby(entities: EntitySpecs[]): Promise<void>`**
|
|
697
|
+
|
|
698
|
+
Display multiple entities (vehicles, warehouses, etc.) on map.
|
|
699
|
+
|
|
700
|
+
```typescript
|
|
701
|
+
await controls.showNearby([
|
|
702
|
+
{
|
|
703
|
+
id: 'vehicle-1',
|
|
704
|
+
type: 'car',
|
|
705
|
+
status: 'ACTIVE',
|
|
706
|
+
grade: '2H',
|
|
707
|
+
currentLocation: { lng: -74.0060, lat: 40.7128 }
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
id: 'warehouse-1',
|
|
711
|
+
type: 'warehouse',
|
|
712
|
+
status: 'ACTIVE',
|
|
713
|
+
grade: '1H',
|
|
714
|
+
currentLocation: { lng: -74.0100, lat: 40.7200 },
|
|
715
|
+
static: true
|
|
716
|
+
}
|
|
717
|
+
]);
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
**`addNearbyEntity(entity: EntitySpecs): Promise<void>`**
|
|
721
|
+
|
|
722
|
+
Add single entity to map.
|
|
723
|
+
|
|
724
|
+
```typescript
|
|
725
|
+
await controls.addNearbyEntity({
|
|
726
|
+
id: 'truck-5',
|
|
727
|
+
type: 'truck',
|
|
728
|
+
status: 'BUSY',
|
|
729
|
+
grade: '3H',
|
|
730
|
+
currentLocation: { lng: -74.0200, lat: 40.7300 }
|
|
731
|
+
});
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
**`moveNearbyEntity(update: ActivePosition): Promise<void>`**
|
|
735
|
+
|
|
736
|
+
Update entity's position (for real-time tracking).
|
|
737
|
+
|
|
738
|
+
```typescript
|
|
739
|
+
await controls.moveNearbyEntity({
|
|
740
|
+
id: 'vehicle-1',
|
|
741
|
+
position: { lng: -74.0065, lat: 40.7130, heading: 90 },
|
|
742
|
+
caption: { label: 'Van #101', duration: 5, unit: 'min' }
|
|
743
|
+
});
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
**`removeNearbyEntity(id: string): Promise<void>`**
|
|
747
|
+
|
|
748
|
+
Remove specific entity from map.
|
|
749
|
+
|
|
750
|
+
```typescript
|
|
751
|
+
await controls.removeNearbyEntity('vehicle-1');
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
**`removeNearby(): Promise<void>`**
|
|
755
|
+
|
|
756
|
+
Clear all nearby entities.
|
|
757
|
+
|
|
758
|
+
```typescript
|
|
759
|
+
await controls.removeNearby();
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
#### Navigation
|
|
763
|
+
|
|
764
|
+
**`mountNavigation(routeId: string): Promise<void>`**
|
|
765
|
+
|
|
766
|
+
Prepare route for turn-by-turn navigation.
|
|
767
|
+
|
|
768
|
+
```typescript
|
|
769
|
+
await controls.mountNavigation('delivery-route-1');
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
**`loadNavigation(): Promise<void>`**
|
|
773
|
+
|
|
774
|
+
Load navigation instructions and UI.
|
|
775
|
+
|
|
776
|
+
```typescript
|
|
777
|
+
await controls.loadNavigation();
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
**`setInitialNavigationPosition(position: RTLocation): Promise<void>`**
|
|
781
|
+
|
|
782
|
+
Set starting position for navigation.
|
|
783
|
+
|
|
784
|
+
```typescript
|
|
785
|
+
await controls.setInitialNavigationPosition({
|
|
786
|
+
latitude: 40.7128,
|
|
787
|
+
longitude: -74.0060,
|
|
788
|
+
accuracy: 10,
|
|
789
|
+
heading: 90,
|
|
790
|
+
speed: 0
|
|
791
|
+
});
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
**`navigate(position: RTLocation): Promise<void>`**
|
|
795
|
+
|
|
796
|
+
Update navigation with current position.
|
|
797
|
+
|
|
798
|
+
```typescript
|
|
799
|
+
// In GPS update loop
|
|
800
|
+
await controls.navigate({
|
|
801
|
+
latitude: 40.7130,
|
|
802
|
+
longitude: -74.0062,
|
|
803
|
+
accuracy: 8,
|
|
804
|
+
heading: 95,
|
|
805
|
+
speed: 15
|
|
806
|
+
});
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
**`casting(routeId: string, direction: any, position?: RTLocation, options?: RouteOptions): Promise<void>`**
|
|
810
|
+
|
|
811
|
+
Upsert navigation direction data (for receiving external navigation).
|
|
812
|
+
|
|
813
|
+
```typescript
|
|
814
|
+
await controls.casting(
|
|
815
|
+
'route-1',
|
|
816
|
+
externalNavigationData,
|
|
817
|
+
currentPosition,
|
|
818
|
+
{ mode: 'driving' }
|
|
819
|
+
);
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
**`dismissNavigation(): Promise<void>`**
|
|
823
|
+
|
|
824
|
+
End navigation session.
|
|
825
|
+
|
|
826
|
+
```typescript
|
|
827
|
+
await controls.dismissNavigation();
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
**`unmountNavigation(): Promise<void>`**
|
|
831
|
+
|
|
832
|
+
Remove navigation from route.
|
|
833
|
+
|
|
834
|
+
```typescript
|
|
835
|
+
await controls.unmountNavigation();
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
#### Waypoint Captions
|
|
839
|
+
|
|
840
|
+
**`setWaypointCaption(routeId: string, id: string | number, caption: Caption): Promise<void>`**
|
|
841
|
+
|
|
842
|
+
Add caption to waypoint.
|
|
843
|
+
|
|
844
|
+
```typescript
|
|
845
|
+
await controls.setWaypointCaption('route-1', 0, {
|
|
846
|
+
label: 'Priority Delivery',
|
|
847
|
+
sublabel: 'Fragile items',
|
|
848
|
+
description: 'Handle with care - Glass contents'
|
|
849
|
+
});
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
**`updateWaypointCaption(routeId: string, id: string | number, caption: Caption): Promise<void>`**
|
|
853
|
+
|
|
854
|
+
Update existing waypoint caption.
|
|
855
|
+
|
|
856
|
+
```typescript
|
|
857
|
+
await controls.updateWaypointCaption('route-1', 0, {
|
|
858
|
+
label: 'Completed',
|
|
859
|
+
sublabel: 'Delivered at 2:30 PM'
|
|
860
|
+
});
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
**`removeWaypointCaption(routeId: string, id: string | number): Promise<void>`**
|
|
864
|
+
|
|
865
|
+
Remove waypoint caption.
|
|
866
|
+
|
|
867
|
+
```typescript
|
|
868
|
+
await controls.removeWaypointCaption('route-1', 0);
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
#### Event Listeners
|
|
872
|
+
|
|
873
|
+
**`on(event: string, listener: Function)`**
|
|
874
|
+
|
|
875
|
+
Subscribe to map events.
|
|
876
|
+
|
|
877
|
+
```typescript
|
|
878
|
+
controls.on('route:updated', (data) => {
|
|
879
|
+
console.log('Route updated:', data);
|
|
880
|
+
});
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
**`off(event: string, listener: Function)`**
|
|
884
|
+
|
|
885
|
+
Unsubscribe from specific event.
|
|
886
|
+
|
|
887
|
+
```typescript
|
|
888
|
+
const handler = (data) => console.log(data);
|
|
889
|
+
controls.on('route:updated', handler);
|
|
890
|
+
controls.off('route:updated', handler);
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
**`removeListeners(listener: Function)`**
|
|
894
|
+
|
|
895
|
+
Remove all instances of a listener.
|
|
896
|
+
|
|
897
|
+
```typescript
|
|
898
|
+
controls.removeListeners(myHandler);
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
---
|
|
902
|
+
|
|
903
|
+
### 3. Handles
|
|
904
|
+
|
|
905
|
+
High-level, stream-based API for real-time data workflows. Handles abstract complex operations into simple, composable streams.
|
|
906
|
+
|
|
907
|
+
#### Location Streams
|
|
908
|
+
|
|
909
|
+
**`myLocation(usertype?: 'client' | 'agent'): Stream`**
|
|
910
|
+
|
|
911
|
+
Create stream of user's current location with continuous updates.
|
|
912
|
+
|
|
913
|
+
```typescript
|
|
914
|
+
const locationStream = handles.myLocation('agent');
|
|
915
|
+
|
|
916
|
+
// Listen to location updates
|
|
917
|
+
locationStream.pipe(location => {
|
|
918
|
+
console.log('Current position:', location.latitude, location.longitude);
|
|
919
|
+
console.log('Speed:', location.speed, 'm/s');
|
|
920
|
+
console.log('Heading:', location.heading, 'degrees');
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
// Handle errors
|
|
924
|
+
locationStream.onerror(error => {
|
|
925
|
+
console.error('Location error:', error);
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
// Clean up when done
|
|
929
|
+
locationStream.onclose(() => {
|
|
930
|
+
console.log('Location tracking stopped');
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
// Stop tracking
|
|
934
|
+
locationStream.close();
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
**`peerLocation(position: RTLocation, caption?: Caption): Stream`**
|
|
938
|
+
|
|
939
|
+
Stream for displaying peer's location (e.g., driver tracking for customer).
|
|
940
|
+
|
|
941
|
+
```typescript
|
|
942
|
+
const driverStream = handles.peerLocation(
|
|
943
|
+
{
|
|
944
|
+
latitude: 40.7128,
|
|
945
|
+
longitude: -74.0060,
|
|
946
|
+
accuracy: 10,
|
|
947
|
+
heading: 180,
|
|
948
|
+
speed: 25
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
label: 'Your Driver',
|
|
952
|
+
sublabel: 'Mike Johnson',
|
|
953
|
+
description: 'White Ford Transit - ABC-123'
|
|
954
|
+
}
|
|
955
|
+
);
|
|
956
|
+
|
|
957
|
+
// Update driver position
|
|
958
|
+
driverStream.write({
|
|
959
|
+
position: {
|
|
960
|
+
latitude: 40.7130,
|
|
961
|
+
longitude: -74.0062,
|
|
962
|
+
accuracy: 8,
|
|
963
|
+
heading: 175,
|
|
964
|
+
speed: 30
|
|
965
|
+
},
|
|
966
|
+
caption: {
|
|
967
|
+
label: 'Your Driver',
|
|
968
|
+
sublabel: 'Mike Johnson',
|
|
969
|
+
description: '5 minutes away'
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
// Close when delivery complete
|
|
974
|
+
driverStream.close();
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
**`onPickLocation(fn: (location: PickedLocation) => void)`**
|
|
978
|
+
|
|
979
|
+
Listen for user-selected locations on map.
|
|
980
|
+
|
|
981
|
+
```typescript
|
|
982
|
+
handles.onPickLocation(location => {
|
|
983
|
+
console.log('Coordinates:', location.coordinates);
|
|
984
|
+
console.log('Address:', location.address);
|
|
985
|
+
console.log('Place:', location.placeName);
|
|
986
|
+
|
|
987
|
+
// Use the picked location
|
|
988
|
+
saveDeliveryAddress(location);
|
|
989
|
+
});
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
#### Entity Management
|
|
993
|
+
|
|
994
|
+
**`nearby(entities: Entity[]): NearbyStream`**
|
|
995
|
+
|
|
996
|
+
Create live stream for managing fleet/nearby entities with real-time controls.
|
|
997
|
+
|
|
998
|
+
```typescript
|
|
999
|
+
// Initialize with current fleet
|
|
1000
|
+
const fleetStream = handles.nearby([
|
|
1001
|
+
{
|
|
1002
|
+
id: 'van-1',
|
|
1003
|
+
type: 'delivery-van',
|
|
1004
|
+
position: [40.7128, -74.0060],
|
|
1005
|
+
caption: { label: 'Van #101', sublabel: '5 deliveries pending' }
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
id: 'truck-1',
|
|
1009
|
+
type: 'freight-truck',
|
|
1010
|
+
position: [40.7200, -74.0100],
|
|
1011
|
+
caption: { label: 'Truck #201', sublabel: '2 pickups remaining' }
|
|
1012
|
+
}
|
|
1013
|
+
]);
|
|
1014
|
+
|
|
1015
|
+
// Get live control interface
|
|
1016
|
+
fleetStream.live(async controls => {
|
|
1017
|
+
// Add new vehicle
|
|
1018
|
+
await controls.add({
|
|
1019
|
+
id: 'van-2',
|
|
1020
|
+
type: 'delivery-van',
|
|
1021
|
+
position: [40.7300, -74.0150],
|
|
1022
|
+
caption: { label: 'Van #102', sublabel: 'Just dispatched' }
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
// Update vehicle position (real-time tracking)
|
|
1026
|
+
setInterval(async () => {
|
|
1027
|
+
await controls.move({
|
|
1028
|
+
id: 'van-1',
|
|
1029
|
+
position: getLatestVehiclePosition('van-1')
|
|
1030
|
+
});
|
|
1031
|
+
}, 5000);
|
|
1032
|
+
|
|
1033
|
+
// Remove vehicle when done
|
|
1034
|
+
await controls.remove('truck-1');
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
// Monitor all fleet changes
|
|
1038
|
+
fleetStream.pipe(update => {
|
|
1039
|
+
console.log(`Fleet ${update.action}:`, update.dataset);
|
|
1040
|
+
console.log('Total vehicles:', update.list.length);
|
|
1041
|
+
|
|
1042
|
+
// Update dashboard
|
|
1043
|
+
updateFleetDashboard(update.list);
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
// Close stream
|
|
1047
|
+
fleetStream.close();
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
#### Route Helpers
|
|
1051
|
+
|
|
1052
|
+
**`pickupPoint(location: Coordinates, caption?: Caption): Promise<void>`**
|
|
1053
|
+
|
|
1054
|
+
Set pickup location with visual marker.
|
|
1055
|
+
|
|
1056
|
+
```typescript
|
|
1057
|
+
await handles.pickupPoint(
|
|
1058
|
+
{ lng: -74.0060, lat: 40.7128 },
|
|
1059
|
+
{
|
|
1060
|
+
label: 'Warehouse A',
|
|
1061
|
+
duration: 5,
|
|
1062
|
+
unit: 'min'
|
|
1063
|
+
}
|
|
1064
|
+
);
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
**`dropoffPoint(location: Coordinates, caption?: Caption): Promise<void>`**
|
|
1068
|
+
|
|
1069
|
+
Set delivery destination with marker.
|
|
1070
|
+
|
|
1071
|
+
```typescript
|
|
1072
|
+
await handles.dropoffPoint(
|
|
1073
|
+
{ lng: -73.9855, lat: 40.7580 },
|
|
1074
|
+
{
|
|
1075
|
+
label: 'Customer Address',
|
|
1076
|
+
duration: 15,
|
|
1077
|
+
unit: 'min'
|
|
1078
|
+
}
|
|
1079
|
+
);
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
#### Navigation Streams
|
|
1083
|
+
|
|
1084
|
+
**`navigation(journey: Journey): Promise<Stream>`**
|
|
1085
|
+
|
|
1086
|
+
Start turn-by-turn navigation with real-time position updates.
|
|
1087
|
+
|
|
1088
|
+
```typescript
|
|
1089
|
+
// Start navigation
|
|
1090
|
+
const navStream = await handles.navigation({
|
|
1091
|
+
routeId: 'delivery-route-1',
|
|
1092
|
+
origin: {
|
|
1093
|
+
coords: await controls.getCurrentLocation(),
|
|
1094
|
+
caption: { label: 'Current Location' }
|
|
1095
|
+
},
|
|
1096
|
+
destination: {
|
|
1097
|
+
coords: [40.7580, -73.9855],
|
|
1098
|
+
caption: { label: 'Delivery Address' }
|
|
1099
|
+
},
|
|
1100
|
+
waypoints: [
|
|
1101
|
+
{
|
|
1102
|
+
coords: [40.7489, -73.9680],
|
|
1103
|
+
caption: { label: 'Package Pickup' },
|
|
1104
|
+
index: 0
|
|
1105
|
+
}
|
|
1106
|
+
],
|
|
1107
|
+
options: {
|
|
1108
|
+
mode: 'driving',
|
|
1109
|
+
avoidTolls: false
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
// Update with GPS positions
|
|
1114
|
+
const locationStream = handles.myLocation('agent');
|
|
1115
|
+
locationStream.pipe(location => {
|
|
1116
|
+
navStream.write({ position: location });
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
// Listen for navigation events
|
|
1120
|
+
handles.on('pe:started', () => console.log('Navigation started'));
|
|
1121
|
+
handles.on('pe:nearby', () => console.log('Approaching destination'));
|
|
1122
|
+
handles.on('pe:arrived', () => {
|
|
1123
|
+
console.log('Arrived at destination!');
|
|
1124
|
+
navStream.close();
|
|
1125
|
+
locationStream.close();
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
// Handle navigation errors
|
|
1129
|
+
handles.on('pe:closed', () => {
|
|
1130
|
+
console.log('Navigation ended');
|
|
1131
|
+
});
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
**`peerDirection(options?: RouteOptions): Stream`**
|
|
1135
|
+
|
|
1136
|
+
Stream for displaying peer's navigation (e.g., showing driver's route to customer).
|
|
1137
|
+
|
|
1138
|
+
```typescript
|
|
1139
|
+
const driverNavStream = handles.peerDirection({
|
|
1140
|
+
mode: 'driving',
|
|
1141
|
+
avoidTolls: false,
|
|
1142
|
+
announceInstructions: false
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
// Receive driver navigation updates from backend
|
|
1146
|
+
websocket.onmessage = (event) => {
|
|
1147
|
+
const { status, direction, position } = JSON.parse(event.data);
|
|
1148
|
+
|
|
1149
|
+
driverNavStream.write({
|
|
1150
|
+
status,
|
|
1151
|
+
direction,
|
|
1152
|
+
position
|
|
1153
|
+
});
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
// Listen to driver proximity events
|
|
1157
|
+
handles.on('pe:nearby', () => {
|
|
1158
|
+
notifyCustomer('Driver is nearby!');
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
handles.on('pe:arrived', () => {
|
|
1162
|
+
notifyCustomer('Driver has arrived!');
|
|
1163
|
+
driverNavStream.close();
|
|
1164
|
+
});
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
#### Navigation Status Events
|
|
1168
|
+
|
|
1169
|
+
```typescript
|
|
1170
|
+
// All available navigation events
|
|
1171
|
+
handles.on('pe:started', () => {}); // Navigation began
|
|
1172
|
+
handles.on('pe:stale', () => {}); // No movement detected
|
|
1173
|
+
handles.on('pe:long_stop', () => {}); // Stopped for extended period
|
|
1174
|
+
handles.on('pe:low_traffic', () => {}); // Light traffic conditions
|
|
1175
|
+
handles.on('pe:moderate_traffic', () => {}); // Moderate traffic
|
|
1176
|
+
handles.on('pe:high_traffic', () => {}); // Heavy traffic
|
|
1177
|
+
handles.on('pe:speed_warning', () => {}); // Speeding detected
|
|
1178
|
+
handles.on('pe:nearby', () => {}); // Approaching destination
|
|
1179
|
+
handles.on('pe:arrived', () => {}); // Reached destination
|
|
1180
|
+
handles.on('pe:closed', () => {}); // Navigation ended
|
|
1181
|
+
```
|
|
1182
|
+
|
|
1183
|
+
---
|
|
1184
|
+
|
|
1185
|
+
### 4. Plugins
|
|
1186
|
+
|
|
1187
|
+
Extensible system for adding custom functionality with access to all SDK hooks.
|
|
1188
|
+
|
|
1189
|
+
#### Plugin Architecture
|
|
1190
|
+
|
|
1191
|
+
```typescript
|
|
1192
|
+
type Plugin<API, Options = {}> = (
|
|
1193
|
+
hooks: PluginHook,
|
|
1194
|
+
options?: Options
|
|
1195
|
+
) => API;
|
|
1196
|
+
|
|
1197
|
+
interface PluginHook {
|
|
1198
|
+
handles: Handles; // Stream-based operations
|
|
1199
|
+
controls: Controls; // Direct map operations
|
|
1200
|
+
map: MapOptions; // Map configuration
|
|
1201
|
+
utils: typeof Utils; // Utility functions
|
|
1202
|
+
}
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
#### Creating a Plugin
|
|
1206
|
+
|
|
1207
|
+
```typescript
|
|
1208
|
+
// Define plugin interface
|
|
1209
|
+
interface DeliveryAnalyticsAPI {
|
|
1210
|
+
trackRoute: (routeId: string) => Promise<void>;
|
|
1211
|
+
getRouteMetrics: (routeId: string) => Promise<RouteMetrics>;
|
|
1212
|
+
exportData: () => Promise<string>;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
interface DeliveryAnalyticsOptions {
|
|
1216
|
+
apiKey?: string;
|
|
1217
|
+
debug?: boolean;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Implement plugin
|
|
1221
|
+
const deliveryAnalyticsPlugin: Plugin<DeliveryAnalyticsAPI, DeliveryAnalyticsOptions> = (
|
|
1222
|
+
hooks,
|
|
1223
|
+
options
|
|
1224
|
+
) => {
|
|
1225
|
+
const { controls, handles, map, utils } = hooks;
|
|
1226
|
+
const routeData = new Map();
|
|
1227
|
+
|
|
1228
|
+
return {
|
|
1229
|
+
async trackRoute(routeId: string) {
|
|
1230
|
+
const startTime = Date.now();
|
|
1231
|
+
|
|
1232
|
+
// Use handles to listen for completion
|
|
1233
|
+
handles.on('pe:arrived', () => {
|
|
1234
|
+
const endTime = Date.now();
|
|
1235
|
+
const duration = endTime - startTime;
|
|
1236
|
+
|
|
1237
|
+
routeData.set(routeId, {
|
|
1238
|
+
duration,
|
|
1239
|
+
timestamp: new Date(),
|
|
1240
|
+
distance: calculateDistance()
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
if (options?.debug) {
|
|
1244
|
+
console.log(`Route ${routeId} completed in ${duration}ms`);
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
},
|
|
1248
|
+
|
|
1249
|
+
async getRouteMetrics(routeId: string) {
|
|
1250
|
+
const data = routeData.get(routeId);
|
|
1251
|
+
if (!data) throw new Error('Route not tracked');
|
|
1252
|
+
|
|
1253
|
+
return {
|
|
1254
|
+
duration: data.duration,
|
|
1255
|
+
distance: data.distance,
|
|
1256
|
+
averageSpeed: data.distance / (data.duration / 1000)
|
|
1257
|
+
};
|
|
1258
|
+
},
|
|
1259
|
+
|
|
1260
|
+
async exportData() {
|
|
1261
|
+
return JSON.stringify(Array.from(routeData.entries()));
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
// Register plugin
|
|
1267
|
+
msi.plugin('deliveryAnalytics', deliveryAnalyticsPlugin);
|
|
1268
|
+
|
|
1269
|
+
// Use plugin after MSI loads
|
|
1270
|
+
const { plugins } = await msi.load();
|
|
1271
|
+
const analytics = plugins.use('deliveryAnalytics', {
|
|
1272
|
+
apiKey: 'your-api-key',
|
|
1273
|
+
debug: true
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
await analytics.trackRoute('route-123');
|
|
1277
|
+
```
|
|
1278
|
+
|
|
1279
|
+
#### Plugin Methods
|
|
1280
|
+
|
|
1281
|
+
**`plugins.mount(list: Record<string, Plugin<any>>)`**
|
|
1282
|
+
|
|
1283
|
+
Mount multiple plugins at once.
|
|
1284
|
+
|
|
1285
|
+
```typescript
|
|
1286
|
+
plugins.mount({
|
|
1287
|
+
analytics: deliveryAnalyticsPlugin,
|
|
1288
|
+
notifications: deliveryNotificationsPlugin,
|
|
1289
|
+
customMarkers: customMarkersPlugin
|
|
1290
|
+
});
|
|
1291
|
+
```
|
|
1292
|
+
|
|
1293
|
+
**`plugins.use<API, Options>(name: string, options?: Options): API`**
|
|
1294
|
+
|
|
1295
|
+
Activate and get plugin interface.
|
|
1296
|
+
|
|
1297
|
+
```typescript
|
|
1298
|
+
const analytics = plugins.use('analytics', { debug: true });
|
|
1299
|
+
const notifications = plugins.use('notifications');
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
#### Example: Fleet Management Plugin
|
|
1303
|
+
|
|
1304
|
+
```typescript
|
|
1305
|
+
const fleetManagementPlugin = (hooks, options) => {
|
|
1306
|
+
const { controls, handles, utils } = hooks;
|
|
1307
|
+
const vehicles = new Map();
|
|
1308
|
+
|
|
1309
|
+
return {
|
|
1310
|
+
async registerVehicle(vehicle) {
|
|
1311
|
+
vehicles.set(vehicle.id, {
|
|
1312
|
+
...vehicle,
|
|
1313
|
+
lastUpdate: Date.now()
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
await controls.addNearbyEntity({
|
|
1317
|
+
id: vehicle.id,
|
|
1318
|
+
type: 'vehicle',
|
|
1319
|
+
position: vehicle.position,
|
|
1320
|
+
caption: {
|
|
1321
|
+
label: vehicle.name,
|
|
1322
|
+
sublabel: `${vehicle.capacity} capacity`
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
},
|
|
1326
|
+
|
|
1327
|
+
async updateVehiclePosition(vehicleId, position) {
|
|
1328
|
+
const vehicle = vehicles.get(vehicleId);
|
|
1329
|
+
if (!vehicle) throw new Error('Vehicle not found');
|
|
1330
|
+
|
|
1331
|
+
vehicle.position = position;
|
|
1332
|
+
vehicle.lastUpdate = Date.now();
|
|
1333
|
+
|
|
1334
|
+
await controls.moveNearbyEntity({
|
|
1335
|
+
id: vehicleId,
|
|
1336
|
+
position
|
|
1337
|
+
});
|
|
1338
|
+
},
|
|
1339
|
+
|
|
1340
|
+
async getVehicleStatus(vehicleId) {
|
|
1341
|
+
const vehicle = vehicles.get(vehicleId);
|
|
1342
|
+
return vehicle || null;
|
|
1343
|
+
},
|
|
1344
|
+
|
|
1345
|
+
async optimizeRoutes(vehicleIds) {
|
|
1346
|
+
// Use controls to calculate optimal routes
|
|
1347
|
+
const routes = [];
|
|
1348
|
+
|
|
1349
|
+
for (const id of vehicleIds) {
|
|
1350
|
+
const vehicle = vehicles.get(id);
|
|
1351
|
+
if (vehicle && vehicle.destination) {
|
|
1352
|
+
routes.push({
|
|
1353
|
+
vehicleId: id,
|
|
1354
|
+
route: await calculateOptimalRoute(
|
|
1355
|
+
vehicle.position,
|
|
1356
|
+
vehicle.destination
|
|
1357
|
+
)
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
return routes;
|
|
1363
|
+
},
|
|
1364
|
+
|
|
1365
|
+
getFleetSummary() {
|
|
1366
|
+
return {
|
|
1367
|
+
totalVehicles: vehicles.size,
|
|
1368
|
+
activeVehicles: Array.from(vehicles.values()).filter(
|
|
1369
|
+
v => Date.now() - v.lastUpdate < 60000
|
|
1370
|
+
).length
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
// Usage
|
|
1377
|
+
msi.plugin('fleetManagement', fleetManagementPlugin);
|
|
1378
|
+
const { plugins } = await msi.load();
|
|
1379
|
+
const fleet = plugins.use('fleetManagement');
|
|
1380
|
+
|
|
1381
|
+
await fleet.registerVehicle({
|
|
1382
|
+
id: 'van-1',
|
|
1383
|
+
name: 'Delivery Van #101',
|
|
1384
|
+
position: [40.7128, -74.0060],
|
|
1385
|
+
capacity: '1500kg'
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
const summary = fleet.getFleetSummary();
|
|
1389
|
+
console.log('Fleet summary:', summary);
|
|
1390
|
+
```
|
|
1391
|
+
|
|
1392
|
+
---
|
|
1393
|
+
|
|
1394
|
+
### 5. Access
|
|
1395
|
+
|
|
1396
|
+
Manages authentication and HTTP requests to De. platform API.
|
|
1397
|
+
|
|
1398
|
+
#### Constructor
|
|
1399
|
+
|
|
1400
|
+
```typescript
|
|
1401
|
+
const access = new De.Access({
|
|
1402
|
+
workspace: 'your-workspace-id', // Required
|
|
1403
|
+
accessToken: 'your-access-token', // Required
|
|
1404
|
+
remoteOrigin: window.location.origin,
|
|
1405
|
+
env: 'prod', // 'dev' | 'prod'
|
|
1406
|
+
version: 1 // API version
|
|
1407
|
+
});
|
|
1408
|
+
```
|
|
1409
|
+
|
|
1410
|
+
#### Making Requests
|
|
1411
|
+
|
|
1412
|
+
**`request<Response>(options: HTTPRequestOptions): Promise<Response>`**
|
|
1413
|
+
|
|
1414
|
+
Execute authenticated HTTP request.
|
|
1415
|
+
|
|
1416
|
+
```typescript
|
|
1417
|
+
// GET request
|
|
1418
|
+
const vehicles = await access.request<VehicleResponse[]>({
|
|
1419
|
+
url: '/fleet/vehicles',
|
|
1420
|
+
method: 'GET'
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
// POST request
|
|
1424
|
+
const order = await access.request<OrderResponse>({
|
|
1425
|
+
url: '/orders',
|
|
1426
|
+
method: 'POST',
|
|
1427
|
+
body: {
|
|
1428
|
+
pickupLocation: [40.7128, -74.0060],
|
|
1429
|
+
deliveryLocation: [40.7580, -73.9855],
|
|
1430
|
+
items: [
|
|
1431
|
+
{ name: 'Package A', weight: 5 }
|
|
1432
|
+
]
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
// PUT request
|
|
1437
|
+
const updated = await access.request({
|
|
1438
|
+
url: '/orders/order-123',
|
|
1439
|
+
method: 'PUT',
|
|
1440
|
+
body: { status: 'in-transit' }
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
// DELETE request
|
|
1444
|
+
await access.request({
|
|
1445
|
+
url: '/orders/order-123',
|
|
1446
|
+
method: 'DELETE'
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
// Custom headers
|
|
1450
|
+
const data = await access.request({
|
|
1451
|
+
url: '/analytics/reports',
|
|
1452
|
+
method: 'GET',
|
|
1453
|
+
headers: {
|
|
1454
|
+
'De-Report-Format': 'json',
|
|
1455
|
+
'De-Include-Metrics': 'true'
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
```
|
|
1459
|
+
|
|
1460
|
+
#### Token Management
|
|
1461
|
+
|
|
1462
|
+
**`setToken(token: string): void`**
|
|
1463
|
+
|
|
1464
|
+
Update access token (e.g., after refresh).
|
|
1465
|
+
|
|
1466
|
+
```typescript
|
|
1467
|
+
// Refresh token flow
|
|
1468
|
+
const newToken = await refreshAuthToken();
|
|
1469
|
+
access.setToken(newToken);
|
|
1470
|
+
|
|
1471
|
+
// Also update MSI if needed
|
|
1472
|
+
controls.refreshToken(newToken);
|
|
1473
|
+
```
|
|
1474
|
+
|
|
1475
|
+
#### Request Configuration
|
|
1476
|
+
|
|
1477
|
+
```typescript
|
|
1478
|
+
interface HTTPRequestOptions {
|
|
1479
|
+
url: string; // API endpoint
|
|
1480
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
1481
|
+
body?: any; // Auto-stringified if object
|
|
1482
|
+
headers?: Record<string, string>; // Custom headers
|
|
1483
|
+
}
|
|
1484
|
+
```
|
|
1485
|
+
|
|
1486
|
+
#### Best Practices
|
|
1487
|
+
|
|
1488
|
+
```typescript
|
|
1489
|
+
class DeliveryService {
|
|
1490
|
+
private access: De.Access;
|
|
1491
|
+
|
|
1492
|
+
constructor(config) {
|
|
1493
|
+
this.access = new De.Access(config);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
async createOrder(orderData) {
|
|
1497
|
+
try {
|
|
1498
|
+
const order = await this.access.request({
|
|
1499
|
+
url: '/orders',
|
|
1500
|
+
method: 'POST',
|
|
1501
|
+
body: orderData
|
|
1502
|
+
});
|
|
1503
|
+
|
|
1504
|
+
return order;
|
|
1505
|
+
} catch (error) {
|
|
1506
|
+
console.error('Order creation failed:', error);
|
|
1507
|
+
throw error;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
async getOrderStatus(orderId) {
|
|
1512
|
+
return await this.access.request({
|
|
1513
|
+
url: `/orders/${orderId}/status`,
|
|
1514
|
+
method: 'GET'
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
async updateOrderLocation(orderId, location) {
|
|
1519
|
+
return await this.access.request({
|
|
1520
|
+
url: `/orders/${orderId}/location`,
|
|
1521
|
+
method: 'PATCH',
|
|
1522
|
+
body: { location }
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
```
|
|
1527
|
+
|
|
1528
|
+
---
|
|
1529
|
+
|
|
1530
|
+
### 6. DClient
|
|
1531
|
+
|
|
1532
|
+
Client-side operations for orders, events, and customer management (separate from MSI).
|
|
1533
|
+
|
|
1534
|
+
```typescript
|
|
1535
|
+
import { DClient } from '@de./sdk';
|
|
1536
|
+
|
|
1537
|
+
const { Client, Order, Event } = DClient;
|
|
1538
|
+
|
|
1539
|
+
// Client management
|
|
1540
|
+
const client = new Client(accessConfig);
|
|
1541
|
+
|
|
1542
|
+
// Order management
|
|
1543
|
+
const order = new Order(accessConfig);
|
|
1544
|
+
|
|
1545
|
+
// Event tracking
|
|
1546
|
+
const event = new Event(accessConfig);
|
|
1547
|
+
```
|
|
1548
|
+
|
|
1549
|
+
**Note**: DClient documentation is maintained separately. See the DClient API reference at https://docs.dedot.io/dclient
|
|
1550
|
+
|
|
1551
|
+
---
|
|
1552
|
+
|
|
1553
|
+
## Code Examples
|
|
1554
|
+
|
|
1555
|
+
### Example 1: Complete E-commerce Delivery Integration
|
|
1556
|
+
|
|
1557
|
+
```typescript
|
|
1558
|
+
import De from '@de./sdk';
|
|
1559
|
+
|
|
1560
|
+
class EcommerceDeliveryIntegration {
|
|
1561
|
+
private msi: De.MSI;
|
|
1562
|
+
private access: De.Access;
|
|
1563
|
+
private controls?: De.Controls;
|
|
1564
|
+
private handles?: De.Handles;
|
|
1565
|
+
|
|
1566
|
+
constructor(config) {
|
|
1567
|
+
this.access = new De.Access(config);
|
|
1568
|
+
this.msi = new De.MSI(config);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
async initialize() {
|
|
1572
|
+
const api = await this.msi.load();
|
|
1573
|
+
this.controls = api.controls;
|
|
1574
|
+
this.handles = api.handles;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
async processCheckout(cartItems, customerAddress) {
|
|
1578
|
+
// 1. Create order via API
|
|
1579
|
+
const order = await this.access.request({
|
|
1580
|
+
url: '/orders',
|
|
1581
|
+
method: 'POST',
|
|
1582
|
+
body: {
|
|
1583
|
+
items: cartItems,
|
|
1584
|
+
deliveryAddress: customerAddress,
|
|
1585
|
+
timestamp: Date.now()
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1589
|
+
// 2. Get warehouse location
|
|
1590
|
+
const warehouse = await this.access.request({
|
|
1591
|
+
url: '/warehouses/nearest',
|
|
1592
|
+
method: 'POST',
|
|
1593
|
+
body: { address: customerAddress }
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
// 3. Display route on map
|
|
1597
|
+
await this.handles.pickupPoint(
|
|
1598
|
+
warehouse.location,
|
|
1599
|
+
{
|
|
1600
|
+
label: warehouse.name,
|
|
1601
|
+
description: `Order #${order.id} origin`
|
|
1602
|
+
}
|
|
1603
|
+
);
|
|
1604
|
+
|
|
1605
|
+
await this.handles.dropoffPoint(
|
|
1606
|
+
customerAddress.coordinates,
|
|
1607
|
+
{
|
|
1608
|
+
label: 'Your Address',
|
|
1609
|
+
sublabel: customerAddress.street,
|
|
1610
|
+
description: `Delivery for order #${order.id}`
|
|
1611
|
+
}
|
|
1612
|
+
);
|
|
1613
|
+
|
|
1614
|
+
// 4. Assign delivery vehicle
|
|
1615
|
+
const vehicle = await this.assignVehicle(order.id);
|
|
1616
|
+
|
|
1617
|
+
// 5. Track vehicle in real-time
|
|
1618
|
+
const trackingStream = this.trackDeliveryVehicle(vehicle);
|
|
1619
|
+
|
|
1620
|
+
return {
|
|
1621
|
+
order,
|
|
1622
|
+
warehouse,
|
|
1623
|
+
vehicle,
|
|
1624
|
+
trackingStream
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
async assignVehicle(orderId) {
|
|
1629
|
+
return await this.access.request({
|
|
1630
|
+
url: `/orders/${orderId}/assign-vehicle`,
|
|
1631
|
+
method: 'POST'
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
trackDeliveryVehicle(vehicle) {
|
|
1636
|
+
const vehicleStream = this.handles.peerLocation(
|
|
1637
|
+
vehicle.currentLocation,
|
|
1638
|
+
{
|
|
1639
|
+
label: 'Your Delivery',
|
|
1640
|
+
sublabel: `Driver: ${vehicle.driverName}`,
|
|
1641
|
+
description: `${vehicle.vehicleType} - ETA: ${vehicle.eta}`
|
|
1642
|
+
}
|
|
1643
|
+
);
|
|
1644
|
+
|
|
1645
|
+
// Connect to real-time updates
|
|
1646
|
+
const ws = new WebSocket(`wss://api.dedot.io/vehicles/${vehicle.id}`);
|
|
1647
|
+
|
|
1648
|
+
ws.onmessage = (event) => {
|
|
1649
|
+
const update = JSON.parse(event.data);
|
|
1650
|
+
vehicleStream.write({
|
|
1651
|
+
position: update.location,
|
|
1652
|
+
caption: {
|
|
1653
|
+
label: 'Your Delivery',
|
|
1654
|
+
sublabel: `Driver: ${vehicle.driverName}`,
|
|
1655
|
+
description: `${update.distance} away - ETA: ${update.eta}`
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
};
|
|
1659
|
+
|
|
1660
|
+
// Listen for delivery events
|
|
1661
|
+
this.handles.on('pe:nearby', () => {
|
|
1662
|
+
this.notifyCustomer('Driver is nearby!', 'warning');
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
this.handles.on('pe:arrived', () => {
|
|
1666
|
+
this.notifyCustomer('Driver has arrived!', 'success');
|
|
1667
|
+
vehicleStream.close();
|
|
1668
|
+
ws.close();
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
return vehicleStream;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
notifyCustomer(message, type) {
|
|
1675
|
+
// Send push notification, SMS, etc.
|
|
1676
|
+
console.log(`[${type.toUpperCase()}] ${message}`);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// Usage
|
|
1681
|
+
const delivery = new EcommerceDeliveryIntegration({
|
|
1682
|
+
element: 'map-container',
|
|
1683
|
+
accessToken: 'your-token',
|
|
1684
|
+
workspace: 'your-workspace',
|
|
1685
|
+
env: 'prod'
|
|
1686
|
+
});
|
|
1687
|
+
|
|
1688
|
+
await delivery.initialize();
|
|
1689
|
+
|
|
1690
|
+
const result = await delivery.processCheckout(
|
|
1691
|
+
[{ id: 'item-1', name: 'Product A', quantity: 2 }],
|
|
1692
|
+
{
|
|
1693
|
+
street: '456 Park Ave, Apt 12B',
|
|
1694
|
+
coordinates: [40.7580, -73.9855]
|
|
1695
|
+
}
|
|
1696
|
+
);
|
|
1697
|
+
|
|
1698
|
+
console.log('Order created:', result.order.id);
|
|
1699
|
+
```
|
|
1700
|
+
|
|
1701
|
+
### Example 2: Fleet Management Dashboard
|
|
1702
|
+
|
|
1703
|
+
```typescript
|
|
1704
|
+
class FleetManagementDashboard {
|
|
1705
|
+
private msi: De.MSI;
|
|
1706
|
+
private access: De.Access;
|
|
1707
|
+
private fleetStream?: any;
|
|
1708
|
+
private wsConnection?: WebSocket;
|
|
1709
|
+
|
|
1710
|
+
constructor(config) {
|
|
1711
|
+
this.msi = new De.MSI(config);
|
|
1712
|
+
this.access = new De.Access(config);
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
async initialize() {
|
|
1716
|
+
const { controls, handles } = await this.msi.load();
|
|
1717
|
+
|
|
1718
|
+
// Get all active vehicles from API
|
|
1719
|
+
const { vehicles } = await this.access.request({
|
|
1720
|
+
url: '/fleet/active',
|
|
1721
|
+
method: 'GET'
|
|
1722
|
+
});
|
|
1723
|
+
|
|
1724
|
+
// Display vehicles on map
|
|
1725
|
+
this.fleetStream = handles.nearby(
|
|
1726
|
+
vehicles.map(v => ({
|
|
1727
|
+
id: v.id,
|
|
1728
|
+
type: v.vehicleType,
|
|
1729
|
+
position: v.currentLocation,
|
|
1730
|
+
caption: {
|
|
1731
|
+
label: v.name,
|
|
1732
|
+
sublabel: `Driver: ${v.driverName}`,
|
|
1733
|
+
description: `Status: ${v.status} | Speed: ${v.speed}km/h`
|
|
1734
|
+
}
|
|
1735
|
+
}))
|
|
1736
|
+
);
|
|
1737
|
+
|
|
1738
|
+
// Setup real-time updates
|
|
1739
|
+
this.setupRealtimeTracking();
|
|
1740
|
+
|
|
1741
|
+
// Monitor fleet changes
|
|
1742
|
+
this.fleetStream.pipe(update => {
|
|
1743
|
+
this.updateDashboardStats(update.list);
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
return { controls, handles };
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
setupRealtimeTracking() {
|
|
1750
|
+
this.wsConnection = new WebSocket('wss://api.dedot.io/fleet/live');
|
|
1751
|
+
|
|
1752
|
+
this.wsConnection.onmessage = async (event) => {
|
|
1753
|
+
const update = JSON.parse(event.data);
|
|
1754
|
+
|
|
1755
|
+
this.fleetStream.live(async controls => {
|
|
1756
|
+
switch (update.type) {
|
|
1757
|
+
case 'position_update':
|
|
1758
|
+
await controls.move({
|
|
1759
|
+
id: update.vehicleId,
|
|
1760
|
+
position: update.location
|
|
1761
|
+
});
|
|
1762
|
+
break;
|
|
1763
|
+
|
|
1764
|
+
case 'vehicle_online':
|
|
1765
|
+
await controls.add({
|
|
1766
|
+
id: update.vehicle.id,
|
|
1767
|
+
type: update.vehicle.type,
|
|
1768
|
+
position: update.vehicle.location,
|
|
1769
|
+
caption: {
|
|
1770
|
+
label: update.vehicle.name,
|
|
1771
|
+
sublabel: `Driver: ${update.vehicle.driverName}`
|
|
1772
|
+
}
|
|
1773
|
+
});
|
|
1774
|
+
break;
|
|
1775
|
+
|
|
1776
|
+
case 'vehicle_offline':
|
|
1777
|
+
await controls.remove(update.vehicleId);
|
|
1778
|
+
break;
|
|
1779
|
+
}
|
|
1780
|
+
});
|
|
1781
|
+
};
|
|
1782
|
+
|
|
1783
|
+
this.wsConnection.onerror = () => {
|
|
1784
|
+
console.error('WebSocket connection lost, reconnecting...');
|
|
1785
|
+
setTimeout(() => this.setupRealtimeTracking(), 5000);
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
updateDashboardStats(vehicles) {
|
|
1790
|
+
const stats = {
|
|
1791
|
+
total: vehicles.length,
|
|
1792
|
+
active: vehicles.filter(v => v.status === 'active').length,
|
|
1793
|
+
idle: vehicles.filter(v => v.status === 'idle').length,
|
|
1794
|
+
offline: vehicles.filter(v => v.status === 'offline').length
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1797
|
+
// Update UI
|
|
1798
|
+
this.renderDashboardStats(stats);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
renderDashboardStats(stats) {
|
|
1802
|
+
document.getElementById('total-vehicles').textContent = stats.total;
|
|
1803
|
+
document.getElementById('active-vehicles').textContent = stats.active;
|
|
1804
|
+
document.getElementById('idle-vehicles').textContent = stats.idle;
|
|
1805
|
+
document.getElementById('offline-vehicles').textContent = stats.offline;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
async optimizeRoutes() {
|
|
1809
|
+
const { routes } = await this.access.request({
|
|
1810
|
+
url: '/fleet/optimize-routes',
|
|
1811
|
+
method: 'POST'
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
console.log('Routes optimized:', routes);
|
|
1815
|
+
return routes;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
cleanup() {
|
|
1819
|
+
this.wsConnection?.close();
|
|
1820
|
+
this.fleetStream?.close();
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// Usage
|
|
1825
|
+
const dashboard = new FleetManagementDashboard({
|
|
1826
|
+
element: 'map-container',
|
|
1827
|
+
accessToken: 'your-token',
|
|
1828
|
+
workspace: 'your-workspace'
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1831
|
+
await dashboard.initialize();
|
|
1832
|
+
```
|
|
1833
|
+
|
|
1834
|
+
### Example 3: Multi-Stop Route Planning
|
|
1835
|
+
|
|
1836
|
+
```typescript
|
|
1837
|
+
async function planMultiStopDeliveryRoute() {
|
|
1838
|
+
const msi = new De.MSI({
|
|
1839
|
+
element: 'map-container',
|
|
1840
|
+
accessToken: 'your-token',
|
|
1841
|
+
workspace: 'your-workspace'
|
|
1842
|
+
});
|
|
1843
|
+
|
|
1844
|
+
const { controls, handles } = await msi.load();
|
|
1845
|
+
|
|
1846
|
+
// Define delivery stops
|
|
1847
|
+
const stops = [
|
|
1848
|
+
{
|
|
1849
|
+
address: 'Warehouse - 123 Industrial Blvd',
|
|
1850
|
+
coords: [40.7128, -74.0060],
|
|
1851
|
+
type: 'pickup',
|
|
1852
|
+
items: ['Package A', 'Package B', 'Package C']
|
|
1853
|
+
},
|
|
1854
|
+
{
|
|
1855
|
+
address: 'Customer 1 - 456 Park Ave',
|
|
1856
|
+
coords: [40.7489, -73.9680],
|
|
1857
|
+
type: 'delivery',
|
|
1858
|
+
items: ['Package A']
|
|
1859
|
+
},
|
|
1860
|
+
{
|
|
1861
|
+
address: 'Customer 2 - 789 Broadway',
|
|
1862
|
+
coords: [40.7300, -73.9950],
|
|
1863
|
+
type: 'delivery',
|
|
1864
|
+
items: ['Package B']
|
|
1865
|
+
},
|
|
1866
|
+
{
|
|
1867
|
+
address: 'Customer 3 - 321 5th Ave',
|
|
1868
|
+
coords: [40.7580, -73.9855],
|
|
1869
|
+
type: 'delivery',
|
|
1870
|
+
items: ['Package C']
|
|
1871
|
+
}
|
|
1872
|
+
];
|
|
1873
|
+
|
|
1874
|
+
// Create route with all stops
|
|
1875
|
+
await controls.setRoute({
|
|
1876
|
+
routeId: 'multi-stop-delivery',
|
|
1877
|
+
origin: {
|
|
1878
|
+
coords: stops[0].coords,
|
|
1879
|
+
caption: {
|
|
1880
|
+
label: stops[0].address,
|
|
1881
|
+
sublabel: 'Pickup Point',
|
|
1882
|
+
description: stops[0].items.join(', ')
|
|
1883
|
+
}
|
|
1884
|
+
},
|
|
1885
|
+
destination: {
|
|
1886
|
+
coords: stops[stops.length - 1].coords,
|
|
1887
|
+
caption: {
|
|
1888
|
+
label: stops[stops.length - 1].address,
|
|
1889
|
+
sublabel: 'Final Delivery',
|
|
1890
|
+
description: `Items: ${stops[stops.length - 1].items.join(', ')}`
|
|
1891
|
+
}
|
|
1892
|
+
},
|
|
1893
|
+
waypoints: stops.slice(1, -1).map((stop, index) => ({
|
|
1894
|
+
coords: stop.coords,
|
|
1895
|
+
caption: {
|
|
1896
|
+
label: `Stop ${index + 1}`,
|
|
1897
|
+
sublabel: stop.address,
|
|
1898
|
+
description: `Deliver: ${stop.items.join(', ')}`
|
|
1899
|
+
},
|
|
1900
|
+
index
|
|
1901
|
+
})),
|
|
1902
|
+
options: {
|
|
1903
|
+
mode: 'driving',
|
|
1904
|
+
optimize: true,
|
|
1905
|
+
avoidTolls: false
|
|
1906
|
+
}
|
|
1907
|
+
});
|
|
1908
|
+
|
|
1909
|
+
// Fit all stops in view
|
|
1910
|
+
await controls.fitRouteBounds('multi-stop-delivery', 80);
|
|
1911
|
+
|
|
1912
|
+
// Start navigation
|
|
1913
|
+
const navStream = await handles.navigation({
|
|
1914
|
+
routeId: 'multi-stop-delivery',
|
|
1915
|
+
origin: {
|
|
1916
|
+
coords: stops[0].coords,
|
|
1917
|
+
caption: { label: 'Start' }
|
|
1918
|
+
},
|
|
1919
|
+
destination: {
|
|
1920
|
+
coords: stops[stops.length - 1].coords,
|
|
1921
|
+
caption: { label: 'End' }
|
|
1922
|
+
},
|
|
1923
|
+
waypoints: stops.slice(1, -1).map((stop, index) => ({
|
|
1924
|
+
coords: stop.coords,
|
|
1925
|
+
caption: { label: `Stop ${index + 1}` },
|
|
1926
|
+
index
|
|
1927
|
+
}))
|
|
1928
|
+
});
|
|
1929
|
+
|
|
1930
|
+
// Track progress
|
|
1931
|
+
let currentStop = 0;
|
|
1932
|
+
handles.on('pe:arrived', async () => {
|
|
1933
|
+
currentStop++;
|
|
1934
|
+
console.log(`Arrived at stop ${currentStop} of ${stops.length}`);
|
|
1935
|
+
|
|
1936
|
+
if (currentStop < stops.length) {
|
|
1937
|
+
// Update waypoint caption to mark as completed
|
|
1938
|
+
await controls.updateWaypointCaption(
|
|
1939
|
+
'multi-stop-delivery',
|
|
1940
|
+
currentStop - 1,
|
|
1941
|
+
{
|
|
1942
|
+
label: `Stop ${currentStop} - Completed ✓`,
|
|
1943
|
+
sublabel: stops[currentStop].address
|
|
1944
|
+
}
|
|
1945
|
+
);
|
|
1946
|
+
} else {
|
|
1947
|
+
console.log('All deliveries completed!');
|
|
1948
|
+
navStream.close();
|
|
1949
|
+
}
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
return { navStream, stops };
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// Usage
|
|
1956
|
+
const { navStream, stops } = await planMultiStopDeliveryRoute();
|
|
1957
|
+
console.log(`Route planned with ${stops.length} stops`);
|
|
1958
|
+
```
|
|
1959
|
+
|
|
1960
|
+
### Example 4: Real-time Order Tracking for Customers
|
|
1961
|
+
|
|
1962
|
+
```typescript
|
|
1963
|
+
class CustomerOrderTracking {
|
|
1964
|
+
private msi: De.MSI;
|
|
1965
|
+
private access: De.Access;
|
|
1966
|
+
|
|
1967
|
+
constructor(config) {
|
|
1968
|
+
this.msi = new De.MSI(config);
|
|
1969
|
+
this.access = new De.Access(config);
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
async trackOrder(orderId: string) {
|
|
1973
|
+
// Initialize MSI
|
|
1974
|
+
const { controls, handles } = await this.msi.load();
|
|
1975
|
+
|
|
1976
|
+
// Get order details
|
|
1977
|
+
const order = await this.access.request({
|
|
1978
|
+
url: `/orders/${orderId}`,
|
|
1979
|
+
method: 'GET'
|
|
1980
|
+
});
|
|
1981
|
+
|
|
1982
|
+
// Show delivery route
|
|
1983
|
+
await handles.pickupPoint(
|
|
1984
|
+
order.pickupLocation,
|
|
1985
|
+
{ label: 'Pickup', description: order.originName }
|
|
1986
|
+
);
|
|
1987
|
+
|
|
1988
|
+
await handles.dropoffPoint(
|
|
1989
|
+
order.deliveryLocation,
|
|
1990
|
+
{ label: 'Your Address', description: order.deliveryAddress }
|
|
1991
|
+
);
|
|
1992
|
+
|
|
1993
|
+
// Show delivery vehicle if assigned
|
|
1994
|
+
if (order.vehicleId) {
|
|
1995
|
+
const vehicle = await this.access.request({
|
|
1996
|
+
url: `/vehicles/${order.vehicleId}`,
|
|
1997
|
+
method: 'GET'
|
|
1998
|
+
});
|
|
1999
|
+
|
|
2000
|
+
// Track driver's location
|
|
2001
|
+
const driverStream = handles.peerLocation(
|
|
2002
|
+
vehicle.currentLocation,
|
|
2003
|
+
{
|
|
2004
|
+
label: 'Your Driver',
|
|
2005
|
+
sublabel: `${vehicle.driverName} - ${vehicle.vehicleModel}`,
|
|
2006
|
+
description: `ETA: ${vehicle.eta}`
|
|
2007
|
+
}
|
|
2008
|
+
);
|
|
2009
|
+
|
|
2010
|
+
// Show driver's navigation
|
|
2011
|
+
const driverNavStream = handles.peerDirection({
|
|
2012
|
+
mode: 'driving'
|
|
2013
|
+
});
|
|
2014
|
+
|
|
2015
|
+
// Connect to real-time updates
|
|
2016
|
+
const ws = new WebSocket(`wss://api.dedot.io/orders/${orderId}/live`);
|
|
2017
|
+
|
|
2018
|
+
ws.onmessage = (event) => {
|
|
2019
|
+
const update = JSON.parse(event.data);
|
|
2020
|
+
|
|
2021
|
+
// Update driver position
|
|
2022
|
+
driverStream.write({
|
|
2023
|
+
position: update.vehicleLocation,
|
|
2024
|
+
caption: {
|
|
2025
|
+
label: 'Your Driver',
|
|
2026
|
+
sublabel: `${vehicle.driverName} - ${vehicle.vehicleModel}`,
|
|
2027
|
+
description: `${update.distanceToDelivery} away - ETA: ${update.eta}`
|
|
2028
|
+
}
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
// Update driver's navigation
|
|
2032
|
+
if (update.navigationData) {
|
|
2033
|
+
driverNavStream.write({
|
|
2034
|
+
status: update.status,
|
|
2035
|
+
direction: update.navigationData,
|
|
2036
|
+
position: update.vehicleLocation
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
|
|
2041
|
+
// Listen for proximity alerts
|
|
2042
|
+
handles.on('pe:nearby', () => {
|
|
2043
|
+
this.showNotification('Driver is nearby! Prepare to receive your delivery.');
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
handles.on('pe:arrived', () => {
|
|
2047
|
+
this.showNotification('Driver has arrived at your location!');
|
|
2048
|
+
ws.close();
|
|
2049
|
+
driverStream.close();
|
|
2050
|
+
driverNavStream.close();
|
|
2051
|
+
});
|
|
2052
|
+
|
|
2053
|
+
return {
|
|
2054
|
+
order,
|
|
2055
|
+
vehicle,
|
|
2056
|
+
streams: { driverStream, driverNavStream },
|
|
2057
|
+
websocket: ws
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
return { order };
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
showNotification(message: string) {
|
|
2065
|
+
// Show browser notification
|
|
2066
|
+
if ('Notification' in window && Notification.permission === 'granted') {
|
|
2067
|
+
new Notification('De. Delivery', {
|
|
2068
|
+
body: message,
|
|
2069
|
+
icon: '/delivery-icon.png'
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
// Also show in-app notification
|
|
2074
|
+
const notification = document.createElement('div');
|
|
2075
|
+
notification.className = 'notification';
|
|
2076
|
+
notification.textContent = message;
|
|
2077
|
+
document.body.appendChild(notification);
|
|
2078
|
+
|
|
2079
|
+
setTimeout(() => notification.remove(), 5000);
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
// Usage
|
|
2084
|
+
const tracking = new CustomerOrderTracking({
|
|
2085
|
+
element: 'map-container',
|
|
2086
|
+
accessToken: 'customer-token',
|
|
2087
|
+
workspace: 'your-workspace'
|
|
2088
|
+
});
|
|
2089
|
+
|
|
2090
|
+
const result = await tracking.trackOrder('order-12345');
|
|
2091
|
+
console.log('Tracking order:', result.order.id);
|
|
2092
|
+
```
|
|
2093
|
+
|
|
2094
|
+
### Example 5: Warehouse Management Plugin
|
|
2095
|
+
|
|
2096
|
+
```typescript
|
|
2097
|
+
const warehouseManagementPlugin = (hooks, options) => {
|
|
2098
|
+
const { controls, handles, utils } = hooks;
|
|
2099
|
+
const warehouses = new Map();
|
|
2100
|
+
const inventory = new Map();
|
|
2101
|
+
|
|
2102
|
+
return {
|
|
2103
|
+
async registerWarehouse(warehouse) {
|
|
2104
|
+
warehouses.set(warehouse.id, warehouse);
|
|
2105
|
+
|
|
2106
|
+
await controls.addNearbyEntity({
|
|
2107
|
+
id: warehouse.id,
|
|
2108
|
+
type: 'warehouse',
|
|
2109
|
+
position: warehouse.location,
|
|
2110
|
+
caption: {
|
|
2111
|
+
label: warehouse.name,
|
|
2112
|
+
sublabel: `Capacity: ${warehouse.capacity}`,
|
|
2113
|
+
description: `Items: ${warehouse.currentStock}`
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
|
|
2117
|
+
return warehouse.id;
|
|
2118
|
+
},
|
|
2119
|
+
|
|
2120
|
+
async updateInventory(warehouseId, items) {
|
|
2121
|
+
inventory.set(warehouseId, items);
|
|
2122
|
+
|
|
2123
|
+
const warehouse = warehouses.get(warehouseId);
|
|
2124
|
+
if (warehouse) {
|
|
2125
|
+
await controls.moveNearbyEntity({
|
|
2126
|
+
id: warehouseId,
|
|
2127
|
+
position: warehouse.location
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
},
|
|
2131
|
+
|
|
2132
|
+
async findNearestWarehouse(location, itemsNeeded) {
|
|
2133
|
+
let nearest = null;
|
|
2134
|
+
let minDistance = Infinity;
|
|
2135
|
+
|
|
2136
|
+
for (const [id, warehouse] of warehouses) {
|
|
2137
|
+
const items = inventory.get(id) || [];
|
|
2138
|
+
const hasAllItems = itemsNeeded.every(item =>
|
|
2139
|
+
items.some(i => i.id === item.id && i.quantity >= item.quantity)
|
|
2140
|
+
);
|
|
2141
|
+
|
|
2142
|
+
if (hasAllItems) {
|
|
2143
|
+
const distance = utils.calculateDistance(
|
|
2144
|
+
location,
|
|
2145
|
+
warehouse.location
|
|
2146
|
+
);
|
|
2147
|
+
|
|
2148
|
+
if (distance < minDistance) {
|
|
2149
|
+
minDistance = distance;
|
|
2150
|
+
nearest = { warehouse, distance };
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
return nearest;
|
|
2156
|
+
},
|
|
2157
|
+
|
|
2158
|
+
async optimizeStockDistribution() {
|
|
2159
|
+
const distributionPlan = [];
|
|
2160
|
+
|
|
2161
|
+
for (const [id, warehouse] of warehouses) {
|
|
2162
|
+
const items = inventory.get(id) || [];
|
|
2163
|
+
const lowStock = items.filter(i => i.quantity < i.minQuantity);
|
|
2164
|
+
|
|
2165
|
+
if (lowStock.length > 0) {
|
|
2166
|
+
distributionPlan.push({
|
|
2167
|
+
warehouseId: id,
|
|
2168
|
+
warehouse: warehouse.name,
|
|
2169
|
+
needsRestocking: lowStock
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
return distributionPlan;
|
|
2175
|
+
},
|
|
2176
|
+
|
|
2177
|
+
getWarehouseStats() {
|
|
2178
|
+
return {
|
|
2179
|
+
total: warehouses.size,
|
|
2180
|
+
averageCapacity: Array.from(warehouses.values())
|
|
2181
|
+
.reduce((sum, w) => sum + w.capacity, 0) / warehouses.size,
|
|
2182
|
+
totalItems: Array.from(inventory.values())
|
|
2183
|
+
.reduce((sum, items) => sum + items.length, 0)
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
};
|
|
2188
|
+
|
|
2189
|
+
// Usage
|
|
2190
|
+
msi.plugin('warehouseManagement', warehouseManagementPlugin);
|
|
2191
|
+
|
|
2192
|
+
const { plugins } = await msi.load();
|
|
2193
|
+
const wms = plugins.use('warehouseManagement');
|
|
2194
|
+
|
|
2195
|
+
// Register warehouses
|
|
2196
|
+
await wms.registerWarehouse({
|
|
2197
|
+
id: 'wh-001',
|
|
2198
|
+
name: 'Brooklyn Distribution Center',
|
|
2199
|
+
location: [40.7128, -74.0060],
|
|
2200
|
+
capacity: 10000,
|
|
2201
|
+
currentStock: 7500
|
|
2202
|
+
});
|
|
2203
|
+
|
|
2204
|
+
// Find nearest warehouse with items
|
|
2205
|
+
const nearest = await wms.findNearestWarehouse(
|
|
2206
|
+
[40.7580, -73.9855],
|
|
2207
|
+
[{ id: 'item-a', quantity: 5 }]
|
|
2208
|
+
);
|
|
2209
|
+
|
|
2210
|
+
console.log('Nearest warehouse:', nearest.warehouse.name);
|
|
2211
|
+
console.log('Distance:', nearest.distance, 'km');
|
|
2212
|
+
```
|
|
2213
|
+
|
|
2214
|
+
---
|
|
2215
|
+
|
|
2216
|
+
## Advanced Usage Patterns
|
|
2217
|
+
|
|
2218
|
+
### Pattern 1: Dynamic Route Optimization
|
|
2219
|
+
|
|
2220
|
+
```typescript
|
|
2221
|
+
class DynamicRouteOptimizer {
|
|
2222
|
+
private controls: De.Controls;
|
|
2223
|
+
private access: De.Access;
|
|
2224
|
+
|
|
2225
|
+
constructor(controls: De.Controls, access: De.Access) {
|
|
2226
|
+
this.controls = controls;
|
|
2227
|
+
this.access = access;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
async optimizeDeliveryRoute(deliveries: Delivery[]) {
|
|
2231
|
+
// Get traffic data from API
|
|
2232
|
+
const trafficData = await this.access.request({
|
|
2233
|
+
url: '/traffic/current',
|
|
2234
|
+
method: 'POST',
|
|
2235
|
+
body: {
|
|
2236
|
+
locations: deliveries.map(d => d.location)
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
|
|
2240
|
+
// Sort deliveries by priority and traffic conditions
|
|
2241
|
+
const optimized = this.sortByOptimalSequence(
|
|
2242
|
+
deliveries,
|
|
2243
|
+
trafficData
|
|
2244
|
+
);
|
|
2245
|
+
|
|
2246
|
+
// Create route with optimized stops
|
|
2247
|
+
await this.controls.setRoute({
|
|
2248
|
+
routeId: 'optimized-route',
|
|
2249
|
+
origin: {
|
|
2250
|
+
coords: optimized[0].location,
|
|
2251
|
+
caption: { label: 'Start' }
|
|
2252
|
+
},
|
|
2253
|
+
destination: {
|
|
2254
|
+
coords: optimized[optimized.length - 1].location,
|
|
2255
|
+
caption: { label: 'End' }
|
|
2256
|
+
},
|
|
2257
|
+
waypoints: optimized.slice(1, -1).map((delivery, index) => ({
|
|
2258
|
+
coords: delivery.location,
|
|
2259
|
+
caption: {
|
|
2260
|
+
label: `Stop ${index + 1}`,
|
|
2261
|
+
description: `Priority: ${delivery.priority}`
|
|
2262
|
+
},
|
|
2263
|
+
index
|
|
2264
|
+
})),
|
|
2265
|
+
options: {
|
|
2266
|
+
mode: 'driving',
|
|
2267
|
+
optimize: true,
|
|
2268
|
+
avoidHighTraffic: true
|
|
2269
|
+
}
|
|
2270
|
+
});
|
|
2271
|
+
|
|
2272
|
+
return optimized;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
private sortByOptimalSequence(
|
|
2276
|
+
deliveries: Delivery[],
|
|
2277
|
+
trafficData: any
|
|
2278
|
+
): Delivery[] {
|
|
2279
|
+
// Complex optimization algorithm
|
|
2280
|
+
// Consider: priority, traffic, distance, time windows
|
|
2281
|
+
|
|
2282
|
+
return deliveries.sort((a, b) => {
|
|
2283
|
+
const scoreA = this.calculateDeliveryScore(a, trafficData);
|
|
2284
|
+
const scoreB = this.calculateDeliveryScore(b, trafficData);
|
|
2285
|
+
return scoreB - scoreA; // Higher score = higher priority
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
private calculateDeliveryScore(delivery: Delivery, trafficData: any): number {
|
|
2290
|
+
let score = 0;
|
|
2291
|
+
|
|
2292
|
+
// Priority weight (0-100)
|
|
2293
|
+
score += delivery.priority * 10;
|
|
2294
|
+
|
|
2295
|
+
// Traffic conditions (lower traffic = higher score)
|
|
2296
|
+
const traffic = trafficData.find(t =>
|
|
2297
|
+
t.location[0] === delivery.location[0] &&
|
|
2298
|
+
t.location[1] === delivery.location[1]
|
|
2299
|
+
);
|
|
2300
|
+
if (traffic) {
|
|
2301
|
+
score += (100 - traffic.congestionLevel);
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
// Time window urgency
|
|
2305
|
+
const timeToDeadline = delivery.deadline - Date.now();
|
|
2306
|
+
if (timeToDeadline < 3600000) { // Less than 1 hour
|
|
2307
|
+
score += 50;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
return score;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
// Usage
|
|
2315
|
+
const optimizer = new DynamicRouteOptimizer(controls, access);
|
|
2316
|
+
|
|
2317
|
+
const deliveries = [
|
|
2318
|
+
{ location: [40.7128, -74.0060], priority: 8, deadline: Date.now() + 7200000 },
|
|
2319
|
+
{ location: [40.7580, -73.9855], priority: 10, deadline: Date.now() + 3600000 },
|
|
2320
|
+
{ location: [40.7489, -73.9680], priority: 5, deadline: Date.now() + 14400000 }
|
|
2321
|
+
];
|
|
2322
|
+
|
|
2323
|
+
const optimized = await optimizer.optimizeDeliveryRoute(deliveries);
|
|
2324
|
+
console.log('Optimized delivery sequence:', optimized);
|
|
2325
|
+
```
|
|
2326
|
+
|
|
2327
|
+
### Pattern 2: Real-time Fleet Coordination
|
|
2328
|
+
|
|
2329
|
+
```typescript
|
|
2330
|
+
class FleetCoordinator {
|
|
2331
|
+
private handles: De.Handles;
|
|
2332
|
+
private access: De.Access;
|
|
2333
|
+
private activeVehicles: Map<string, VehicleState> = new Map();
|
|
2334
|
+
private orders: Map<string, Order> = new Map();
|
|
2335
|
+
|
|
2336
|
+
constructor(handles: De.Handles, access: De.Access) {
|
|
2337
|
+
this.handles = handles;
|
|
2338
|
+
this.access = access;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
async initialize() {
|
|
2342
|
+
// Get all active vehicles
|
|
2343
|
+
const { vehicles } = await this.access.request({
|
|
2344
|
+
url: '/fleet/active',
|
|
2345
|
+
method: 'GET'
|
|
2346
|
+
});
|
|
2347
|
+
|
|
2348
|
+
// Display vehicles on map
|
|
2349
|
+
const fleetStream = this.handles.nearby(
|
|
2350
|
+
vehicles.map(v => this.formatVehicleEntity(v))
|
|
2351
|
+
);
|
|
2352
|
+
|
|
2353
|
+
// Setup real-time coordination
|
|
2354
|
+
fleetStream.live(async controls => {
|
|
2355
|
+
this.setupWebSocketConnection(controls);
|
|
2356
|
+
this.monitorVehicleStatus();
|
|
2357
|
+
});
|
|
2358
|
+
|
|
2359
|
+
return fleetStream;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
private formatVehicleEntity(vehicle: any) {
|
|
2363
|
+
return {
|
|
2364
|
+
id: vehicle.id,
|
|
2365
|
+
type: vehicle.type,
|
|
2366
|
+
position: vehicle.currentLocation,
|
|
2367
|
+
caption: {
|
|
2368
|
+
label: vehicle.name,
|
|
2369
|
+
sublabel: `Status: ${vehicle.status}`,
|
|
2370
|
+
description: vehicle.currentOrder
|
|
2371
|
+
? `Delivering order #${vehicle.currentOrder}`
|
|
2372
|
+
: 'Available'
|
|
2373
|
+
}
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
private setupWebSocketConnection(controls: any) {
|
|
2378
|
+
const ws = new WebSocket('wss://api.dedot.io/fleet/coordination');
|
|
2379
|
+
|
|
2380
|
+
ws.onmessage = async (event) => {
|
|
2381
|
+
const message = JSON.parse(event.data);
|
|
2382
|
+
|
|
2383
|
+
switch (message.type) {
|
|
2384
|
+
case 'vehicle_update':
|
|
2385
|
+
await this.handleVehicleUpdate(controls, message.data);
|
|
2386
|
+
break;
|
|
2387
|
+
|
|
2388
|
+
case 'new_order':
|
|
2389
|
+
await this.handleNewOrder(controls, message.data);
|
|
2390
|
+
break;
|
|
2391
|
+
|
|
2392
|
+
case 'order_completed':
|
|
2393
|
+
await this.handleOrderCompletion(controls, message.data);
|
|
2394
|
+
break;
|
|
2395
|
+
}
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
private async handleVehicleUpdate(controls: any, data: any) {
|
|
2400
|
+
this.activeVehicles.set(data.vehicleId, {
|
|
2401
|
+
location: data.location,
|
|
2402
|
+
status: data.status,
|
|
2403
|
+
lastUpdate: Date.now()
|
|
2404
|
+
});
|
|
2405
|
+
|
|
2406
|
+
await controls.move({
|
|
2407
|
+
id: data.vehicleId,
|
|
2408
|
+
position: data.location
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
private async handleNewOrder(controls: any, order: Order) {
|
|
2413
|
+
this.orders.set(order.id, order);
|
|
2414
|
+
|
|
2415
|
+
// Find best vehicle for this order
|
|
2416
|
+
const vehicle = await this.assignBestVehicle(order);
|
|
2417
|
+
|
|
2418
|
+
if (vehicle) {
|
|
2419
|
+
console.log(`Assigned order ${order.id} to vehicle ${vehicle.id}`);
|
|
2420
|
+
|
|
2421
|
+
// Notify vehicle via API
|
|
2422
|
+
await this.access.request({
|
|
2423
|
+
url: `/vehicles/${vehicle.id}/assign-order`,
|
|
2424
|
+
method: 'POST',
|
|
2425
|
+
body: { orderId: order.id }
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
private async handleOrderCompletion(controls: any, data: any) {
|
|
2431
|
+
this.orders.delete(data.orderId);
|
|
2432
|
+
|
|
2433
|
+
// Update vehicle status
|
|
2434
|
+
const vehicle = this.activeVehicles.get(data.vehicleId);
|
|
2435
|
+
if (vehicle) {
|
|
2436
|
+
vehicle.status = 'available';
|
|
2437
|
+
this.activeVehicles.set(data.vehicleId, vehicle);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
private async assignBestVehicle(order: Order): Promise<any | null> {
|
|
2442
|
+
let bestVehicle = null;
|
|
2443
|
+
let minDistance = Infinity;
|
|
2444
|
+
|
|
2445
|
+
for (const [id, state] of this.activeVehicles) {
|
|
2446
|
+
if (state.status === 'available') {
|
|
2447
|
+
const distance = this.calculateDistance(
|
|
2448
|
+
state.location,
|
|
2449
|
+
order.pickupLocation
|
|
2450
|
+
);
|
|
2451
|
+
|
|
2452
|
+
if (distance < minDistance) {
|
|
2453
|
+
minDistance = distance;
|
|
2454
|
+
bestVehicle = { id, ...state };
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
return bestVehicle;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
private monitorVehicleStatus() {
|
|
2463
|
+
setInterval(() => {
|
|
2464
|
+
const now = Date.now();
|
|
2465
|
+
|
|
2466
|
+
for (const [id, state] of this.activeVehicles) {
|
|
2467
|
+
const timeSinceUpdate = now - state.lastUpdate;
|
|
2468
|
+
|
|
2469
|
+
// Alert if vehicle hasn't updated in 5 minutes
|
|
2470
|
+
if (timeSinceUpdate > 300000) {
|
|
2471
|
+
console.warn(`Vehicle ${id} connection lost`);
|
|
2472
|
+
this.alertDispatcher(`Vehicle ${id} offline`);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
}, 60000); // Check every minute
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
private calculateDistance(from: number[], to: number[]): number {
|
|
2479
|
+
const R = 6371; // Earth radius in km
|
|
2480
|
+
const dLat = this.toRad(to[0] - from[0]);
|
|
2481
|
+
const dLon = this.toRad(to[1] - from[1]);
|
|
2482
|
+
|
|
2483
|
+
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
2484
|
+
Math.cos(this.toRad(from[0])) * Math.cos(this.toRad(to[0])) *
|
|
2485
|
+
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
|
2486
|
+
|
|
2487
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
2488
|
+
return R * c;
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
private toRad(deg: number): number {
|
|
2492
|
+
return deg * (Math.PI / 180);
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
private alertDispatcher(message: string) {
|
|
2496
|
+
console.log('[ALERT]', message);
|
|
2497
|
+
// Send notification to dispatcher
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
getFleetMetrics() {
|
|
2501
|
+
const available = Array.from(this.activeVehicles.values())
|
|
2502
|
+
.filter(v => v.status === 'available').length;
|
|
2503
|
+
|
|
2504
|
+
return {
|
|
2505
|
+
totalVehicles: this.activeVehicles.size,
|
|
2506
|
+
available,
|
|
2507
|
+
busy: this.activeVehicles.size - available,
|
|
2508
|
+
pendingOrders: this.orders.size
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// Usage
|
|
2514
|
+
const coordinator = new FleetCoordinator(handles, access);
|
|
2515
|
+
const fleetStream = await coordinator.initialize();
|
|
2516
|
+
|
|
2517
|
+
setInterval(() => {
|
|
2518
|
+
const metrics = coordinator.getFleetMetrics();
|
|
2519
|
+
console.log('Fleet metrics:', metrics);
|
|
2520
|
+
}, 30000); // Every 30 seconds
|
|
2521
|
+
```
|
|
2522
|
+
|
|
2523
|
+
### Pattern 3: Geofencing and Zone Management
|
|
2524
|
+
|
|
2525
|
+
```typescript
|
|
2526
|
+
class GeofenceManager {
|
|
2527
|
+
private controls: De.Controls;
|
|
2528
|
+
private handles: De.Handles;
|
|
2529
|
+
private zones: Map<string, Zone> = new Map();
|
|
2530
|
+
|
|
2531
|
+
constructor(controls: De.Controls, handles: De.Handles) {
|
|
2532
|
+
this.controls = controls;
|
|
2533
|
+
this.handles = handles;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
async createDeliveryZone(zone: Zone) {
|
|
2537
|
+
this.zones.set(zone.id, zone);
|
|
2538
|
+
|
|
2539
|
+
// Display zone on map
|
|
2540
|
+
await this.controls.addNearbyEntity({
|
|
2541
|
+
id: zone.id,
|
|
2542
|
+
type: 'zone',
|
|
2543
|
+
position: zone.center,
|
|
2544
|
+
radius: zone.radius,
|
|
2545
|
+
caption: {
|
|
2546
|
+
label: zone.name,
|
|
2547
|
+
description: `Delivery zone - ${zone.restrictions || 'No restrictions'}`
|
|
2548
|
+
}
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
async monitorVehicleInZones(vehicleId: string) {
|
|
2553
|
+
const locationStream = this.handles.myLocation('agent');
|
|
2554
|
+
|
|
2555
|
+
locationStream.pipe(location => {
|
|
2556
|
+
for (const [zoneId, zone] of this.zones) {
|
|
2557
|
+
const isInside = this.isLocationInZone(location, zone);
|
|
2558
|
+
|
|
2559
|
+
if (isInside && !zone.activeVehicles?.includes(vehicleId)) {
|
|
2560
|
+
this.handleVehicleEnterZone(vehicleId, zone);
|
|
2561
|
+
} else if (!isInside && zone.activeVehicles?.includes(vehicleId)) {
|
|
2562
|
+
this.handleVehicleExitZone(vehicleId, zone);
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
});
|
|
2566
|
+
|
|
2567
|
+
return locationStream;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
private isLocationInZone(location: RTLocation, zone: Zone): boolean {
|
|
2571
|
+
const distance = this.calculateDistance(
|
|
2572
|
+
[location.latitude, location.longitude],
|
|
2573
|
+
zone.center
|
|
2574
|
+
);
|
|
2575
|
+
|
|
2576
|
+
return distance <= zone.radius;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
private handleVehicleEnterZone(vehicleId: string, zone: Zone) {
|
|
2580
|
+
console.log(`Vehicle ${vehicleId} entered zone ${zone.name}`);
|
|
2581
|
+
|
|
2582
|
+
if (!zone.activeVehicles) {
|
|
2583
|
+
zone.activeVehicles = [];
|
|
2584
|
+
}
|
|
2585
|
+
zone.activeVehicles.push(vehicleId);
|
|
2586
|
+
|
|
2587
|
+
// Apply zone restrictions
|
|
2588
|
+
if (zone.speedLimit) {
|
|
2589
|
+
this.notifyDriver(vehicleId, `Speed limit: ${zone.speedLimit}km/h`);
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
if (zone.restrictions) {
|
|
2593
|
+
this.notifyDriver(vehicleId, `Zone restrictions: ${zone.restrictions}`);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
private handleVehicleExitZone(vehicleId: string, zone: Zone) {
|
|
2598
|
+
console.log(`Vehicle ${vehicleId} exited zone ${zone.name}`);
|
|
2599
|
+
|
|
2600
|
+
if (zone.activeVehicles) {
|
|
2601
|
+
zone.activeVehicles = zone.activeVehicles.filter(id => id !== vehicleId);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
private calculateDistance(from: number[], to: number[]): number {
|
|
2606
|
+
const R = 6371000; // Earth radius in meters
|
|
2607
|
+
const dLat = this.toRad(to[0] - from[0]);
|
|
2608
|
+
const dLon = this.toRad(to[1] - from[1]);
|
|
2609
|
+
|
|
2610
|
+
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
2611
|
+
Math.cos(this.toRad(from[0])) * Math.cos(this.toRad(to[0])) *
|
|
2612
|
+
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
|
2613
|
+
|
|
2614
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
2615
|
+
return R * c;
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
private toRad(deg: number): number {
|
|
2619
|
+
return deg * (Math.PI / 180);
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
private notifyDriver(vehicleId: string, message: string) {
|
|
2623
|
+
console.log(`[Notification to ${vehicleId}]: ${message}`);
|
|
2624
|
+
// Send push notification or in-app message
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
getZoneStatistics(zoneId: string) {
|
|
2628
|
+
const zone = this.zones.get(zoneId);
|
|
2629
|
+
if (!zone) return null;
|
|
2630
|
+
|
|
2631
|
+
return {
|
|
2632
|
+
name: zone.name,
|
|
2633
|
+
activeVehicles: zone.activeVehicles?.length || 0,
|
|
2634
|
+
capacity: zone.capacity,
|
|
2635
|
+
utilization: ((zone.activeVehicles?.length || 0) / zone.capacity) * 100
|
|
2636
|
+
};
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
// Usage
|
|
2641
|
+
const geofence = new GeofenceManager(controls, handles);
|
|
2642
|
+
|
|
2643
|
+
// Create delivery zones
|
|
2644
|
+
await geofence.createDeliveryZone({
|
|
2645
|
+
id: 'zone-downtown',
|
|
2646
|
+
name: 'Downtown District',
|
|
2647
|
+
center: [40.7128, -74.0060],
|
|
2648
|
+
radius: 2000, // meters
|
|
2649
|
+
speedLimit: 40,
|
|
2650
|
+
restrictions: 'No large trucks between 7AM-9AM',
|
|
2651
|
+
capacity: 50
|
|
2652
|
+
});
|
|
2653
|
+
|
|
2654
|
+
await geofence.createDeliveryZone({
|
|
2655
|
+
id: 'zone-residential',
|
|
2656
|
+
name: 'Residential Area',
|
|
2657
|
+
center: [40.7580, -73.9855],
|
|
2658
|
+
radius: 1500,
|
|
2659
|
+
speedLimit: 30,
|
|
2660
|
+
restrictions: 'Quiet zone - No honking',
|
|
2661
|
+
capacity: 30
|
|
2662
|
+
});
|
|
2663
|
+
|
|
2664
|
+
// Monitor vehicle
|
|
2665
|
+
const vehicleStream = await geofence.monitorVehicleInZones('vehicle-123');
|
|
2666
|
+
```
|
|
2667
|
+
|
|
2668
|
+
---
|
|
2669
|
+
|
|
2670
|
+
## Plugin Development
|
|
2671
|
+
|
|
2672
|
+
### Plugin Best Practices
|
|
2673
|
+
|
|
2674
|
+
1. **Use hooks efficiently** - Cache expensive operations
|
|
2675
|
+
|
|
2676
|
+
```typescript
|
|
2677
|
+
const efficientPlugin = (hooks, options) => {
|
|
2678
|
+
const { controls, handles, utils } = hooks;
|
|
2679
|
+
const cache = new Map();
|
|
2680
|
+
|
|
2681
|
+
return {
|
|
2682
|
+
async getDataWithCache(key: string) {
|
|
2683
|
+
if (cache.has(key)) {
|
|
2684
|
+
return cache.get(key);
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
const data = await controls.someExpensiveOperation(key);
|
|
2688
|
+
cache.set(key, data);
|
|
2689
|
+
|
|
2690
|
+
// Auto-expire after 5 minutes
|
|
2691
|
+
setTimeout(() => cache.delete(key), 300000);
|
|
2692
|
+
|
|
2693
|
+
return data;
|
|
2694
|
+
}
|
|
2695
|
+
};
|
|
2696
|
+
};
|
|
2697
|
+
```
|
|
2698
|
+
|
|
2699
|
+
2. **Clean up resources** - Always remove listeners and close streams
|
|
2700
|
+
|
|
2701
|
+
```typescript
|
|
2702
|
+
const cleanupPlugin = (hooks, options) => {
|
|
2703
|
+
const { handles } = hooks;
|
|
2704
|
+
const listeners = new Map();
|
|
2705
|
+
|
|
2706
|
+
return {
|
|
2707
|
+
startMonitoring(eventName: string, handler: Function) {
|
|
2708
|
+
handles.on(eventName, handler);
|
|
2709
|
+
listeners.set(eventName, handler);
|
|
2710
|
+
},
|
|
2711
|
+
|
|
2712
|
+
stopMonitoring(eventName: string) {
|
|
2713
|
+
const handler = listeners.get(eventName);
|
|
2714
|
+
if (handler) {
|
|
2715
|
+
handles.off(eventName, handler);
|
|
2716
|
+
listeners.delete(eventName);
|
|
2717
|
+
}
|
|
2718
|
+
},
|
|
2719
|
+
|
|
2720
|
+
cleanup() {
|
|
2721
|
+
for (const [event, handler] of listeners) {
|
|
2722
|
+
handles.off(event, handler);
|
|
2723
|
+
}
|
|
2724
|
+
listeners.clear();
|
|
2725
|
+
}
|
|
2726
|
+
};
|
|
2727
|
+
};
|
|
2728
|
+
```
|
|
2729
|
+
|
|
2730
|
+
3. **Handle errors gracefully** - Never let plugin errors crash the app
|
|
2731
|
+
|
|
2732
|
+
```typescript
|
|
2733
|
+
const safePlugin = (hooks, options) => {
|
|
2734
|
+
const { controls } = hooks;
|
|
2735
|
+
|
|
2736
|
+
return {
|
|
2737
|
+
async safeOperation(params: any) {
|
|
2738
|
+
try {
|
|
2739
|
+
const result = await controls.performOperation(params);
|
|
2740
|
+
return { success: true, data: result };
|
|
2741
|
+
} catch (error) {
|
|
2742
|
+
console.error('[Plugin Error]', error);
|
|
2743
|
+
return { success: false, error: error.message };
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
};
|
|
2747
|
+
};
|
|
2748
|
+
```
|
|
2749
|
+
|
|
2750
|
+
4. **Provide configuration options** - Make plugins flexible
|
|
2751
|
+
|
|
2752
|
+
```typescript
|
|
2753
|
+
interface AnalyticsPluginOptions {
|
|
2754
|
+
trackingId: string;
|
|
2755
|
+
debug?: boolean;
|
|
2756
|
+
sampleRate?: number;
|
|
2757
|
+
endpoint?: string;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
const analyticsPlugin = (hooks, options: AnalyticsPluginOptions) => {
|
|
2761
|
+
const debug = options.debug ?? false;
|
|
2762
|
+
const sampleRate = options.sampleRate ?? 1.0;
|
|
2763
|
+
|
|
2764
|
+
const log = (...args: any[]) => {
|
|
2765
|
+
if (debug) console.log('[Analytics]', ...args);
|
|
2766
|
+
};
|
|
2767
|
+
|
|
2768
|
+
return {
|
|
2769
|
+
track(event: string, data: any) {
|
|
2770
|
+
if (Math.random() > sampleRate) return; // Sampling
|
|
2771
|
+
|
|
2772
|
+
log('Tracking event:', event, data);
|
|
2773
|
+
|
|
2774
|
+
// Send to analytics endpoint
|
|
2775
|
+
fetch(options.endpoint || 'https://analytics.dedot.io', {
|
|
2776
|
+
method: 'POST',
|
|
2777
|
+
body: JSON.stringify({
|
|
2778
|
+
trackingId: options.trackingId,
|
|
2779
|
+
event,
|
|
2780
|
+
data,
|
|
2781
|
+
timestamp: Date.now()
|
|
2782
|
+
})
|
|
2783
|
+
}).catch(error => log('Failed to send event:', error));
|
|
2784
|
+
}
|
|
2785
|
+
};
|
|
2786
|
+
};
|
|
2787
|
+
```
|
|
2788
|
+
|
|
2789
|
+
### Complete Plugin Template
|
|
2790
|
+
|
|
2791
|
+
```typescript
|
|
2792
|
+
import type { Plugin, PluginHook } from '@de./sdk';
|
|
2793
|
+
|
|
2794
|
+
/**
|
|
2795
|
+
* Plugin Configuration Options
|
|
2796
|
+
*/
|
|
2797
|
+
interface MyPluginOptions {
|
|
2798
|
+
apiKey?: string;
|
|
2799
|
+
endpoint?: string;
|
|
2800
|
+
debug?: boolean;
|
|
2801
|
+
retries?: number;
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
/**
|
|
2805
|
+
* Plugin Public API
|
|
2806
|
+
*/
|
|
2807
|
+
interface MyPluginAPI {
|
|
2808
|
+
initialize: () => Promise<void>;
|
|
2809
|
+
performAction: (params: any) => Promise<any>;
|
|
2810
|
+
getStatus: () => PluginStatus;
|
|
2811
|
+
cleanup: () => void;
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
interface PluginStatus {
|
|
2815
|
+
initialized: boolean;
|
|
2816
|
+
activeOperations: number;
|
|
2817
|
+
lastError?: string;
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
/**
|
|
2821
|
+
* My Custom Plugin
|
|
2822
|
+
*/
|
|
2823
|
+
const myCustomPlugin: Plugin<MyPluginAPI, MyPluginOptions> = (
|
|
2824
|
+
hooks: PluginHook,
|
|
2825
|
+
options?: MyPluginOptions
|
|
2826
|
+
) => {
|
|
2827
|
+
// Destructure hooks
|
|
2828
|
+
const { controls, handles, map, utils } = hooks;
|
|
2829
|
+
|
|
2830
|
+
// Plugin configuration with defaults
|
|
2831
|
+
const config = {
|
|
2832
|
+
apiKey: options?.apiKey || '',
|
|
2833
|
+
endpoint: options?.endpoint || 'https://api.example.com',
|
|
2834
|
+
debug: options?.debug ?? false,
|
|
2835
|
+
retries: options?.retries ?? 3
|
|
2836
|
+
};
|
|
2837
|
+
|
|
2838
|
+
// Plugin state
|
|
2839
|
+
let initialized = false;
|
|
2840
|
+
let activeOperations = 0;
|
|
2841
|
+
let lastError: string | undefined;
|
|
2842
|
+
|
|
2843
|
+
// Internal logging
|
|
2844
|
+
const log = (...args: any[]) => {
|
|
2845
|
+
if (config.debug) {
|
|
2846
|
+
console.log('[MyPlugin]', ...args);
|
|
2847
|
+
}
|
|
2848
|
+
};
|
|
2849
|
+
|
|
2850
|
+
// Internal error handling
|
|
2851
|
+
const handleError = (error: any, context: string) => {
|
|
2852
|
+
lastError = error.message || String(error);
|
|
2853
|
+
log('Error in', context, ':', error);
|
|
2854
|
+
};
|
|
2855
|
+
|
|
2856
|
+
// Return public API
|
|
2857
|
+
return {
|
|
2858
|
+
async initialize() {
|
|
2859
|
+
if (initialized) {
|
|
2860
|
+
log('Already initialized');
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
try {
|
|
2865
|
+
log('Initializing plugin...');
|
|
2866
|
+
|
|
2867
|
+
// Setup plugin (e.g., fetch config, register handlers)
|
|
2868
|
+
await controls.setMapStyle('streets');
|
|
2869
|
+
|
|
2870
|
+
handles.on('pe:arrived', () => {
|
|
2871
|
+
log('Arrival event detected');
|
|
2872
|
+
});
|
|
2873
|
+
|
|
2874
|
+
initialized = true;
|
|
2875
|
+
log('Plugin initialized successfully');
|
|
2876
|
+
} catch (error) {
|
|
2877
|
+
handleError(error, 'initialize');
|
|
2878
|
+
throw error;
|
|
2879
|
+
}
|
|
2880
|
+
},
|
|
2881
|
+
|
|
2882
|
+
async performAction(params: any) {
|
|
2883
|
+
if (!initialized) {
|
|
2884
|
+
throw new Error('Plugin not initialized');
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
activeOperations++;
|
|
2888
|
+
|
|
2889
|
+
try {
|
|
2890
|
+
log('Performing action with params:', params);
|
|
2891
|
+
|
|
2892
|
+
// Implement your plugin logic here
|
|
2893
|
+
const result = await someAsyncOperation(params);
|
|
2894
|
+
|
|
2895
|
+
return result;
|
|
2896
|
+
} catch (error) {
|
|
2897
|
+
handleError(error, 'performAction');
|
|
2898
|
+
throw error;
|
|
2899
|
+
} finally {
|
|
2900
|
+
activeOperations--;
|
|
2901
|
+
}
|
|
2902
|
+
},
|
|
2903
|
+
|
|
2904
|
+
getStatus(): PluginStatus {
|
|
2905
|
+
return {
|
|
2906
|
+
initialized,
|
|
2907
|
+
activeOperations,
|
|
2908
|
+
lastError
|
|
2909
|
+
};
|
|
2910
|
+
},
|
|
2911
|
+
|
|
2912
|
+
cleanup() {
|
|
2913
|
+
log('Cleaning up plugin resources');
|
|
2914
|
+
|
|
2915
|
+
// Remove event listeners
|
|
2916
|
+
handles.removeListeners(() => {});
|
|
2917
|
+
|
|
2918
|
+
// Clear state
|
|
2919
|
+
initialized = false;
|
|
2920
|
+
activeOperations = 0;
|
|
2921
|
+
lastError = undefined;
|
|
2922
|
+
|
|
2923
|
+
log('Plugin cleanup complete');
|
|
2924
|
+
}
|
|
2925
|
+
};
|
|
2926
|
+
};
|
|
2927
|
+
|
|
2928
|
+
// Helper function (not exposed in API)
|
|
2929
|
+
async function someAsyncOperation(params: any): Promise<any> {
|
|
2930
|
+
// Implementation
|
|
2931
|
+
return { success: true };
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
export default myCustomPlugin;
|
|
2935
|
+
```
|
|
2936
|
+
|
|
2937
|
+
---
|
|
2938
|
+
|
|
2939
|
+
## Error Handling
|
|
2940
|
+
|
|
2941
|
+
### Common Errors and Solutions
|
|
2942
|
+
|
|
2943
|
+
#### 1. Timeout Errors
|
|
2944
|
+
|
|
2945
|
+
All Controls methods have a 12-second timeout.
|
|
2946
|
+
|
|
2947
|
+
```typescript
|
|
2948
|
+
try {
|
|
2949
|
+
await controls.setRoute(journey);
|
|
2950
|
+
} catch (error) {
|
|
2951
|
+
if (error === 'Event timeout') {
|
|
2952
|
+
console.error('Operation timed out after 12 seconds');
|
|
2953
|
+
|
|
2954
|
+
// Retry with exponential backoff
|
|
2955
|
+
await retryWithBackoff(() => controls.setRoute(journey));
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
async function retryWithBackoff(
|
|
2960
|
+
operation: () => Promise<any>,
|
|
2961
|
+
maxRetries: number = 3,
|
|
2962
|
+
baseDelay: number = 1000
|
|
2963
|
+
) {
|
|
2964
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
2965
|
+
try {
|
|
2966
|
+
return await operation();
|
|
2967
|
+
} catch (error) {
|
|
2968
|
+
if (i === maxRetries - 1) throw error;
|
|
2969
|
+
|
|
2970
|
+
const delay = baseDelay * Math.pow(2, i);
|
|
2971
|
+
console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`);
|
|
2972
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
```
|
|
2977
|
+
|
|
2978
|
+
#### 2. Location Permission Errors
|
|
2979
|
+
|
|
2980
|
+
```typescript
|
|
2981
|
+
try {
|
|
2982
|
+
const location = await controls.getCurrentLocation();
|
|
2983
|
+
} catch (error) {
|
|
2984
|
+
console.error('Location access denied:', error);
|
|
2985
|
+
|
|
2986
|
+
// Show UI prompt
|
|
2987
|
+
showLocationPermissionDialog({
|
|
2988
|
+
title: 'Location Access Required',
|
|
2989
|
+
message: 'Please enable location services to continue',
|
|
2990
|
+
onEnable: async () => {
|
|
2991
|
+
// Retry after user enables location
|
|
2992
|
+
const location = await controls.getCurrentLocation();
|
|
2993
|
+
handleLocation(location);
|
|
2994
|
+
}
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2997
|
+
```
|
|
2998
|
+
|
|
2999
|
+
#### 3. Invalid Coordinates
|
|
3000
|
+
|
|
3001
|
+
```typescript
|
|
3002
|
+
function validateCoordinates(coords: [number, number]): boolean {
|
|
3003
|
+
const [lat, lng] = coords;
|
|
3004
|
+
|
|
3005
|
+
if (typeof lat !== 'number' || typeof lng !== 'number') {
|
|
3006
|
+
return false;
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
if (lat < -90 || lat > 90) {
|
|
3010
|
+
return false;
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
if (lng < -180 || lng > 180) {
|
|
3014
|
+
return false;
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
return true;
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
// Usage
|
|
3021
|
+
if (!validateCoordinates(userCoords)) {
|
|
3022
|
+
throw new Error(`Invalid coordinates: ${userCoords}`);
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
await controls.pinCurrentLocation();
|
|
3026
|
+
```
|
|
3027
|
+
|
|
3028
|
+
#### 4. Stream Errors
|
|
3029
|
+
|
|
3030
|
+
```typescript
|
|
3031
|
+
const stream = handles.myLocation('agent');
|
|
3032
|
+
|
|
3033
|
+
stream
|
|
3034
|
+
.pipe(location => {
|
|
3035
|
+
// Process location
|
|
3036
|
+
updateDriverPosition(location);
|
|
3037
|
+
})
|
|
3038
|
+
.onerror(error => {
|
|
3039
|
+
console.error('Stream error:', error);
|
|
3040
|
+
|
|
3041
|
+
// Attempt to restart stream
|
|
3042
|
+
setTimeout(() => {
|
|
3043
|
+
const newStream = handles.myLocation('agent');
|
|
3044
|
+
setupStreamHandlers(newStream);
|
|
3045
|
+
}, 5000);
|
|
3046
|
+
})
|
|
3047
|
+
.onclose(() => {
|
|
3048
|
+
console.log('Stream closed gracefully');
|
|
3049
|
+
});
|
|
3050
|
+
|
|
3051
|
+
// Always close streams when component unmounts
|
|
3052
|
+
onComponentUnmount(() => {
|
|
3053
|
+
if (stream.isActive()) {
|
|
3054
|
+
stream.close();
|
|
3055
|
+
}
|
|
3056
|
+
});
|
|
3057
|
+
```
|
|
3058
|
+
|
|
3059
|
+
#### 5. API Request Errors
|
|
3060
|
+
|
|
3061
|
+
```typescript
|
|
3062
|
+
async function makeRequest<T>(
|
|
3063
|
+
access: De.Access,
|
|
3064
|
+
options: any
|
|
3065
|
+
): Promise<T> {
|
|
3066
|
+
try {
|
|
3067
|
+
return await access.request<T>(options);
|
|
3068
|
+
} catch (error) {
|
|
3069
|
+
// Handle specific HTTP errors
|
|
3070
|
+
if (error.status === 401) {
|
|
3071
|
+
console.error('Unauthorized - refresh token');
|
|
3072
|
+
const newToken = await refreshAccessToken();
|
|
3073
|
+
access.setToken(newToken);
|
|
3074
|
+
|
|
3075
|
+
// Retry request
|
|
3076
|
+
return await access.request<T>(options);
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
if (error.status === 429) {
|
|
3080
|
+
console.error('Rate limited - wait and retry');
|
|
3081
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
3082
|
+
return await access.request<T>(options);
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
if (error.status >= 500) {
|
|
3086
|
+
console.error('Server error - retry later');
|
|
3087
|
+
throw new Error('Service temporarily unavailable');
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
throw error;
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
// Usage
|
|
3095
|
+
const vehicles = await makeRequest<Vehicle[]>(access, {
|
|
3096
|
+
url: '/fleet/vehicles',
|
|
3097
|
+
method: 'GET'
|
|
3098
|
+
});
|
|
3099
|
+
```
|
|
3100
|
+
|
|
3101
|
+
#### 6. MSI Load Errors
|
|
3102
|
+
|
|
3103
|
+
```typescript
|
|
3104
|
+
const msi = new De.MSI({
|
|
3105
|
+
element: 'map-container',
|
|
3106
|
+
accessToken: 'your-token',
|
|
3107
|
+
workspace: 'your-workspace',
|
|
3108
|
+
env: 'prod'
|
|
3109
|
+
});
|
|
3110
|
+
|
|
3111
|
+
// Handle MSI errors
|
|
3112
|
+
msi.on('error', (error: Error) => {
|
|
3113
|
+
console.error('MSI initialization error:', error);
|
|
3114
|
+
|
|
3115
|
+
if (error.message.includes('Element')) {
|
|
3116
|
+
showError('Map container not found. Please check your HTML.');
|
|
3117
|
+
} else if (error.message.includes('network')) {
|
|
3118
|
+
showError('Network error. Please check your internet connection.');
|
|
3119
|
+
} else {
|
|
3120
|
+
showError('Failed to load map. Please refresh the page.');
|
|
3121
|
+
}
|
|
3122
|
+
});
|
|
3123
|
+
|
|
3124
|
+
try {
|
|
3125
|
+
const { controls, handles, plugins } = await msi.load();
|
|
3126
|
+
console.log('MSI loaded successfully');
|
|
3127
|
+
} catch (error) {
|
|
3128
|
+
console.error('Failed to load MSI:', error);
|
|
3129
|
+
|
|
3130
|
+
// Show user-friendly error message
|
|
3131
|
+
showErrorDialog({
|
|
3132
|
+
title: 'Map Loading Failed',
|
|
3133
|
+
message: 'Unable to load the map interface. Please try again.',
|
|
3134
|
+
actions: [
|
|
3135
|
+
{ label: 'Retry', onClick: () => window.location.reload() },
|
|
3136
|
+
{ label: 'Contact Support', onClick: () => openSupportChat() }
|
|
3137
|
+
]
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
3140
|
+
```
|
|
3141
|
+
|
|
3142
|
+
### Error Handling Best Practices
|
|
3143
|
+
|
|
3144
|
+
#### 1. Always Use Try-Catch with Async Operations
|
|
3145
|
+
|
|
3146
|
+
```typescript
|
|
3147
|
+
async function setupDeliveryRoute() {
|
|
3148
|
+
try {
|
|
3149
|
+
await controls.setRoute(journey);
|
|
3150
|
+
await controls.fitRouteBounds('route-1');
|
|
3151
|
+
console.log('Route setup successful');
|
|
3152
|
+
} catch (error) {
|
|
3153
|
+
console.error('Route setup failed:', error);
|
|
3154
|
+
notifyUser('Unable to setup route. Please try again.');
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
```
|
|
3158
|
+
|
|
3159
|
+
#### 2. Handle Stream Lifecycle Properly
|
|
3160
|
+
|
|
3161
|
+
```typescript
|
|
3162
|
+
function setupLocationTracking() {
|
|
3163
|
+
const stream = handles.myLocation('agent');
|
|
3164
|
+
|
|
3165
|
+
// Always attach error handler
|
|
3166
|
+
stream.onerror(error => {
|
|
3167
|
+
console.error('Location tracking error:', error);
|
|
3168
|
+
// Attempt recovery
|
|
3169
|
+
attemptStreamRecovery();
|
|
3170
|
+
});
|
|
3171
|
+
|
|
3172
|
+
// Always attach close handler
|
|
3173
|
+
stream.onclose(() => {
|
|
3174
|
+
console.log('Location tracking stopped');
|
|
3175
|
+
cleanupResources();
|
|
3176
|
+
});
|
|
3177
|
+
|
|
3178
|
+
// Process data
|
|
3179
|
+
stream.pipe(location => {
|
|
3180
|
+
updateUI(location);
|
|
3181
|
+
});
|
|
3182
|
+
|
|
3183
|
+
// Return cleanup function
|
|
3184
|
+
return () => {
|
|
3185
|
+
if (stream.isActive()) {
|
|
3186
|
+
stream.close();
|
|
3187
|
+
}
|
|
3188
|
+
};
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
// In React/Vue/Angular
|
|
3192
|
+
useEffect(() => {
|
|
3193
|
+
const cleanup = setupLocationTracking();
|
|
3194
|
+
return cleanup; // Cleanup on unmount
|
|
3195
|
+
}, []);
|
|
3196
|
+
```
|
|
3197
|
+
|
|
3198
|
+
#### 3. Validate Input Before API Calls
|
|
3199
|
+
|
|
3200
|
+
```typescript
|
|
3201
|
+
function validateJourney(journey: Journey): string[] {
|
|
3202
|
+
const errors: string[] = [];
|
|
3203
|
+
|
|
3204
|
+
if (!journey.origin) {
|
|
3205
|
+
errors.push('Origin is required');
|
|
3206
|
+
} else if (!validateCoordinates(journey.origin.coords)) {
|
|
3207
|
+
errors.push('Invalid origin coordinates');
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
if (!journey.destination) {
|
|
3211
|
+
errors.push('Destination is required');
|
|
3212
|
+
} else if (!validateCoordinates(journey.destination.coords)) {
|
|
3213
|
+
errors.push('Invalid destination coordinates');
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
if (journey.waypoints) {
|
|
3217
|
+
journey.waypoints.forEach((wp, index) => {
|
|
3218
|
+
if (!validateCoordinates(wp.coords)) {
|
|
3219
|
+
errors.push(`Invalid coordinates for waypoint ${index + 1}`);
|
|
3220
|
+
}
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
return errors;
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
// Usage
|
|
3228
|
+
const errors = validateJourney(myJourney);
|
|
3229
|
+
if (errors.length > 0) {
|
|
3230
|
+
console.error('Journey validation failed:', errors);
|
|
3231
|
+
showValidationErrors(errors);
|
|
3232
|
+
} else {
|
|
3233
|
+
await controls.setRoute(myJourney);
|
|
3234
|
+
}
|
|
3235
|
+
```
|
|
3236
|
+
|
|
3237
|
+
#### 4. Implement Retry Logic for Critical Operations
|
|
3238
|
+
|
|
3239
|
+
```typescript
|
|
3240
|
+
async function retryOperation<T>(
|
|
3241
|
+
operation: () => Promise<T>,
|
|
3242
|
+
options: {
|
|
3243
|
+
maxRetries?: number;
|
|
3244
|
+
delay?: number;
|
|
3245
|
+
backoff?: boolean;
|
|
3246
|
+
onRetry?: (attempt: number, error: any) => void;
|
|
3247
|
+
} = {}
|
|
3248
|
+
): Promise<T> {
|
|
3249
|
+
const {
|
|
3250
|
+
maxRetries = 3,
|
|
3251
|
+
delay = 1000,
|
|
3252
|
+
backoff = true,
|
|
3253
|
+
onRetry
|
|
3254
|
+
} = options;
|
|
3255
|
+
|
|
3256
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
3257
|
+
try {
|
|
3258
|
+
return await operation();
|
|
3259
|
+
} catch (error) {
|
|
3260
|
+
if (attempt === maxRetries) {
|
|
3261
|
+
throw error;
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
const waitTime = backoff ? delay * Math.pow(2, attempt - 1) : delay;
|
|
3265
|
+
|
|
3266
|
+
if (onRetry) {
|
|
3267
|
+
onRetry(attempt, error);
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
console.log(`Retry ${attempt}/${maxRetries} after ${waitTime}ms`);
|
|
3271
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
throw new Error('Max retries exceeded');
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
// Usage
|
|
3279
|
+
const location = await retryOperation(
|
|
3280
|
+
() => controls.getCurrentLocation(),
|
|
3281
|
+
{
|
|
3282
|
+
maxRetries: 3,
|
|
3283
|
+
delay: 2000,
|
|
3284
|
+
backoff: true,
|
|
3285
|
+
onRetry: (attempt, error) => {
|
|
3286
|
+
console.log(`Attempt ${attempt} failed:`, error.message);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
);
|
|
3290
|
+
```
|
|
3291
|
+
|
|
3292
|
+
#### 5. Proactive Token Refresh
|
|
3293
|
+
|
|
3294
|
+
```typescript
|
|
3295
|
+
class TokenManager {
|
|
3296
|
+
private access: De.Access;
|
|
3297
|
+
private controls?: De.Controls;
|
|
3298
|
+
private refreshInterval?: NodeJS.Timeout;
|
|
3299
|
+
private tokenExpiryTime: number;
|
|
3300
|
+
|
|
3301
|
+
constructor(access: De.Access, tokenExpiryTime: number) {
|
|
3302
|
+
this.access = access;
|
|
3303
|
+
this.tokenExpiryTime = tokenExpiryTime;
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
setControls(controls: De.Controls) {
|
|
3307
|
+
this.controls = controls;
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
startAutoRefresh() {
|
|
3311
|
+
// Refresh token 5 minutes before expiry
|
|
3312
|
+
const refreshTime = this.tokenExpiryTime - 300000;
|
|
3313
|
+
|
|
3314
|
+
this.refreshInterval = setInterval(async () => {
|
|
3315
|
+
try {
|
|
3316
|
+
console.log('Refreshing access token...');
|
|
3317
|
+
const newToken = await this.fetchNewToken();
|
|
3318
|
+
|
|
3319
|
+
// Update token in both access and controls
|
|
3320
|
+
this.access.setToken(newToken);
|
|
3321
|
+
if (this.controls) {
|
|
3322
|
+
this.controls.refreshToken(newToken);
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
console.log('Token refreshed successfully');
|
|
3326
|
+
} catch (error) {
|
|
3327
|
+
console.error('Token refresh failed:', error);
|
|
3328
|
+
this.handleRefreshFailure(error);
|
|
3329
|
+
}
|
|
3330
|
+
}, refreshTime);
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
stopAutoRefresh() {
|
|
3334
|
+
if (this.refreshInterval) {
|
|
3335
|
+
clearInterval(this.refreshInterval);
|
|
3336
|
+
this.refreshInterval = undefined;
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
private async fetchNewToken(): Promise<string> {
|
|
3341
|
+
const response = await fetch('https://api.dedot.io/auth/refresh', {
|
|
3342
|
+
method: 'POST',
|
|
3343
|
+
headers: {
|
|
3344
|
+
'Content-Type': 'application/json'
|
|
3345
|
+
},
|
|
3346
|
+
body: JSON.stringify({
|
|
3347
|
+
// Refresh token payload
|
|
3348
|
+
})
|
|
3349
|
+
});
|
|
3350
|
+
|
|
3351
|
+
if (!response.ok) {
|
|
3352
|
+
throw new Error('Token refresh failed');
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
const data = await response.json();
|
|
3356
|
+
return data.accessToken;
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
private handleRefreshFailure(error: any) {
|
|
3360
|
+
// Log user out or redirect to login
|
|
3361
|
+
console.error('Unable to refresh token. User needs to re-authenticate.');
|
|
3362
|
+
redirectToLogin();
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
// Usage
|
|
3367
|
+
const tokenManager = new TokenManager(access, 3600000); // 1 hour expiry
|
|
3368
|
+
tokenManager.setControls(controls);
|
|
3369
|
+
tokenManager.startAutoRefresh();
|
|
3370
|
+
|
|
3371
|
+
// Cleanup on app exit
|
|
3372
|
+
onAppExit(() => {
|
|
3373
|
+
tokenManager.stopAutoRefresh();
|
|
3374
|
+
});
|
|
3375
|
+
```
|
|
3376
|
+
|
|
3377
|
+
---
|
|
3378
|
+
|
|
3379
|
+
## Performance Optimization
|
|
3380
|
+
|
|
3381
|
+
### 1. Batch Operations
|
|
3382
|
+
|
|
3383
|
+
Avoid multiple individual operations when you can batch them.
|
|
3384
|
+
|
|
3385
|
+
```typescript
|
|
3386
|
+
// ❌ Bad: Multiple individual operations
|
|
3387
|
+
for (const vehicle of vehicles) {
|
|
3388
|
+
await controls.addNearbyEntity(vehicle);
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
// ✅ Good: Single batch operation
|
|
3392
|
+
await controls.showNearby(vehicles);
|
|
3393
|
+
```
|
|
3394
|
+
|
|
3395
|
+
### 2. Debounce Frequent Updates
|
|
3396
|
+
|
|
3397
|
+
```typescript
|
|
3398
|
+
import { debounce } from 'lodash';
|
|
3399
|
+
|
|
3400
|
+
// Debounce vehicle position updates
|
|
3401
|
+
const debouncedUpdatePosition = debounce(
|
|
3402
|
+
async (vehicleId: string, position: Coordinates) => {
|
|
3403
|
+
await controls.moveNearbyEntity({ id: vehicleId, position });
|
|
3404
|
+
},
|
|
3405
|
+
500, // Update at most every 500ms
|
|
3406
|
+
{ leading: true, trailing: true }
|
|
3407
|
+
);
|
|
3408
|
+
|
|
3409
|
+
// In WebSocket message handler
|
|
3410
|
+
websocket.onmessage = (event) => {
|
|
3411
|
+
const { vehicleId, position } = JSON.parse(event.data);
|
|
3412
|
+
debouncedUpdatePosition(vehicleId, position);
|
|
3413
|
+
};
|
|
3414
|
+
```
|
|
3415
|
+
|
|
3416
|
+
### 3. Throttle High-Frequency Events
|
|
3417
|
+
|
|
3418
|
+
```typescript
|
|
3419
|
+
import { throttle } from 'lodash';
|
|
3420
|
+
|
|
3421
|
+
// Throttle navigation updates
|
|
3422
|
+
const throttledNavigate = throttle(
|
|
3423
|
+
async (position: RTLocation) => {
|
|
3424
|
+
await controls.navigate(position);
|
|
3425
|
+
},
|
|
3426
|
+
1000, // At most once per second
|
|
3427
|
+
{ leading: true, trailing: false }
|
|
3428
|
+
);
|
|
3429
|
+
|
|
3430
|
+
// In GPS update loop
|
|
3431
|
+
gpsStream.pipe(position => {
|
|
3432
|
+
throttledNavigate(position);
|
|
3433
|
+
});
|
|
3434
|
+
```
|
|
3435
|
+
|
|
3436
|
+
### 4. Cleanup Streams Properly
|
|
3437
|
+
|
|
3438
|
+
```typescript
|
|
3439
|
+
class StreamManager {
|
|
3440
|
+
private streams: Map<string, Stream> = new Map();
|
|
3441
|
+
|
|
3442
|
+
registerStream(id: string, stream: Stream) {
|
|
3443
|
+
this.streams.set(id, stream);
|
|
3444
|
+
|
|
3445
|
+
// Auto-cleanup on close
|
|
3446
|
+
stream.onclose(() => {
|
|
3447
|
+
this.streams.delete(id);
|
|
3448
|
+
});
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
closeStream(id: string) {
|
|
3452
|
+
const stream = this.streams.get(id);
|
|
3453
|
+
if (stream && stream.isActive()) {
|
|
3454
|
+
stream.close();
|
|
3455
|
+
}
|
|
3456
|
+
this.streams.delete(id);
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
closeAllStreams() {
|
|
3460
|
+
for (const [id, stream] of this.streams) {
|
|
3461
|
+
if (stream.isActive()) {
|
|
3462
|
+
stream.close();
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
this.streams.clear();
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
getActiveStreamCount(): number {
|
|
3469
|
+
return Array.from(this.streams.values())
|
|
3470
|
+
.filter(s => s.isActive()).length;
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
// Usage
|
|
3475
|
+
const streamManager = new StreamManager();
|
|
3476
|
+
|
|
3477
|
+
const locationStream = handles.myLocation('agent');
|
|
3478
|
+
streamManager.registerStream('location', locationStream);
|
|
3479
|
+
|
|
3480
|
+
// Cleanup on component unmount
|
|
3481
|
+
onComponentUnmount(() => {
|
|
3482
|
+
streamManager.closeAllStreams();
|
|
3483
|
+
});
|
|
3484
|
+
```
|
|
3485
|
+
|
|
3486
|
+
### 5. Cache Expensive Operations
|
|
3487
|
+
|
|
3488
|
+
```typescript
|
|
3489
|
+
class LocationCache {
|
|
3490
|
+
private cache = new Map<string, {
|
|
3491
|
+
data: any;
|
|
3492
|
+
timestamp: number;
|
|
3493
|
+
}>();
|
|
3494
|
+
private ttl: number = 300000; // 5 minutes
|
|
3495
|
+
|
|
3496
|
+
async get<T>(
|
|
3497
|
+
key: string,
|
|
3498
|
+
fetcher: () => Promise<T>
|
|
3499
|
+
): Promise<T> {
|
|
3500
|
+
// Check cache
|
|
3501
|
+
const cached = this.cache.get(key);
|
|
3502
|
+
if (cached && Date.now() - cached.timestamp < this.ttl) {
|
|
3503
|
+
console.log(`Cache hit for ${key}`);
|
|
3504
|
+
return cached.data as T;
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
// Fetch and cache
|
|
3508
|
+
console.log(`Cache miss for ${key}, fetching...`);
|
|
3509
|
+
const data = await fetcher();
|
|
3510
|
+
|
|
3511
|
+
this.cache.set(key, {
|
|
3512
|
+
data,
|
|
3513
|
+
timestamp: Date.now()
|
|
3514
|
+
});
|
|
3515
|
+
|
|
3516
|
+
return data;
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
invalidate(key: string) {
|
|
3520
|
+
this.cache.delete(key);
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
clear() {
|
|
3524
|
+
this.cache.clear();
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
// Auto-cleanup expired entries
|
|
3528
|
+
startAutoCleanup() {
|
|
3529
|
+
setInterval(() => {
|
|
3530
|
+
const now = Date.now();
|
|
3531
|
+
for (const [key, value] of this.cache) {
|
|
3532
|
+
if (now - value.timestamp > this.ttl) {
|
|
3533
|
+
this.cache.delete(key);
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
}, 60000); // Every minute
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
// Usage
|
|
3541
|
+
const cache = new LocationCache();
|
|
3542
|
+
cache.startAutoCleanup();
|
|
3543
|
+
|
|
3544
|
+
// Cache geocoding results
|
|
3545
|
+
const location = await cache.get(
|
|
3546
|
+
`geocode:${address}`,
|
|
3547
|
+
() => controls.resolvePlace(address)
|
|
3548
|
+
);
|
|
3549
|
+
|
|
3550
|
+
// Cache route calculations
|
|
3551
|
+
const route = await cache.get(
|
|
3552
|
+
`route:${origin}-${destination}`,
|
|
3553
|
+
() => calculateRoute(origin, destination)
|
|
3554
|
+
);
|
|
3555
|
+
```
|
|
3556
|
+
|
|
3557
|
+
### 6. Lazy Load Components
|
|
3558
|
+
|
|
3559
|
+
```typescript
|
|
3560
|
+
// Only load MSI when needed
|
|
3561
|
+
class LazyMapLoader {
|
|
3562
|
+
private msiPromise?: Promise<any>;
|
|
3563
|
+
|
|
3564
|
+
async loadWhenNeeded() {
|
|
3565
|
+
if (this.msiPromise) {
|
|
3566
|
+
return this.msiPromise;
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
this.msiPromise = this.initializeMSI();
|
|
3570
|
+
return this.msiPromise;
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
private async initializeMSI() {
|
|
3574
|
+
console.log('Initializing MSI...');
|
|
3575
|
+
|
|
3576
|
+
const msi = new De.MSI({
|
|
3577
|
+
element: 'map-container',
|
|
3578
|
+
accessToken: 'your-token',
|
|
3579
|
+
workspace: 'your-workspace'
|
|
3580
|
+
});
|
|
3581
|
+
|
|
3582
|
+
return await msi.load();
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
// Usage
|
|
3587
|
+
const mapLoader = new LazyMapLoader();
|
|
3588
|
+
|
|
3589
|
+
// Only load map when user clicks "Track Delivery"
|
|
3590
|
+
button.onclick = async () => {
|
|
3591
|
+
const { controls, handles } = await mapLoader.loadWhenNeeded();
|
|
3592
|
+
// Use controls and handles
|
|
3593
|
+
};
|
|
3594
|
+
```
|
|
3595
|
+
|
|
3596
|
+
### 7. Optimize WebSocket Connections
|
|
3597
|
+
|
|
3598
|
+
```typescript
|
|
3599
|
+
class OptimizedWebSocket {
|
|
3600
|
+
private ws?: WebSocket;
|
|
3601
|
+
private reconnectAttempts = 0;
|
|
3602
|
+
private maxReconnectAttempts = 5;
|
|
3603
|
+
private reconnectDelay = 1000;
|
|
3604
|
+
private messageQueue: any[] = [];
|
|
3605
|
+
|
|
3606
|
+
connect(url: string) {
|
|
3607
|
+
this.ws = new WebSocket(url);
|
|
3608
|
+
|
|
3609
|
+
this.ws.onopen = () => {
|
|
3610
|
+
console.log('WebSocket connected');
|
|
3611
|
+
this.reconnectAttempts = 0;
|
|
3612
|
+
|
|
3613
|
+
// Send queued messages
|
|
3614
|
+
while (this.messageQueue.length > 0) {
|
|
3615
|
+
const message = this.messageQueue.shift();
|
|
3616
|
+
this.send(message);
|
|
3617
|
+
}
|
|
3618
|
+
};
|
|
3619
|
+
|
|
3620
|
+
this.ws.onerror = (error) => {
|
|
3621
|
+
console.error('WebSocket error:', error);
|
|
3622
|
+
};
|
|
3623
|
+
|
|
3624
|
+
this.ws.onclose = () => {
|
|
3625
|
+
console.log('WebSocket closed');
|
|
3626
|
+
this.attemptReconnect(url);
|
|
3627
|
+
};
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
send(message: any) {
|
|
3631
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
3632
|
+
this.ws.send(JSON.stringify(message));
|
|
3633
|
+
} else {
|
|
3634
|
+
// Queue message for later
|
|
3635
|
+
this.messageQueue.push(message);
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
private attemptReconnect(url: string) {
|
|
3640
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
3641
|
+
console.error('Max reconnection attempts reached');
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
this.reconnectAttempts++;
|
|
3646
|
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
3647
|
+
|
|
3648
|
+
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
3649
|
+
|
|
3650
|
+
setTimeout(() => {
|
|
3651
|
+
this.connect(url);
|
|
3652
|
+
}, delay);
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
close() {
|
|
3656
|
+
if (this.ws) {
|
|
3657
|
+
this.ws.close();
|
|
3658
|
+
this.ws = undefined;
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
// Usage
|
|
3664
|
+
const ws = new OptimizedWebSocket();
|
|
3665
|
+
ws.connect('wss://api.dedot.io/fleet/live');
|
|
3666
|
+
```
|
|
3667
|
+
|
|
3668
|
+
---
|
|
3669
|
+
|
|
3670
|
+
## Testing
|
|
3671
|
+
|
|
3672
|
+
### Unit Testing with Jest
|
|
3673
|
+
|
|
3674
|
+
```typescript
|
|
3675
|
+
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
|
|
3676
|
+
import De from '@de./sdk';
|
|
3677
|
+
|
|
3678
|
+
describe('De. SDK - MSI', () => {
|
|
3679
|
+
let msi: De.MSI;
|
|
3680
|
+
|
|
3681
|
+
beforeEach(() => {
|
|
3682
|
+
// Mock DOM element
|
|
3683
|
+
document.body.innerHTML = '<div id="map-container"></div>';
|
|
3684
|
+
|
|
3685
|
+
// Mock fetch for iframe loading
|
|
3686
|
+
global.fetch = jest.fn(() =>
|
|
3687
|
+
Promise.resolve({
|
|
3688
|
+
ok: true,
|
|
3689
|
+
json: () => Promise.resolve({})
|
|
3690
|
+
} as Response)
|
|
3691
|
+
);
|
|
3692
|
+
|
|
3693
|
+
msi = new De.MSI({
|
|
3694
|
+
element: 'map-container',
|
|
3695
|
+
accessToken: 'test-token',
|
|
3696
|
+
workspace: 'test-workspace',
|
|
3697
|
+
env: 'dev'
|
|
3698
|
+
});
|
|
3699
|
+
});
|
|
3700
|
+
|
|
3701
|
+
afterEach(() => {
|
|
3702
|
+
jest.clearAllMocks();
|
|
3703
|
+
});
|
|
3704
|
+
|
|
3705
|
+
it('should initialize MSI with correct options', () => {
|
|
3706
|
+
expect(msi).toBeDefined();
|
|
3707
|
+
expect(msi.isReady()).toBe(false);
|
|
3708
|
+
});
|
|
3709
|
+
|
|
3710
|
+
it('should load MSI and return API components', async () => {
|
|
3711
|
+
const api = await msi.load();
|
|
3712
|
+
|
|
3713
|
+
expect(api.controls).toBeDefined();
|
|
3714
|
+
expect(api.handles).toBeDefined();
|
|
3715
|
+
expect(api.plugins).toBeDefined();
|
|
3716
|
+
});
|
|
3717
|
+
|
|
3718
|
+
it('should emit error event on initialization failure', (done) => {
|
|
3719
|
+
const failingMsi = new De.MSI({
|
|
3720
|
+
element: 'non-existent',
|
|
3721
|
+
accessToken: 'test-token',
|
|
3722
|
+
workspace: 'test-workspace'
|
|
3723
|
+
});
|
|
3724
|
+
|
|
3725
|
+
failingMsi.on('error', (error) => {
|
|
3726
|
+
expect(error).toBeDefined();
|
|
3727
|
+
expect(error.message).toContain('Not Found');
|
|
3728
|
+
done();
|
|
3729
|
+
});
|
|
3730
|
+
|
|
3731
|
+
failingMsi.load().catch(() => {});
|
|
3732
|
+
});
|
|
3733
|
+
});
|
|
3734
|
+
|
|
3735
|
+
describe('De. SDK - Controls', () => {
|
|
3736
|
+
let controls: De.Controls;
|
|
3737
|
+
|
|
3738
|
+
beforeEach(async () => {
|
|
3739
|
+
document.body.innerHTML = '<div id="map-container"></div>';
|
|
3740
|
+
|
|
3741
|
+
const msi = new De.MSI({
|
|
3742
|
+
element: 'map-container',
|
|
3743
|
+
accessToken: 'test-token',
|
|
3744
|
+
workspace: 'test-workspace',
|
|
3745
|
+
env: 'dev'
|
|
3746
|
+
});
|
|
3747
|
+
|
|
3748
|
+
const api = await msi.load();
|
|
3749
|
+
controls = api.controls;
|
|
3750
|
+
});
|
|
3751
|
+
|
|
3752
|
+
it('should get current location', async () => {
|
|
3753
|
+
// Mock getCurrentLocation
|
|
3754
|
+
const mockLocation = {
|
|
3755
|
+
latitude: 40.7128,
|
|
3756
|
+
longitude: -74.0060,
|
|
3757
|
+
accuracy: 10,
|
|
3758
|
+
heading: 180,
|
|
3759
|
+
speed: 0
|
|
3760
|
+
};
|
|
3761
|
+
|
|
3762
|
+
jest.spyOn(controls, 'getCurrentLocation')
|
|
3763
|
+
.mockResolvedValue(mockLocation);
|
|
3764
|
+
|
|
3765
|
+
const location = await controls.getCurrentLocation();
|
|
3766
|
+
|
|
3767
|
+
expect(location).toEqual(mockLocation);
|
|
3768
|
+
expect(location?.latitude).toBe(40.7128);
|
|
3769
|
+
});
|
|
3770
|
+
|
|
3771
|
+
it('should set route with journey', async () => {
|
|
3772
|
+
const journey: De.Journey = {
|
|
3773
|
+
routeId: 'test-route',
|
|
3774
|
+
origin: {
|
|
3775
|
+
coords: [40.7128, -74.0060],
|
|
3776
|
+
caption: { label: 'Origin' }
|
|
3777
|
+
},
|
|
3778
|
+
destination: {
|
|
3779
|
+
coords: [40.7580, -73.9855],
|
|
3780
|
+
caption: { label: 'Destination' }
|
|
3781
|
+
}
|
|
3782
|
+
};
|
|
3783
|
+
|
|
3784
|
+
jest.spyOn(controls, 'setRoute').mockResolvedValue();
|
|
3785
|
+
|
|
3786
|
+
await controls.setRoute(journey);
|
|
3787
|
+
|
|
3788
|
+
expect(controls.setRoute).toHaveBeenCalledWith(journey);
|
|
3789
|
+
});
|
|
3790
|
+
});
|
|
3791
|
+
|
|
3792
|
+
describe('De. SDK - Handles', () => {
|
|
3793
|
+
let handles: De.Handles;
|
|
3794
|
+
|
|
3795
|
+
beforeEach(async () => {
|
|
3796
|
+
document.body.innerHTML = '<div id="map-container"></div>';
|
|
3797
|
+
|
|
3798
|
+
const msi = new De.MSI({
|
|
3799
|
+
element: 'map-container',
|
|
3800
|
+
accessToken: 'test-token',
|
|
3801
|
+
workspace: 'test-workspace',
|
|
3802
|
+
env: 'dev'
|
|
3803
|
+
});
|
|
3804
|
+
|
|
3805
|
+
const api = await msi.load();
|
|
3806
|
+
handles = api.handles;
|
|
3807
|
+
});
|
|
3808
|
+
|
|
3809
|
+
it('should create location stream', () => {
|
|
3810
|
+
const stream = handles.myLocation('agent');
|
|
3811
|
+
|
|
3812
|
+
expect(stream).toBeDefined();
|
|
3813
|
+
expect(typeof stream.pipe).toBe('function');
|
|
3814
|
+
expect(typeof stream.close).toBe('function');
|
|
3815
|
+
});
|
|
3816
|
+
|
|
3817
|
+
it('should handle location updates in stream', (done) => {
|
|
3818
|
+
const stream = handles.myLocation('agent');
|
|
3819
|
+
const mockLocation = {
|
|
3820
|
+
latitude: 40.7128,
|
|
3821
|
+
longitude: -74.0060,
|
|
3822
|
+
accuracy: 10
|
|
3823
|
+
};
|
|
3824
|
+
|
|
3825
|
+
stream.pipe(location => {
|
|
3826
|
+
expect(location).toEqual(mockLocation);
|
|
3827
|
+
stream.close();
|
|
3828
|
+
done();
|
|
3829
|
+
});
|
|
3830
|
+
|
|
3831
|
+
// Simulate location update
|
|
3832
|
+
stream.write(mockLocation);
|
|
3833
|
+
});
|
|
3834
|
+
});
|
|
3835
|
+
|
|
3836
|
+
describe('De. SDK - Access', () => {
|
|
3837
|
+
let access: De.Access;
|
|
3838
|
+
|
|
3839
|
+
beforeEach(() => {
|
|
3840
|
+
access = new De.Access({
|
|
3841
|
+
workspace: 'test-workspace',
|
|
3842
|
+
accessToken: 'test-token',
|
|
3843
|
+
env: 'dev',
|
|
3844
|
+
version: 1
|
|
3845
|
+
});
|
|
3846
|
+
|
|
3847
|
+
// Mock fetch
|
|
3848
|
+
global.fetch = jest.fn();
|
|
3849
|
+
});
|
|
3850
|
+
|
|
3851
|
+
it('should make GET request', async () => {
|
|
3852
|
+
const mockResponse = { vehicles: [] };
|
|
3853
|
+
|
|
3854
|
+
(global.fetch as jest.Mock).mockResolvedValue({
|
|
3855
|
+
ok: true,
|
|
3856
|
+
json: () => Promise.resolve(mockResponse)
|
|
3857
|
+
});
|
|
3858
|
+
|
|
3859
|
+
const result = await access.request({
|
|
3860
|
+
url: '/fleet/vehicles',
|
|
3861
|
+
method: 'GET'
|
|
3862
|
+
});
|
|
3863
|
+
|
|
3864
|
+
expect(result).toEqual(mockResponse);
|
|
3865
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
3866
|
+
expect.stringContaining('/fleet/vehicles'),
|
|
3867
|
+
expect.objectContaining({
|
|
3868
|
+
method: 'GET'
|
|
3869
|
+
})
|
|
3870
|
+
);
|
|
3871
|
+
});
|
|
3872
|
+
|
|
3873
|
+
it('should make POST request with body', async () => {
|
|
3874
|
+
const mockBody = { name: 'Test Order' };
|
|
3875
|
+
const mockResponse = { id: 'order-123' };
|
|
3876
|
+
|
|
3877
|
+
(global.fetch as jest.Mock).mockResolvedValue({
|
|
3878
|
+
ok: true,
|
|
3879
|
+
json: () => Promise.resolve(mockResponse)
|
|
3880
|
+
});
|
|
3881
|
+
|
|
3882
|
+
const result = await access.request({
|
|
3883
|
+
url: '/orders',
|
|
3884
|
+
method: 'POST',
|
|
3885
|
+
body: mockBody
|
|
3886
|
+
});
|
|
3887
|
+
|
|
3888
|
+
expect(result).toEqual(mockResponse);
|
|
3889
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
3890
|
+
expect.any(String),
|
|
3891
|
+
expect.objectContaining({
|
|
3892
|
+
method: 'POST',
|
|
3893
|
+
body: JSON.stringify(mockBody)
|
|
3894
|
+
})
|
|
3895
|
+
);
|
|
3896
|
+
});
|
|
3897
|
+
|
|
3898
|
+
it('should update access token', () => {
|
|
3899
|
+
const newToken = 'new-test-token';
|
|
3900
|
+
access.setToken(newToken);
|
|
3901
|
+
|
|
3902
|
+
// Verify token was updated (implementation-specific test)
|
|
3903
|
+
expect(access).toBeDefined();
|
|
3904
|
+
});
|
|
3905
|
+
});
|
|
3906
|
+
```
|
|
3907
|
+
|
|
3908
|
+
### Integration Testing
|
|
3909
|
+
|
|
3910
|
+
```typescript
|
|
3911
|
+
describe('De. SDK - Integration Tests', () => {
|
|
3912
|
+
let msi: De.MSI;
|
|
3913
|
+
let access: De.Access;
|
|
3914
|
+
let controls: De.Controls;
|
|
3915
|
+
let handles: De.Handles;
|
|
3916
|
+
|
|
3917
|
+
beforeAll(async () => {
|
|
3918
|
+
// Setup full environment
|
|
3919
|
+
document.body.innerHTML = '<div id="map-container"></div>';
|
|
3920
|
+
|
|
3921
|
+
access = new De.Access({
|
|
3922
|
+
workspace: 'test-workspace',
|
|
3923
|
+
accessToken: 'test-token',
|
|
3924
|
+
env: 'dev'
|
|
3925
|
+
});
|
|
3926
|
+
|
|
3927
|
+
msi = new De.MSI({
|
|
3928
|
+
element: 'map-container',
|
|
3929
|
+
accessToken: 'test-token',
|
|
3930
|
+
workspace: 'test-workspace',
|
|
3931
|
+
env: 'dev'
|
|
3932
|
+
});
|
|
3933
|
+
|
|
3934
|
+
const api = await msi.load();
|
|
3935
|
+
controls = api.controls;
|
|
3936
|
+
handles = api.handles;
|
|
3937
|
+
});
|
|
3938
|
+
|
|
3939
|
+
it('should complete full delivery flow', async () => {
|
|
3940
|
+
// 1. Create order via API
|
|
3941
|
+
const order = await access.request({
|
|
3942
|
+
url: '/orders',
|
|
3943
|
+
method: 'POST',
|
|
3944
|
+
body: {
|
|
3945
|
+
pickupLocation: [40.7128, -74.0060],
|
|
3946
|
+
deliveryLocation: [40.7580, -73.9855]
|
|
3947
|
+
}
|
|
3948
|
+
});
|
|
3949
|
+
|
|
3950
|
+
expect(order).toBeDefined();
|
|
3951
|
+
|
|
3952
|
+
// 2. Set route on map
|
|
3953
|
+
await handles.pickupPoint(
|
|
3954
|
+
[40.7128, -74.0060],
|
|
3955
|
+
{ label: 'Pickup' }
|
|
3956
|
+
);
|
|
3957
|
+
|
|
3958
|
+
await handles.dropoffPoint(
|
|
3959
|
+
[40.7580, -73.9855],
|
|
3960
|
+
{ label: 'Delivery' }
|
|
3961
|
+
);
|
|
3962
|
+
|
|
3963
|
+
// 3. Track vehicle
|
|
3964
|
+
const vehicle = { id: 'test-vehicle', location: [40.7128, -74.0060] };
|
|
3965
|
+
const vehicleStream = handles.peerLocation(
|
|
3966
|
+
vehicle.location,
|
|
3967
|
+
{ label: 'Driver' }
|
|
3968
|
+
);
|
|
3969
|
+
|
|
3970
|
+
expect(vehicleStream).toBeDefined();
|
|
3971
|
+
|
|
3972
|
+
// Cleanup
|
|
3973
|
+
vehicleStream.close();
|
|
3974
|
+
});
|
|
3975
|
+
|
|
3976
|
+
afterAll(() => {
|
|
3977
|
+
// Cleanup
|
|
3978
|
+
if (msi) {
|
|
3979
|
+
// Close all streams and cleanup
|
|
3980
|
+
}
|
|
3981
|
+
});
|
|
3982
|
+
});
|
|
3983
|
+
```
|
|
3984
|
+
|
|
3985
|
+
---
|
|
3986
|
+
|
|
3987
|
+
## TypeScript Types Reference
|
|
3988
|
+
|
|
3989
|
+
### Core Types
|
|
3990
|
+
|
|
3991
|
+
```typescript
|
|
3992
|
+
// HTTP Request Types
|
|
3993
|
+
interface HTTPRequestOptions {
|
|
3994
|
+
url: string;
|
|
3995
|
+
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
|
3996
|
+
headers?: { [index: string]: string };
|
|
3997
|
+
body?: any;
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
interface HTTPResponse {
|
|
4001
|
+
error: boolean;
|
|
4002
|
+
message?: string;
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
// Coordinates
|
|
4006
|
+
type LngLat = [number, number]; // [longitude, latitude]
|
|
4007
|
+
|
|
4008
|
+
interface Coordinates {
|
|
4009
|
+
lng: number; // longitude
|
|
4010
|
+
lat: number; // latitude
|
|
4011
|
+
}
|
|
4012
|
+
|
|
4013
|
+
interface RTLocation extends Coordinates {
|
|
4014
|
+
heading?: number; // degrees (0-360)
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
interface PickedLocation {
|
|
4018
|
+
point: {
|
|
4019
|
+
x: number;
|
|
4020
|
+
y: number;
|
|
4021
|
+
};
|
|
4022
|
+
coordinates: Coordinates;
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
interface ActivePosition {
|
|
4026
|
+
id: string;
|
|
4027
|
+
position: RTLocation;
|
|
4028
|
+
caption?: Caption;
|
|
4029
|
+
}
|
|
4030
|
+
|
|
4031
|
+
// Captions
|
|
4032
|
+
interface Caption {
|
|
4033
|
+
duration?: number;
|
|
4034
|
+
unit?: string;
|
|
4035
|
+
label?: string;
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
// Waypoints
|
|
4039
|
+
interface MapWaypoint {
|
|
4040
|
+
index?: number;
|
|
4041
|
+
coords: Coordinates;
|
|
4042
|
+
caption?: Caption;
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
interface Waypoint {
|
|
4046
|
+
no: number;
|
|
4047
|
+
type: 'pickup' | 'dropoff';
|
|
4048
|
+
description: string;
|
|
4049
|
+
coordinates: Coordinates;
|
|
4050
|
+
address?: string;
|
|
4051
|
+
contact: {
|
|
4052
|
+
type: string;
|
|
4053
|
+
reference: string;
|
|
4054
|
+
phone?: string;
|
|
4055
|
+
email?: string;
|
|
4056
|
+
};
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
type WaypointIndex = 'origin' | 'destination' | number;
|
|
4060
|
+
|
|
4061
|
+
interface WaypointOptions {
|
|
4062
|
+
no?: number;
|
|
4063
|
+
type?: 'pickup' | 'dropoff';
|
|
4064
|
+
description?: string;
|
|
4065
|
+
coordinates?: Coordinates;
|
|
4066
|
+
address?: string;
|
|
4067
|
+
'contact.type'?: string;
|
|
4068
|
+
'contact.reference'?: string;
|
|
4069
|
+
'contact.phone'?: string;
|
|
4070
|
+
'contact.email'?: string;
|
|
4071
|
+
}
|
|
4072
|
+
|
|
4073
|
+
// Journey & Routes
|
|
4074
|
+
interface Journey {
|
|
4075
|
+
routeId: string | number;
|
|
4076
|
+
origin?: MapWaypoint;
|
|
4077
|
+
destination?: MapWaypoint;
|
|
4078
|
+
waypoints?: MapWaypoint[];
|
|
4079
|
+
options?: RouteOptions;
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
interface ActiveDirection {
|
|
4083
|
+
routeId: string | number;
|
|
4084
|
+
profile: string;
|
|
4085
|
+
origin?: Coordinates;
|
|
4086
|
+
destination?: Coordinates;
|
|
4087
|
+
waypoints: Coordinates[];
|
|
4088
|
+
route: any;
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
type RouteID = string | number;
|
|
4092
|
+
|
|
4093
|
+
interface RoutesFitBoundsOptions {
|
|
4094
|
+
includes?: RouteID[];
|
|
4095
|
+
margin?: number;
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
interface RouteOptions {
|
|
4099
|
+
id?: string | number;
|
|
4100
|
+
mode?: 'default' | 'navigation';
|
|
4101
|
+
profile?: 'driving-traffic' | 'driving' | 'cycling' | 'biking' | 'walking' | 'transit';
|
|
4102
|
+
unit?: 'metric' | 'imperial';
|
|
4103
|
+
preference?: 'TRAFFIC_AWARE' | 'TRAFFIC_UNAWARE';
|
|
4104
|
+
pointless?: boolean;
|
|
4105
|
+
styles?: any;
|
|
4106
|
+
animation?: AnimatedRouteOptions;
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
// Animated Routes
|
|
4110
|
+
type AnimatedRouteNativePathType = 'dot' | 'solid';
|
|
4111
|
+
|
|
4112
|
+
type AnimatedRouteNativeMethods =
|
|
4113
|
+
| 'dot:flow'
|
|
4114
|
+
| 'dot:fade'
|
|
4115
|
+
| 'dot:pulse'
|
|
4116
|
+
| 'dot:directional'
|
|
4117
|
+
| 'solid:flow'
|
|
4118
|
+
| 'solid:fade'
|
|
4119
|
+
| 'solid:pulse'
|
|
4120
|
+
| 'solid:directional';
|
|
4121
|
+
|
|
4122
|
+
interface AnimatedRouteRules {
|
|
4123
|
+
styles?: any;
|
|
4124
|
+
speed?: number; // pixels per frame
|
|
4125
|
+
fadeLength?: number; // length of fade effect in pixels
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
interface AnimatedRoute {
|
|
4129
|
+
key: number | null;
|
|
4130
|
+
currentOffset: number;
|
|
4131
|
+
rules: Required<AnimatedRouteRules>;
|
|
4132
|
+
startTime?: number;
|
|
4133
|
+
polyline?: any;
|
|
4134
|
+
engine: any;
|
|
4135
|
+
path: Coordinates[];
|
|
4136
|
+
|
|
4137
|
+
create(pathType?: AnimatedRouteNativePathType): void;
|
|
4138
|
+
apply: Record<string, () => void>;
|
|
4139
|
+
stop(): void;
|
|
4140
|
+
remove(): void;
|
|
4141
|
+
}
|
|
4142
|
+
|
|
4143
|
+
type AnimatedRouteOptions = AnimatedRouteNativeMethods | {
|
|
4144
|
+
handler?: new (engine: Engine, path: Coordinates[], rules?: AnimatedRouteRules) => AnimatedRoute;
|
|
4145
|
+
method?: string;
|
|
4146
|
+
rules?: AnimatedRouteRules;
|
|
4147
|
+
};
|
|
4148
|
+
|
|
4149
|
+
// Search
|
|
4150
|
+
interface SearchPlace {
|
|
4151
|
+
name: string;
|
|
4152
|
+
location: Coordinates;
|
|
4153
|
+
address: string;
|
|
4154
|
+
}
|
|
4155
|
+
|
|
4156
|
+
// Entities
|
|
4157
|
+
interface Entity {
|
|
4158
|
+
id: string;
|
|
4159
|
+
status: 'ACTIVE' | 'BUSY';
|
|
4160
|
+
grade: '1H' | '2H' | '3H';
|
|
4161
|
+
currentLocation: RTLocation;
|
|
4162
|
+
static?: boolean;
|
|
4163
|
+
type: 'moto' | 'car' | 'bike' | 'truck' | 'plane' | 'ship' | 'restaurant' | 'hotel' | 'store' | 'office' | 'warehouse';
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
interface EntitySpecs {
|
|
4167
|
+
id: string;
|
|
4168
|
+
status: 'ACTIVE' | 'BUSY';
|
|
4169
|
+
grade: '1H' | '2H' | '3H';
|
|
4170
|
+
currentLocation: RTLocation;
|
|
4171
|
+
static?: boolean;
|
|
4172
|
+
type: 'moto' | 'car' | 'bike' | 'truck' | 'plane' | 'ship' | 'restaurant' | 'hotel' | 'store' | 'office' | 'warehouse';
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
// Map
|
|
4176
|
+
interface MapOptions {
|
|
4177
|
+
element: string;
|
|
4178
|
+
accessToken: string;
|
|
4179
|
+
version?: number;
|
|
4180
|
+
env?: 'dev' | 'prod';
|
|
4181
|
+
}
|
|
4182
|
+
|
|
4183
|
+
type MapLayerStyle = 'streets' | 'outdoors' | 'light' | 'dark' | 'satellite';
|
|
4184
|
+
|
|
4185
|
+
// User Location
|
|
4186
|
+
interface UserLocationOptions {
|
|
4187
|
+
// Base point styling options
|
|
4188
|
+
borderRadius?: number;
|
|
4189
|
+
borderColor?: string;
|
|
4190
|
+
borderOpacity?: number;
|
|
4191
|
+
dotColor?: string;
|
|
4192
|
+
showInnerDot?: boolean;
|
|
4193
|
+
noRing?: boolean;
|
|
4194
|
+
|
|
4195
|
+
// User location specific options
|
|
4196
|
+
showDirectionArrow?: boolean;
|
|
4197
|
+
arrowColor?: string;
|
|
4198
|
+
arrowSize?: number;
|
|
4199
|
+
pulseAnimation?: boolean;
|
|
4200
|
+
accuracyCircle?: boolean;
|
|
4201
|
+
accuracyColor?: string;
|
|
4202
|
+
accuracyOpacity?: number;
|
|
4203
|
+
|
|
4204
|
+
// Callbacks
|
|
4205
|
+
onLocationUpdate?: (location: RTLocation) => void;
|
|
4206
|
+
onLocationError?: (error: GeolocationPositionError) => void;
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
// Drag Pick
|
|
4210
|
+
interface DragPickOptions {
|
|
4211
|
+
snapToRoad?: boolean;
|
|
4212
|
+
pinPoints?: boolean;
|
|
4213
|
+
pointOptions?: CustomPointOptions;
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
type DragPickContentType = 'duration' | 'distance' | 'preloader';
|
|
4217
|
+
|
|
4218
|
+
interface DragPickContent {
|
|
4219
|
+
time?: number;
|
|
4220
|
+
unit?: 'min' | 'sec' | 'hr' | 'km' | 'mi' | 'm';
|
|
4221
|
+
distance?: number;
|
|
4222
|
+
preloader?: boolean;
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4225
|
+
type DragPickEvent = 'dragstart' | 'dragend' | 'zoom_changed' | 'idle';
|
|
4226
|
+
|
|
4227
|
+
interface DragPickInterface {
|
|
4228
|
+
enable(origin?: Coordinates): void;
|
|
4229
|
+
disable(): void;
|
|
4230
|
+
content(type: DragPickContentType, content: DragPickContent): void;
|
|
4231
|
+
}
|
|
4232
|
+
|
|
4233
|
+
// Package
|
|
4234
|
+
interface Package {
|
|
4235
|
+
waypointNo: number;
|
|
4236
|
+
careLevel: number;
|
|
4237
|
+
category: string;
|
|
4238
|
+
weight: number;
|
|
4239
|
+
note?: string;
|
|
4240
|
+
}
|
|
4241
|
+
|
|
4242
|
+
interface PackageOptions {
|
|
4243
|
+
waypointNo?: number;
|
|
4244
|
+
careLevel?: number;
|
|
4245
|
+
category?: string;
|
|
4246
|
+
weight?: number;
|
|
4247
|
+
note?: string;
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
// Order Service
|
|
4251
|
+
type PaymentMode = 'cash' | 'card' | 'momo' | 'wigo';
|
|
4252
|
+
|
|
4253
|
+
interface OrderService {
|
|
4254
|
+
fees: {
|
|
4255
|
+
total: {
|
|
4256
|
+
amount: number;
|
|
4257
|
+
currency: string;
|
|
4258
|
+
};
|
|
4259
|
+
tax: number;
|
|
4260
|
+
discount: number;
|
|
4261
|
+
};
|
|
4262
|
+
payment: {
|
|
4263
|
+
mode: PaymentMode;
|
|
4264
|
+
paid: boolean;
|
|
4265
|
+
};
|
|
4266
|
+
xpress: string;
|
|
4267
|
+
}
|
|
4268
|
+
|
|
4269
|
+
interface OrderServiceOptions {
|
|
4270
|
+
'fees.total.amount'?: number;
|
|
4271
|
+
'fees.total.currency'?: string;
|
|
4272
|
+
'fees.tax'?: string;
|
|
4273
|
+
'fees.discount'?: string;
|
|
4274
|
+
'payment.mode'?: PaymentMode;
|
|
4275
|
+
'payment.option'?: string;
|
|
4276
|
+
'payment.paid'?: boolean;
|
|
4277
|
+
xpress?: string;
|
|
4278
|
+
}
|
|
4279
|
+
|
|
4280
|
+
interface OrderOperator {}
|
|
4281
|
+
|
|
4282
|
+
interface OrderStage {
|
|
4283
|
+
current: string;
|
|
4284
|
+
status: string;
|
|
4285
|
+
}
|
|
4286
|
+
|
|
4287
|
+
// Messages
|
|
4288
|
+
interface Message {
|
|
4289
|
+
type: 'text' | 'location' | 'media';
|
|
4290
|
+
sender: string;
|
|
4291
|
+
content: string;
|
|
4292
|
+
timestamp: string;
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4295
|
+
interface Peer {
|
|
4296
|
+
utype: string;
|
|
4297
|
+
id: string;
|
|
4298
|
+
}
|
|
4299
|
+
```
|
|
4300
|
+
|
|
4301
|
+
### Authentication Types
|
|
4302
|
+
|
|
4303
|
+
```typescript
|
|
4304
|
+
interface AuthOptions {
|
|
4305
|
+
env?: 'dev' | 'prod';
|
|
4306
|
+
version?: number;
|
|
4307
|
+
autorefresh?: boolean;
|
|
4308
|
+
onNewToken?: (token: string) => void;
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
interface AuthCredentials {
|
|
4312
|
+
workspace: string;
|
|
4313
|
+
remoteOrigin: string;
|
|
4314
|
+
cid: string;
|
|
4315
|
+
secret: string;
|
|
4316
|
+
}
|
|
4317
|
+
|
|
4318
|
+
interface SocketAuthCredentials {
|
|
4319
|
+
utype: string;
|
|
4320
|
+
id: string;
|
|
4321
|
+
remoteOrigin: string;
|
|
4322
|
+
accessToken: string;
|
|
4323
|
+
}
|
|
4324
|
+
|
|
4325
|
+
interface AuthRequestOptions {
|
|
4326
|
+
url: string;
|
|
4327
|
+
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
|
4328
|
+
headers?: { [index: string]: string };
|
|
4329
|
+
body?: any;
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
interface AccessOptions {
|
|
4333
|
+
env?: 'dev' | 'prod';
|
|
4334
|
+
version?: number;
|
|
4335
|
+
workspace: string;
|
|
4336
|
+
accessToken: string;
|
|
4337
|
+
remoteOrigin?: string;
|
|
4338
|
+
}
|
|
4339
|
+
```
|
|
4340
|
+
|
|
4341
|
+
---
|
|
4342
|
+
|
|
4343
|
+
## Support & Resources
|
|
4344
|
+
|
|
4345
|
+
- **Official Documentation**: https://docs.dedot.io
|
|
4346
|
+
- **API Reference**: https://docs.dedot.io/api
|
|
4347
|
+
- **Developer Portal**: https://developers.dedot.io
|
|
4348
|
+
- **GitHub**: https://github.com/dedot
|
|
4349
|
+
- **Community Forum**: https://community.dedot.io
|
|
4350
|
+
- **Support Email**: support@dedot.io
|
|
4351
|
+
- **Status Page**: https://status.dedot.io
|
|
4352
|
+
|
|
4353
|
+
---
|
|
4354
|
+
|
|
4355
|
+
## Changelog
|
|
4356
|
+
|
|
4357
|
+
### Version 2.0.0 (Current)
|
|
4358
|
+
- ✨ Added Handles API with streaming support
|
|
4359
|
+
- ✨ Introduced comprehensive Plugin system
|
|
4360
|
+
- ✨ Enhanced Access client with automatic retry and token management
|
|
4361
|
+
- ✨ Added full TypeScript type definitions
|
|
4362
|
+
- ✨ Improved error handling with 12-second timeout protection
|
|
4363
|
+
- ✨ Added support for multi-stop route optimization
|
|
4364
|
+
- ✨ Enhanced nearby entity management with live controls
|
|
4365
|
+
- 🐛 Fixed iframe communication stability issues
|
|
4366
|
+
- 📚 Complete documentation overhaul
|
|
4367
|
+
|
|
4368
|
+
### Version 1.x
|
|
4369
|
+
- 🎉 Initial release with Controls API
|
|
4370
|
+
- 🗺️ Basic map integration via iframe
|
|
4371
|
+
- 📍 Location services (GPS, geocoding)
|
|
4372
|
+
- 🚗 Simple vehicle tracking
|
|
4373
|
+
- 🛣️ Basic routing capabilities
|
|
4374
|
+
|
|
4375
|
+
---
|
|
4376
|
+
|
|
4377
|
+
## License
|
|
4378
|
+
|
|
4379
|
+
MIT License - See LICENSE file for details
|
|
4380
|
+
|
|
4381
|
+
---
|
|
4382
|
+
|
|
4383
|
+
*Last Updated: October 2025*
|
|
4384
|
+
*SDK Version: 2.0.0*
|
|
4385
|
+
*Compatible with: MSI Gateway v3.0+, De. Platform API v1.0+*
|
|
4386
|
+
|
|
4387
|
+
---
|
|
4388
|
+
|
|
4389
|
+
**Built by the De. Team**
|
|
4390
|
+
|
|
4391
|
+
*Making logistics as simple as flipping a switch.*
|