@madebylars.com/mbl-fleetmap 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/README.md +301 -0
- package/dist/module.d.mts +18 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +32 -0
- package/dist/runtime/components/MblFleetMap.d.vue.ts +20 -0
- package/dist/runtime/components/MblFleetMap.vue +41 -0
- package/dist/runtime/components/MblFleetMap.vue.d.ts +20 -0
- package/dist/runtime/composables/useDriverMarkers.d.ts +22 -0
- package/dist/runtime/composables/useDriverMarkers.js +72 -0
- package/dist/runtime/composables/useFleetMap.d.ts +7 -0
- package/dist/runtime/composables/useFleetMap.js +16 -0
- package/dist/runtime/composables/useFleetRoute.d.ts +772 -0
- package/dist/runtime/composables/useFleetRoute.js +28 -0
- package/dist/runtime/composables/useJobPins.d.ts +6 -0
- package/dist/runtime/composables/useJobPins.js +62 -0
- package/dist/runtime/composables/useMapEvents.d.ts +7 -0
- package/dist/runtime/composables/useMapEvents.js +19 -0
- package/dist/runtime/plugin.d.ts +2 -0
- package/dist/runtime/plugin.js +7 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/utils/events.d.ts +19 -0
- package/dist/runtime/utils/events.js +24 -0
- package/dist/runtime/utils/registry.d.ts +6 -0
- package/dist/runtime/utils/registry.js +10 -0
- package/dist/types.d.mts +3 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# mbl-fleetmap
|
|
2
|
+
|
|
3
|
+
[![npm version][npm-version-src]][npm-version-href]
|
|
4
|
+
[![License][license-src]][license-href]
|
|
5
|
+
[![Nuxt][nuxt-src]][nuxt-href]
|
|
6
|
+
|
|
7
|
+
A Nuxt 4 module that wraps Leaflet with a transport-specific composable API — driver markers, live position updates, route display, and pickup/dropoff pins. No API key required (OpenStreetMap tiles).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @madebylars.com/mbl-fleetmap
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Register the module in `nuxt.config.ts`:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
export default defineNuxtConfig({
|
|
21
|
+
modules: ['@madebylars.com/mbl-fleetmap'],
|
|
22
|
+
|
|
23
|
+
mblFleetMap: {
|
|
24
|
+
defaultCenter: [59.913, 10.752], // [lat, lng] — Oslo
|
|
25
|
+
defaultZoom: 13,
|
|
26
|
+
// tileUrl and attribution default to OpenStreetMap — no changes needed
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
That's it. The component and all composables are auto-imported.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Configuration options
|
|
36
|
+
|
|
37
|
+
| Option | Type | Default | Description |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| `tileUrl` | `string` | OpenStreetMap URL | Tile server URL template |
|
|
40
|
+
| `attribution` | `string` | `© OpenStreetMap contributors` | Map attribution text |
|
|
41
|
+
| `defaultZoom` | `number` | `13` | Initial zoom level |
|
|
42
|
+
| `defaultCenter` | `[number, number]` | `[51.505, -0.09]` | Initial `[lat, lng]` |
|
|
43
|
+
| `clusterMarkers` | `boolean` | `true` | Cluster driver markers when zoomed out |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
### 1. Add the map to a page
|
|
50
|
+
|
|
51
|
+
```vue
|
|
52
|
+
<template>
|
|
53
|
+
<div class="map-container">
|
|
54
|
+
<MblFleetMap
|
|
55
|
+
map-id="fleet"
|
|
56
|
+
@ready="onMapReady"
|
|
57
|
+
@click="onMapClick"
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<style scoped>
|
|
63
|
+
.map-container {
|
|
64
|
+
height: 100vh;
|
|
65
|
+
width: 100%;
|
|
66
|
+
}
|
|
67
|
+
</style>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The component is renderless — it fills whatever container you give it. Apply height via CSS; without a defined height Leaflet has nothing to render into.
|
|
71
|
+
|
|
72
|
+
### 2. Add drivers and routes on ready
|
|
73
|
+
|
|
74
|
+
```vue
|
|
75
|
+
<script setup lang="ts">
|
|
76
|
+
const { addDriver, updateDriver } = useDriverMarkers('fleet')
|
|
77
|
+
const { setPickup, setDropoff } = useJobPins('fleet')
|
|
78
|
+
const { drawRoute } = useFleetRoute('fleet')
|
|
79
|
+
|
|
80
|
+
function onMapReady() {
|
|
81
|
+
// Add a driver marker (green circle with initials)
|
|
82
|
+
addDriver({
|
|
83
|
+
driverId: 'd1',
|
|
84
|
+
name: 'Alice Smith',
|
|
85
|
+
lat: 59.913,
|
|
86
|
+
lng: 10.752,
|
|
87
|
+
status: 'active', // 'active' | 'idle' | 'offline'
|
|
88
|
+
jobId: 'job1',
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Pickup (green pin) and dropoff (red pin)
|
|
92
|
+
setPickup( 'job1', 59.920, 10.740, 'Oslo S')
|
|
93
|
+
setDropoff('job1', 59.905, 10.765, 'Grønland')
|
|
94
|
+
|
|
95
|
+
// Straight-line route (replace with OSRM/GraphHopper waypoints for real routing)
|
|
96
|
+
drawRoute('job1', [
|
|
97
|
+
[59.920, 10.740],
|
|
98
|
+
[59.913, 10.752],
|
|
99
|
+
[59.905, 10.765],
|
|
100
|
+
])
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function onMapClick({ lat, lng }: { lat: number; lng: number }) {
|
|
104
|
+
console.log('clicked', lat, lng)
|
|
105
|
+
}
|
|
106
|
+
</script>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Composables
|
|
112
|
+
|
|
113
|
+
All composables take a `mapId` string that matches the `map-id` prop on `<MblFleetMap>`. You can have multiple independent maps on one page by using different IDs.
|
|
114
|
+
|
|
115
|
+
### `useFleetMap(mapId)`
|
|
116
|
+
|
|
117
|
+
Access the raw Leaflet instance and control the viewport.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const {
|
|
121
|
+
map, // ComputedRef<L.Map | null> — null until 'ready' fires
|
|
122
|
+
isReady, // ComputedRef<boolean>
|
|
123
|
+
setCenter, // (lat, lng, zoom?) => void
|
|
124
|
+
fitBounds, // (bounds: L.LatLngBounds) => void
|
|
125
|
+
} = useFleetMap('fleet')
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `useDriverMarkers(mapId)`
|
|
129
|
+
|
|
130
|
+
Manage driver markers. Each marker is a coloured circle with the driver's initials (CSS only, no images).
|
|
131
|
+
|
|
132
|
+
| Status | Colour |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `active` | Green `#16a34a` |
|
|
135
|
+
| `idle` | Amber `#d97706` |
|
|
136
|
+
| `offline` | Grey `#6b7280` |
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const {
|
|
140
|
+
drivers, // Ref<Map<string, DriverMarker>>
|
|
141
|
+
addDriver, // (driver: DriverMarker) => void
|
|
142
|
+
updateDriver, // (driverId, [lat, lng]) => void — animates smoothly over 500 ms
|
|
143
|
+
removeDriver, // (driverId) => void
|
|
144
|
+
clearDrivers, // () => void
|
|
145
|
+
} = useDriverMarkers('fleet')
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Live position updates** — call `updateDriver` as often as you receive GPS ticks. The marker slides to its new position over 500 ms using a CSS transform transition.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
// e.g. fed by a WebSocket or polling composable
|
|
152
|
+
watch(livePosition, pos => {
|
|
153
|
+
updateDriver('d1', [pos.lat, pos.lng])
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### `useJobPins(mapId)`
|
|
158
|
+
|
|
159
|
+
Pickup (green teardrop) and dropoff (red teardrop) pins with hover tooltips. CSS only, no images.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const {
|
|
163
|
+
setPickup, // (jobId, lat, lng, label?) => void
|
|
164
|
+
setDropoff, // (jobId, lat, lng, label?) => void
|
|
165
|
+
clearJob, // (jobId) => void
|
|
166
|
+
clearAll, // () => void
|
|
167
|
+
} = useJobPins('fleet')
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### `useFleetRoute(mapId)`
|
|
171
|
+
|
|
172
|
+
Draw a polyline between waypoints (solid blue, weight 4).
|
|
173
|
+
|
|
174
|
+
> **Note:** The module draws straight lines between the waypoints you provide. Hook up OSRM, GraphHopper, or any routing API in your own code and pass the snapped waypoints here.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const {
|
|
178
|
+
routes, // Ref<Map<string, L.Polyline>>
|
|
179
|
+
drawRoute, // (jobId, waypoints: [lat, lng][]) => void
|
|
180
|
+
clearRoute, // (jobId) => void
|
|
181
|
+
clearAll, // () => void
|
|
182
|
+
} = useFleetRoute('fleet')
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### `useMapEvents(mapId)`
|
|
186
|
+
|
|
187
|
+
Subscribe to map and marker events. Call `offAll()` when the component that registered the listeners unmounts.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const { onDriverClick, onMapClick, onDriverHover, offAll } = useMapEvents('fleet')
|
|
191
|
+
|
|
192
|
+
onDriverClick(driver => {
|
|
193
|
+
// driver is the full DriverMarker object at the time of the click
|
|
194
|
+
openDriverPanel(driver.driverId)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
onDriverHover(driver => {
|
|
198
|
+
// driver is null when the cursor leaves a marker
|
|
199
|
+
hoveredDriver.value = driver
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
onMapClick((lat, lng) => {
|
|
203
|
+
console.log('map click', lat, lng)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
onUnmounted(offAll)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Multiple maps on one page
|
|
212
|
+
|
|
213
|
+
Use a different `mapId` for each `<MblFleetMap>` instance. Every composable scopes its state to the ID you pass, so they never interfere.
|
|
214
|
+
|
|
215
|
+
```vue
|
|
216
|
+
<!-- Dispatcher full-screen map -->
|
|
217
|
+
<MblFleetMap map-id="dispatch" @ready="setupDispatch" />
|
|
218
|
+
|
|
219
|
+
<!-- Small order-detail inset -->
|
|
220
|
+
<MblFleetMap map-id="order-detail" @ready="setupOrderDetail" />
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
const { addDriver } = useDriverMarkers('dispatch')
|
|
225
|
+
const { setDropoff } = useJobPins('order-detail')
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## TypeScript types
|
|
231
|
+
|
|
232
|
+
All types are exported from the package root:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
import type {
|
|
236
|
+
DriverMarker,
|
|
237
|
+
JobPin,
|
|
238
|
+
RouteOptions,
|
|
239
|
+
MarkerStatus,
|
|
240
|
+
LatLng,
|
|
241
|
+
} from '@madebylars.com/mbl-fleetmap'
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
interface DriverMarker {
|
|
246
|
+
driverId: string
|
|
247
|
+
name: string
|
|
248
|
+
lat: number
|
|
249
|
+
lng: number
|
|
250
|
+
status: 'active' | 'idle' | 'offline'
|
|
251
|
+
jobId?: string
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## SSR
|
|
258
|
+
|
|
259
|
+
Leaflet requires the browser DOM and cannot run server-side. The module handles this automatically:
|
|
260
|
+
|
|
261
|
+
- The plugin is registered as `mode: 'client'` — it never runs during SSR.
|
|
262
|
+
- `<MblFleetMap>` is registered as `mode: 'client'` — it is not rendered on the server.
|
|
263
|
+
- Composable methods are no-ops when called before the client plugin has initialised (which should never happen in normal use, since `ready` only fires client-side).
|
|
264
|
+
|
|
265
|
+
No `<ClientOnly>` wrapper or `process.client` guards are needed in your own code.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Integration with mbl-transport
|
|
270
|
+
|
|
271
|
+
Replace all `mbl-mapman` references:
|
|
272
|
+
|
|
273
|
+
| View | Composables |
|
|
274
|
+
|---|---|
|
|
275
|
+
| Dispatcher fleet | `useDriverMarkers('fleet')` fed by `useFleet()` from mbl-order · `useMapEvents('fleet').onDriverClick()` opens driver panel |
|
|
276
|
+
| Driver job navigation | `useJobPins` + `useFleetRoute` + `useDriverMarkers` with a single self-position marker |
|
|
277
|
+
| Customer order detail | Single driver marker + dropoff pin, read-only (no click handlers) |
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Contributing
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
npm install # install dependencies
|
|
285
|
+
npm run dev:prepare # generate type stubs
|
|
286
|
+
npm run dev # start playground at localhost:3000
|
|
287
|
+
npm run lint
|
|
288
|
+
npm run test
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
<!-- Badges -->
|
|
294
|
+
[npm-version-src]: https://img.shields.io/npm/v/@madebylars.com/mbl-fleetmap/latest.svg?style=flat&colorA=020420&colorB=00DC82
|
|
295
|
+
[npm-version-href]: https://npmjs.com/package/@madebylars.com/mbl-fleetmap
|
|
296
|
+
|
|
297
|
+
[license-src]: https://img.shields.io/npm/l/@madebylars.com/mbl-fleetmap.svg?style=flat&colorA=020420&colorB=00DC82
|
|
298
|
+
[license-href]: https://npmjs.com/package/@madebylars.com/mbl-fleetmap
|
|
299
|
+
|
|
300
|
+
[nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt
|
|
301
|
+
[nuxt-href]: https://nuxt.com
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
interface ModuleOptions {
|
|
4
|
+
tileUrl: string;
|
|
5
|
+
attribution: string;
|
|
6
|
+
defaultZoom: number;
|
|
7
|
+
defaultCenter: [number, number];
|
|
8
|
+
clusterMarkers: boolean;
|
|
9
|
+
}
|
|
10
|
+
declare module 'nuxt/schema' {
|
|
11
|
+
interface PublicRuntimeConfig {
|
|
12
|
+
mblFleetMap: ModuleOptions;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
16
|
+
|
|
17
|
+
export { _default as default };
|
|
18
|
+
export type { ModuleOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver, addPlugin, addComponent, addImportsDir } from '@nuxt/kit';
|
|
2
|
+
|
|
3
|
+
const module$1 = defineNuxtModule({
|
|
4
|
+
meta: {
|
|
5
|
+
name: "mbl-fleetmap",
|
|
6
|
+
configKey: "mblFleetMap"
|
|
7
|
+
},
|
|
8
|
+
defaults: {
|
|
9
|
+
tileUrl: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
|
10
|
+
attribution: "\xA9 OpenStreetMap contributors",
|
|
11
|
+
defaultZoom: 13,
|
|
12
|
+
defaultCenter: [51.505, -0.09],
|
|
13
|
+
clusterMarkers: true
|
|
14
|
+
},
|
|
15
|
+
setup(options, nuxt) {
|
|
16
|
+
const resolver = createResolver(import.meta.url);
|
|
17
|
+
nuxt.options.css.push("leaflet/dist/leaflet.css");
|
|
18
|
+
nuxt.options.runtimeConfig.public.mblFleetMap = options;
|
|
19
|
+
addPlugin({
|
|
20
|
+
src: resolver.resolve("./runtime/plugin"),
|
|
21
|
+
mode: "client"
|
|
22
|
+
});
|
|
23
|
+
addComponent({
|
|
24
|
+
name: "MblFleetMap",
|
|
25
|
+
filePath: resolver.resolve("./runtime/components/MblFleetMap.vue"),
|
|
26
|
+
mode: "client"
|
|
27
|
+
});
|
|
28
|
+
addImportsDir(resolver.resolve("./runtime/composables"));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export { module$1 as default };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
mapId: string;
|
|
3
|
+
center?: [number, number];
|
|
4
|
+
zoom?: number;
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
7
|
+
ready: () => any;
|
|
8
|
+
click: (args_0: {
|
|
9
|
+
lat: number;
|
|
10
|
+
lng: number;
|
|
11
|
+
}) => any;
|
|
12
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
13
|
+
onReady?: (() => any) | undefined;
|
|
14
|
+
onClick?: ((args_0: {
|
|
15
|
+
lat: number;
|
|
16
|
+
lng: number;
|
|
17
|
+
}) => any) | undefined;
|
|
18
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const _default: typeof __VLS_export;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { onMounted, onUnmounted, ref } from "vue";
|
|
3
|
+
import { useRuntimeConfig } from "#app";
|
|
4
|
+
import { registerMap, getMap, removeMap, getLeaflet } from "../utils/registry";
|
|
5
|
+
import { emitMapClick } from "../utils/events";
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
mapId: { type: String, required: true },
|
|
8
|
+
center: { type: Array, required: false },
|
|
9
|
+
zoom: { type: Number, required: false }
|
|
10
|
+
});
|
|
11
|
+
const emit = defineEmits(["ready", "click"]);
|
|
12
|
+
const containerEl = ref(null);
|
|
13
|
+
onMounted(() => {
|
|
14
|
+
const L = getLeaflet();
|
|
15
|
+
if (!L || !containerEl.value) return;
|
|
16
|
+
const config = useRuntimeConfig().public.mblFleetMap;
|
|
17
|
+
const map = L.map(containerEl.value, {
|
|
18
|
+
center: props.center ?? config.defaultCenter,
|
|
19
|
+
zoom: props.zoom ?? config.defaultZoom
|
|
20
|
+
});
|
|
21
|
+
L.tileLayer(config.tileUrl, {
|
|
22
|
+
attribution: config.attribution
|
|
23
|
+
}).addTo(map);
|
|
24
|
+
map.on("click", (e) => {
|
|
25
|
+
const payload = { lat: e.latlng.lat, lng: e.latlng.lng };
|
|
26
|
+
emitMapClick(props.mapId, payload);
|
|
27
|
+
emit("click", payload);
|
|
28
|
+
});
|
|
29
|
+
registerMap(props.mapId, map);
|
|
30
|
+
emit("ready");
|
|
31
|
+
});
|
|
32
|
+
onUnmounted(() => {
|
|
33
|
+
const map = getMap(props.mapId);
|
|
34
|
+
if (map) map.remove();
|
|
35
|
+
removeMap(props.mapId);
|
|
36
|
+
});
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<div ref="containerEl" />
|
|
41
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
mapId: string;
|
|
3
|
+
center?: [number, number];
|
|
4
|
+
zoom?: number;
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
7
|
+
ready: () => any;
|
|
8
|
+
click: (args_0: {
|
|
9
|
+
lat: number;
|
|
10
|
+
lng: number;
|
|
11
|
+
}) => any;
|
|
12
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
13
|
+
onReady?: (() => any) | undefined;
|
|
14
|
+
onClick?: ((args_0: {
|
|
15
|
+
lat: number;
|
|
16
|
+
lng: number;
|
|
17
|
+
}) => any) | undefined;
|
|
18
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const _default: typeof __VLS_export;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { DriverMarker, LatLng } from '../../types.js';
|
|
2
|
+
export declare function useDriverMarkers(mapId: string): {
|
|
3
|
+
addDriver: (driver: DriverMarker) => void;
|
|
4
|
+
updateDriver: (driverId: string, position: LatLng) => void;
|
|
5
|
+
removeDriver: (driverId: string) => void;
|
|
6
|
+
clearDrivers: () => void;
|
|
7
|
+
drivers: import("vue").Ref<Map<string, {
|
|
8
|
+
driverId: string;
|
|
9
|
+
name: string;
|
|
10
|
+
lat: number;
|
|
11
|
+
lng: number;
|
|
12
|
+
status: "active" | "idle" | "offline";
|
|
13
|
+
jobId?: string | undefined;
|
|
14
|
+
}> & Omit<Map<string, DriverMarker>, keyof Map<any, any>>, Map<string, DriverMarker> | (Map<string, {
|
|
15
|
+
driverId: string;
|
|
16
|
+
name: string;
|
|
17
|
+
lat: number;
|
|
18
|
+
lng: number;
|
|
19
|
+
status: "active" | "idle" | "offline";
|
|
20
|
+
jobId?: string | undefined;
|
|
21
|
+
}> & Omit<Map<string, DriverMarker>, keyof Map<any, any>>)>;
|
|
22
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
2
|
+
import { getMap, getLeaflet } from "../utils/registry.js";
|
|
3
|
+
import { emitDriverClick, emitDriverHover } from "../utils/events.js";
|
|
4
|
+
const STATUS_COLORS = {
|
|
5
|
+
active: "#16a34a",
|
|
6
|
+
idle: "#d97706",
|
|
7
|
+
offline: "#6b7280"
|
|
8
|
+
};
|
|
9
|
+
function initials(name) {
|
|
10
|
+
return name.split(" ").slice(0, 2).map((n) => n[0] ?? "").join("").toUpperCase();
|
|
11
|
+
}
|
|
12
|
+
function createDriverIcon(L, driver) {
|
|
13
|
+
const color = STATUS_COLORS[driver.status];
|
|
14
|
+
const label = initials(driver.name);
|
|
15
|
+
return L.divIcon({
|
|
16
|
+
className: "",
|
|
17
|
+
html: `<div style="
|
|
18
|
+
width:36px;height:36px;border-radius:50%;
|
|
19
|
+
background:${color};color:#fff;
|
|
20
|
+
display:flex;align-items:center;justify-content:center;
|
|
21
|
+
font-size:13px;font-weight:600;font-family:sans-serif;
|
|
22
|
+
box-shadow:0 2px 4px rgba(0,0,0,.3);
|
|
23
|
+
cursor:pointer;
|
|
24
|
+
">${label}</div>`,
|
|
25
|
+
iconSize: [36, 36],
|
|
26
|
+
iconAnchor: [18, 18]
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function useDriverMarkers(mapId) {
|
|
30
|
+
const drivers = ref(/* @__PURE__ */ new Map());
|
|
31
|
+
const markerInstances = /* @__PURE__ */ new Map();
|
|
32
|
+
const addDriver = (driver) => {
|
|
33
|
+
const L = getLeaflet();
|
|
34
|
+
const map = getMap(mapId);
|
|
35
|
+
if (!L || !map) return;
|
|
36
|
+
const icon = createDriverIcon(L, driver);
|
|
37
|
+
const marker = L.marker([driver.lat, driver.lng], { icon }).addTo(map);
|
|
38
|
+
const el = marker.getElement();
|
|
39
|
+
if (el) el.style.transition = "transform 500ms ease-out";
|
|
40
|
+
marker.on("click", () => {
|
|
41
|
+
const current = drivers.value.get(driver.driverId);
|
|
42
|
+
if (current) emitDriverClick(mapId, current);
|
|
43
|
+
});
|
|
44
|
+
marker.on("mouseover", () => {
|
|
45
|
+
const current = drivers.value.get(driver.driverId);
|
|
46
|
+
if (current) emitDriverHover(mapId, current);
|
|
47
|
+
});
|
|
48
|
+
marker.on("mouseout", () => emitDriverHover(mapId, null));
|
|
49
|
+
markerInstances.set(driver.driverId, marker);
|
|
50
|
+
drivers.value.set(driver.driverId, driver);
|
|
51
|
+
};
|
|
52
|
+
const updateDriver = (driverId, position) => {
|
|
53
|
+
const marker = markerInstances.get(driverId);
|
|
54
|
+
if (!marker) return;
|
|
55
|
+
marker.setLatLng(position);
|
|
56
|
+
const existing = drivers.value.get(driverId);
|
|
57
|
+
if (existing) {
|
|
58
|
+
drivers.value.set(driverId, { ...existing, lat: position[0], lng: position[1] });
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const removeDriver = (driverId) => {
|
|
62
|
+
markerInstances.get(driverId)?.remove();
|
|
63
|
+
markerInstances.delete(driverId);
|
|
64
|
+
drivers.value.delete(driverId);
|
|
65
|
+
};
|
|
66
|
+
const clearDrivers = () => {
|
|
67
|
+
markerInstances.forEach((m) => m.remove());
|
|
68
|
+
markerInstances.clear();
|
|
69
|
+
drivers.value.clear();
|
|
70
|
+
};
|
|
71
|
+
return { addDriver, updateDriver, removeDriver, clearDrivers, drivers };
|
|
72
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type L from 'leaflet';
|
|
2
|
+
export declare function useFleetMap(mapId: string): {
|
|
3
|
+
map: import("vue").ComputedRef<L.Map | null>;
|
|
4
|
+
setCenter: (lat: number, lng: number, zoom?: number) => void;
|
|
5
|
+
fitBounds: (bounds: L.LatLngBounds) => void;
|
|
6
|
+
isReady: import("vue").ComputedRef<boolean>;
|
|
7
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { getMap } from "../utils/registry.js";
|
|
3
|
+
export function useFleetMap(mapId) {
|
|
4
|
+
const map = computed(() => getMap(mapId));
|
|
5
|
+
const isReady = computed(() => getMap(mapId) !== null);
|
|
6
|
+
const setCenter = (lat, lng, zoom) => {
|
|
7
|
+
const m = getMap(mapId);
|
|
8
|
+
if (!m) return;
|
|
9
|
+
if (zoom !== void 0) m.setView([lat, lng], zoom);
|
|
10
|
+
else m.panTo([lat, lng]);
|
|
11
|
+
};
|
|
12
|
+
const fitBounds = (bounds) => {
|
|
13
|
+
getMap(mapId)?.fitBounds(bounds);
|
|
14
|
+
};
|
|
15
|
+
return { map, setCenter, fitBounds, isReady };
|
|
16
|
+
}
|