wovvmap-webview-bridge 1.0.28 → 1.0.39
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 +1563 -221
- package/dist/handlers/WebBridgeHandlers.d.ts +3 -9
- package/dist/handlers/WebBridgeHandlers.d.ts.map +1 -1
- package/dist/handlers/WebBridgeHandlers.js +94 -51
- package/dist/index.d.ts +19 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -3
- package/dist/types/types.d.ts +280 -302
- package/dist/types/types.d.ts.map +1 -1
- package/dist/web.d.ts +19 -3
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +19 -3
- package/dist/webviewBridge/BridgeService.d.ts +34 -15
- package/dist/webviewBridge/BridgeService.d.ts.map +1 -1
- package/dist/webviewBridge/BridgeService.js +227 -45
- package/dist/webviewBridge/WebViewBridgeRef.d.ts +3 -3
- package/dist/webviewBridge/WebViewBridgeRef.d.ts.map +1 -1
- package/dist/webviewBridge/WebViewBridgeRef.js +1 -1
- package/dist/webviewBridge/WebViewScreen.d.ts.map +1 -1
- package/dist/webviewBridge/services/DirectionBridgeService.d.ts +32 -0
- package/dist/webviewBridge/services/DirectionBridgeService.d.ts.map +1 -0
- package/dist/webviewBridge/services/DirectionBridgeService.js +72 -0
- package/dist/webviewBridge/services/FloorBridgeService.d.ts +4 -0
- package/dist/webviewBridge/services/FloorBridgeService.d.ts.map +1 -0
- package/dist/webviewBridge/services/FloorBridgeService.js +11 -0
- package/dist/webviewBridge/services/SelectionBridgeService.d.ts +10 -0
- package/dist/webviewBridge/services/SelectionBridgeService.d.ts.map +1 -0
- package/dist/webviewBridge/services/SelectionBridgeService.js +43 -0
- package/dist/webviewBridge/services/StepNavigationBridgeService.d.ts +7 -0
- package/dist/webviewBridge/services/StepNavigationBridgeService.d.ts.map +1 -0
- package/dist/webviewBridge/services/StepNavigationBridgeService.js +15 -0
- package/dist/webviewBridge/services/ViewerBridgeService.d.ts +8 -0
- package/dist/webviewBridge/services/ViewerBridgeService.d.ts.map +1 -0
- package/dist/webviewBridge/services/ViewerBridgeService.js +20 -0
- package/dist/webviewBridge/services/ZoomBridgeService.d.ts +5 -0
- package/dist/webviewBridge/services/ZoomBridgeService.d.ts.map +1 -0
- package/dist/webviewBridge/services/ZoomBridgeService.js +9 -0
- package/dist/webviewBridge/store/useAmenityStore.d.ts +20 -0
- package/dist/webviewBridge/store/useAmenityStore.d.ts.map +1 -0
- package/dist/webviewBridge/store/useAmenityStore.js +6 -0
- package/dist/webviewBridge/store/useBridgeStorage.d.ts +14 -0
- package/dist/webviewBridge/store/useBridgeStorage.d.ts.map +1 -0
- package/dist/webviewBridge/store/useBridgeStorage.js +13 -0
- package/dist/webviewBridge/store/useCategoryStore.d.ts +20 -0
- package/dist/webviewBridge/store/useCategoryStore.d.ts.map +1 -0
- package/dist/webviewBridge/store/useCategoryStore.js +6 -0
- package/dist/webviewBridge/store/useDirectionStore.d.ts +55 -0
- package/dist/webviewBridge/store/useDirectionStore.d.ts.map +1 -0
- package/dist/webviewBridge/store/useDirectionStore.js +35 -0
- package/dist/webviewBridge/store/useDirectionsRouteRequestStore.d.ts +14 -0
- package/dist/webviewBridge/store/useDirectionsRouteRequestStore.d.ts.map +1 -0
- package/dist/webviewBridge/store/useDirectionsRouteRequestStore.js +25 -0
- package/dist/webviewBridge/store/useFloorStore.d.ts +18 -0
- package/dist/webviewBridge/store/useFloorStore.d.ts.map +1 -0
- package/dist/webviewBridge/store/useFloorStore.js +8 -0
- package/dist/webviewBridge/store/useNavigationStore.d.ts +23 -0
- package/dist/webviewBridge/store/useNavigationStore.d.ts.map +1 -0
- package/dist/webviewBridge/store/useNavigationStore.js +18 -0
- package/dist/webviewBridge/store/useNodePointStore.d.ts +42 -0
- package/dist/webviewBridge/store/useNodePointStore.d.ts.map +1 -0
- package/dist/webviewBridge/store/useNodePointStore.js +19 -0
- package/dist/webviewBridge/store/useSelectionStore.d.ts +35 -0
- package/dist/webviewBridge/store/useSelectionStore.d.ts.map +1 -0
- package/dist/webviewBridge/store/useSelectionStore.js +32 -0
- package/dist/webviewBridge/store/useSubCategoryStore.d.ts +20 -0
- package/dist/webviewBridge/store/useSubCategoryStore.d.ts.map +1 -0
- package/dist/webviewBridge/store/useSubCategoryStore.js +6 -0
- package/dist/webviewBridge/store/useViewerStore.d.ts +35 -0
- package/dist/webviewBridge/store/useViewerStore.d.ts.map +1 -0
- package/dist/webviewBridge/store/useViewerStore.js +35 -0
- package/dist/webviewBridge/useBridgeStorage.d.ts +45 -0
- package/dist/webviewBridge/useBridgeStorage.d.ts.map +1 -0
- package/dist/webviewBridge/useBridgeStorage.js +43 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,282 +1,1624 @@
|
|
|
1
1
|
# wovvmap-webview-bridge
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
-
|
|
3
|
+
`wovvmap-webview-bridge` is the host-side SDK for embedding a WovvMap viewer inside a web iframe or a React Native WebView.
|
|
4
|
+
|
|
5
|
+
It gives the host app:
|
|
6
|
+
|
|
7
|
+
- iframe/WebView components
|
|
8
|
+
- typed bridge command services
|
|
9
|
+
- synced Zustand stores for viewer data
|
|
10
|
+
- event handlers for viewer interactions
|
|
11
|
+
- TypeScript types for categories, stores, directions, floors, navigation, and viewer configuration
|
|
12
|
+
|
|
13
|
+
The package is designed so a host developer does not need to create bridge command services manually. UI code should call the exported `*BridgeService` classes and read state from the exported stores.
|
|
14
|
+
|
|
15
|
+
## Entry Points
|
|
16
|
+
|
|
17
|
+
Use the web entry point in React web apps:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import {
|
|
21
|
+
WebIframeScreen,
|
|
22
|
+
DirectionBridgeService,
|
|
23
|
+
useCategoryStore,
|
|
24
|
+
} from "wovvmap-webview-bridge/web";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Use the root entry point in React Native apps:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import {
|
|
31
|
+
WebViewScreen,
|
|
32
|
+
DirectionBridgeService,
|
|
33
|
+
useCategoryStore,
|
|
34
|
+
} from "wovvmap-webview-bridge";
|
|
35
|
+
```
|
|
8
36
|
|
|
9
37
|
## Installation
|
|
10
38
|
|
|
39
|
+
For web iframe hosts:
|
|
40
|
+
|
|
11
41
|
```bash
|
|
12
|
-
npm install
|
|
13
|
-
# or
|
|
14
|
-
yarn add react-native-webview zustand
|
|
42
|
+
npm install wovvmap-webview-bridge zustand
|
|
15
43
|
```
|
|
16
44
|
|
|
17
|
-
|
|
45
|
+
For React Native hosts:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install wovvmap-webview-bridge zustand react-native-webview
|
|
49
|
+
```
|
|
18
50
|
|
|
19
|
-
|
|
51
|
+
## Basic Web Iframe Setup
|
|
20
52
|
|
|
21
|
-
|
|
53
|
+
The host app owns the map id. The package does not store `mapId` because each host decides which map to open.
|
|
22
54
|
|
|
23
55
|
```tsx
|
|
24
|
-
import React from "react";
|
|
25
56
|
import { WebIframeScreen } from "wovvmap-webview-bridge/web";
|
|
26
57
|
|
|
27
|
-
export
|
|
58
|
+
export function MapHost({ mapId }: { mapId: string | null }) {
|
|
59
|
+
if (!mapId) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
28
63
|
return (
|
|
29
64
|
<WebIframeScreen
|
|
30
|
-
url=
|
|
31
|
-
origin="https://
|
|
32
|
-
onload={() => console.log("iframe loaded")}
|
|
33
|
-
style={{ width: "100%", height: "
|
|
65
|
+
url={`https://viewer.example.com/viewer/${mapId}?v=v2&template=embedded`}
|
|
66
|
+
origin="https://viewer.example.com"
|
|
67
|
+
onload={() => console.log("Viewer iframe loaded")}
|
|
68
|
+
style={{ width: "100%", height: "100%", border: "none" }}
|
|
34
69
|
/>
|
|
35
70
|
);
|
|
36
71
|
}
|
|
37
72
|
```
|
|
38
73
|
|
|
39
|
-
|
|
74
|
+
For local development this can look like:
|
|
40
75
|
|
|
41
76
|
```tsx
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
77
|
+
<WebIframeScreen
|
|
78
|
+
url={`http://localhost:5174/viewer/${mapId}?v=v2&template=embedded`}
|
|
79
|
+
origin="http://localhost:5174"
|
|
80
|
+
style={{ width: "100%", height: "100%", border: "none" }}
|
|
81
|
+
/>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Basic React Native Setup
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import { WebViewScreen } from "wovvmap-webview-bridge";
|
|
88
|
+
|
|
89
|
+
export function MapWebView({ mapId }: { mapId: string }) {
|
|
90
|
+
return (
|
|
91
|
+
<WebViewScreen
|
|
92
|
+
url={`https://viewer.example.com/viewer/${mapId}?v=v2&template=embedded`}
|
|
93
|
+
onload={() => console.log("Viewer loaded")}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Optional Manual Iframe Setup
|
|
100
|
+
|
|
101
|
+
Use `attachIframeBridge` when you already render your own iframe.
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { useEffect, useRef } from "react";
|
|
105
|
+
import { attachIframeBridge } from "wovvmap-webview-bridge/web";
|
|
48
106
|
|
|
49
|
-
export
|
|
107
|
+
export function CustomIframe({ url }: { url: string }) {
|
|
50
108
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
51
109
|
|
|
52
110
|
useEffect(() => {
|
|
53
111
|
if (!iframeRef.current) return;
|
|
54
112
|
|
|
55
|
-
|
|
113
|
+
return attachIframeBridge({
|
|
56
114
|
iframe: iframeRef.current,
|
|
57
|
-
origin: "https://
|
|
58
|
-
onLoad: () =>
|
|
115
|
+
origin: "https://viewer.example.com",
|
|
116
|
+
onLoad: () => console.log("loaded"),
|
|
59
117
|
});
|
|
118
|
+
}, []);
|
|
60
119
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
120
|
+
return <iframe ref={iframeRef} src={url} title="Wovv Map" />;
|
|
121
|
+
}
|
|
122
|
+
```
|
|
64
123
|
|
|
65
|
-
|
|
66
|
-
|
|
124
|
+
## Recommended Import Surface
|
|
125
|
+
|
|
126
|
+
Most apps should import only from `"wovvmap-webview-bridge/web"` or `"wovvmap-webview-bridge"`.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import {
|
|
130
|
+
WebIframeScreen,
|
|
131
|
+
DirectionBridgeService,
|
|
132
|
+
SelectionBridgeService,
|
|
133
|
+
FloorBridgeService,
|
|
134
|
+
StepNavigationBridgeService,
|
|
135
|
+
ZoomBridgeService,
|
|
136
|
+
ViewerBridgeService,
|
|
137
|
+
useBridgeStorage,
|
|
138
|
+
useViewerStore,
|
|
139
|
+
useCategoryStore,
|
|
140
|
+
useSubCategoryStore,
|
|
141
|
+
useAmenityStore,
|
|
142
|
+
useNodePointStore,
|
|
143
|
+
useSelectionStore,
|
|
144
|
+
useDirectionStore,
|
|
145
|
+
useFloorStore,
|
|
146
|
+
useNavigationStore,
|
|
147
|
+
useDirectionsRouteRequestStore,
|
|
148
|
+
registerBridgeHandler,
|
|
149
|
+
} from "wovvmap-webview-bridge/web";
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Exported Components
|
|
153
|
+
|
|
154
|
+
### `WebIframeScreen`
|
|
155
|
+
|
|
156
|
+
Web-only component for embedding the WovvMap viewer in an iframe.
|
|
157
|
+
|
|
158
|
+
Props:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
type WebIframeScreenProps = {
|
|
162
|
+
url: string;
|
|
163
|
+
origin?: string;
|
|
164
|
+
onload?: () => void;
|
|
165
|
+
title?: string;
|
|
166
|
+
style?: React.CSSProperties;
|
|
167
|
+
};
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Use this in React web apps.
|
|
171
|
+
|
|
172
|
+
### `WebViewScreen`
|
|
173
|
+
|
|
174
|
+
React Native component for embedding the WovvMap viewer in `react-native-webview`.
|
|
175
|
+
|
|
176
|
+
Props:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
type WebViewScreenProps = {
|
|
180
|
+
url: string;
|
|
181
|
+
onload?: () => void;
|
|
182
|
+
};
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Use this in React Native apps.
|
|
186
|
+
|
|
187
|
+
### `attachIframeBridge`
|
|
188
|
+
|
|
189
|
+
Manual bridge attachment for custom iframe implementations.
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
attachIframeBridge({
|
|
193
|
+
iframe,
|
|
194
|
+
origin,
|
|
195
|
+
onLoad,
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Command Services
|
|
200
|
+
|
|
201
|
+
Use these services from host UI. They update host-side package stores when needed and send the correct bridge event to the embedded viewer.
|
|
202
|
+
|
|
203
|
+
### `DirectionBridgeService`
|
|
204
|
+
|
|
205
|
+
Controls origin, destination, route generation, path clearing, and origin/destination swap.
|
|
206
|
+
|
|
207
|
+
Methods:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
DirectionBridgeService.configure(config);
|
|
211
|
+
DirectionBridgeService.setDestination(nodeId, cameraAnimate, clearSelection);
|
|
212
|
+
DirectionBridgeService.setOrigin(nodeId, cameraAnimate, clearSelection);
|
|
213
|
+
DirectionBridgeService.clearDestination(clearQuery);
|
|
214
|
+
DirectionBridgeService.clearOrigin();
|
|
215
|
+
DirectionBridgeService.swapOriginDestination();
|
|
216
|
+
DirectionBridgeService.generateDirectionsRoute(callbacks);
|
|
217
|
+
DirectionBridgeService.clearActivePath();
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Parameters:
|
|
221
|
+
|
|
222
|
+
- `nodeId`: a node key string. Multiple store locations can be joined with `"<->"`, for example `"2105,1601,3<->3166,1570,3"`.
|
|
223
|
+
- `cameraAnimate`: when `true`, viewer camera animates to the point.
|
|
224
|
+
- `clearSelection`: when `true`, viewer clears active category/subcategory/amenity selection.
|
|
225
|
+
- `clearQuery`: when `true`, your host input adapter clears the visible input text.
|
|
226
|
+
|
|
227
|
+
Basic destination example:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
DirectionBridgeService.setDestination("2105,1601,3", true, false);
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Basic origin example:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
DirectionBridgeService.setOrigin("4360,2330,5", true, true);
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Clear destination:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
DirectionBridgeService.clearDestination(true);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Generate route:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
DirectionBridgeService.generateDirectionsRoute({
|
|
249
|
+
onSuccess: (result) => {
|
|
250
|
+
console.log("Route ready", result.navigationResult);
|
|
251
|
+
},
|
|
252
|
+
onError: (result) => {
|
|
253
|
+
console.log("Route failed", result.error);
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Swap from/to:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
DirectionBridgeService.swapOriginDestination();
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Clear drawn path:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
DirectionBridgeService.clearActivePath();
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
#### Direction Input Adapter
|
|
271
|
+
|
|
272
|
+
The package does not own your input UI state. If your app has local "From" and "To" input text, connect it once with `configure`.
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
import {
|
|
276
|
+
DirectionBridgeService,
|
|
277
|
+
SelectionBridgeService,
|
|
278
|
+
useNodePointStore,
|
|
279
|
+
} from "wovvmap-webview-bridge/web";
|
|
280
|
+
import { useDirectionInputStore } from "./store/useDirectionInputStore";
|
|
281
|
+
|
|
282
|
+
DirectionBridgeService.configure({
|
|
283
|
+
directionInputAdapter: {
|
|
284
|
+
setDestinationState: (query, selectedId) => {
|
|
285
|
+
useDirectionInputStore.getState().setDestinationState(query, selectedId);
|
|
286
|
+
},
|
|
287
|
+
setOriginState: (query, selectedId) => {
|
|
288
|
+
useDirectionInputStore.getState().setOriginState(query, selectedId);
|
|
289
|
+
},
|
|
290
|
+
clearDestinationState: (clearQuery) => {
|
|
291
|
+
useDirectionInputStore.getState().clearDestinationState(clearQuery);
|
|
292
|
+
},
|
|
293
|
+
clearOriginState: () => {
|
|
294
|
+
useDirectionInputStore.getState().clearOriginState();
|
|
295
|
+
},
|
|
296
|
+
setActiveDirectionField: (field) => {
|
|
297
|
+
useDirectionInputStore.getState().setActiveDirectionField(field);
|
|
298
|
+
},
|
|
299
|
+
getOriginQuery: () => useDirectionInputStore.getState().originQuery,
|
|
300
|
+
getDestinationQuery: () => useDirectionInputStore.getState().destinationQuery,
|
|
301
|
+
},
|
|
302
|
+
resolveLocationName: (nodeId) => {
|
|
303
|
+
const nodeKey = nodeId.split("<->")[0];
|
|
304
|
+
const node = useNodePointStore.getState().getNodePointByKey(nodeKey);
|
|
305
|
+
return node?.assets?.locationName?.text || nodeKey;
|
|
306
|
+
},
|
|
307
|
+
onClearSelection: () => {
|
|
308
|
+
SelectionBridgeService.clearSelection();
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Why this exists:
|
|
314
|
+
|
|
315
|
+
- different companies can build different input UIs
|
|
316
|
+
- the SDK still updates local input text when `setOrigin`, `setDestination`, or `swapOriginDestination` is called
|
|
317
|
+
- SDK code does not import app-specific stores
|
|
318
|
+
|
|
319
|
+
If you do not configure an adapter, direction commands still work. Only local input text will not auto-update.
|
|
320
|
+
|
|
321
|
+
### `SelectionBridgeService`
|
|
322
|
+
|
|
323
|
+
Controls category, subcategory, amenity, offers, and clear selection.
|
|
324
|
+
|
|
325
|
+
Methods:
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
SelectionBridgeService.selectCategory(category);
|
|
329
|
+
SelectionBridgeService.selectSubCategory(subCategory);
|
|
330
|
+
SelectionBridgeService.selectAmenity(amenity);
|
|
331
|
+
SelectionBridgeService.toggleOffers();
|
|
332
|
+
SelectionBridgeService.clearSelection();
|
|
333
|
+
SelectionBridgeService.syncHighlightsWithState();
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Category example:
|
|
337
|
+
|
|
338
|
+
```tsx
|
|
339
|
+
import { SelectionBridgeService, useCategoryStore } from "wovvmap-webview-bridge/web";
|
|
340
|
+
|
|
341
|
+
function CategoryButton({ code }: { code: string }) {
|
|
342
|
+
const category = useCategoryStore((s) => s.categories[code]);
|
|
343
|
+
const activeSelection = useSelectionStore((s) => s.activeSelection);
|
|
344
|
+
|
|
345
|
+
if (!category) return null;
|
|
346
|
+
|
|
347
|
+
const isActive =
|
|
348
|
+
activeSelection.type === "Category" &&
|
|
349
|
+
activeSelection.id === category.code.toString();
|
|
67
350
|
|
|
68
351
|
return (
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
352
|
+
<button
|
|
353
|
+
className={isActive ? "active" : ""}
|
|
354
|
+
onClick={() => SelectionBridgeService.selectCategory(category)}
|
|
355
|
+
>
|
|
356
|
+
{category.name}
|
|
357
|
+
</button>
|
|
75
358
|
);
|
|
76
359
|
}
|
|
77
360
|
```
|
|
78
361
|
|
|
79
|
-
|
|
362
|
+
Subcategory example:
|
|
80
363
|
|
|
81
364
|
```tsx
|
|
82
|
-
|
|
83
|
-
|
|
365
|
+
SelectionBridgeService.selectSubCategory(subCategory);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Amenity example:
|
|
369
|
+
|
|
370
|
+
```tsx
|
|
371
|
+
SelectionBridgeService.selectAmenity(amenity);
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Amenity selection also sets the destination with the amenity node keys:
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
DirectionBridgeService.setDestination(amenity.nodePointsKey.join("<->"), true, false);
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Offers button example:
|
|
381
|
+
|
|
382
|
+
```tsx
|
|
383
|
+
import { SelectionBridgeService, useNodePointStore, useSelectionStore } from "wovvmap-webview-bridge/web";
|
|
384
|
+
|
|
385
|
+
export function OffersButton() {
|
|
386
|
+
const offersNodeMap = useNodePointStore((s) => s.offersNodeMap);
|
|
387
|
+
const isOfferSelected = useSelectionStore((s) => s.isOfferSelected);
|
|
388
|
+
const hasOffers = (offersNodeMap.all_offers?.nodePointsKey?.length ?? 0) > 0;
|
|
84
389
|
|
|
85
|
-
|
|
86
|
-
|
|
390
|
+
if (!hasOffers) return null;
|
|
391
|
+
|
|
392
|
+
return (
|
|
393
|
+
<button
|
|
394
|
+
className={isOfferSelected ? "active" : ""}
|
|
395
|
+
onClick={() => SelectionBridgeService.toggleOffers()}
|
|
396
|
+
>
|
|
397
|
+
Offers
|
|
398
|
+
</button>
|
|
399
|
+
);
|
|
87
400
|
}
|
|
88
401
|
```
|
|
89
402
|
|
|
90
|
-
|
|
403
|
+
Important selection states:
|
|
404
|
+
|
|
405
|
+
- Category active: `useSelectionStore((s) => s.activeSelection.type === "Category")`
|
|
406
|
+
- Selected category id: `useSelectionStore((s) => s.activeSelection.id)`
|
|
407
|
+
- Active category object: `useSelectionStore((s) => s.activeCategory)`
|
|
408
|
+
- Subcategory active: `activeSelection.type === "SubCategory"`
|
|
409
|
+
- Amenity active: `activeSelection.type === "Amenity"`
|
|
410
|
+
- Offers active: `useSelectionStore((s) => s.isOfferSelected)`
|
|
411
|
+
|
|
412
|
+
### `FloorBridgeService`
|
|
413
|
+
|
|
414
|
+
Controls active floor.
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
FloorBridgeService.changeFloor(floorIndex, animateCamera);
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Floor selector example:
|
|
91
421
|
|
|
92
422
|
```tsx
|
|
93
|
-
import {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
sendPathPreBtnClick,
|
|
99
|
-
sendPathFinishBtnClick,
|
|
100
|
-
sendSelectCategory,
|
|
101
|
-
sendZoomIn,
|
|
102
|
-
sendZoomOut,
|
|
103
|
-
sendClearStartAndEndPoint,
|
|
104
|
-
sendPathFilter,
|
|
105
|
-
sendGetDirectionToBridge,
|
|
106
|
-
sendNavigateToBridge,
|
|
107
|
-
sendMapThemeToBridge,
|
|
108
|
-
} from "wovvmap-webview-bridge";
|
|
423
|
+
import { FloorBridgeService, useFloorStore } from "wovvmap-webview-bridge/web";
|
|
424
|
+
|
|
425
|
+
export function FloorSelector() {
|
|
426
|
+
const floors = useFloorStore((s) => s.floors);
|
|
427
|
+
const activeFloor = useFloorStore((s) => s.activeFloor);
|
|
109
428
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
sendMapThemeToBridge({ "theme-path-color": "#00AAFF" });
|
|
429
|
+
return (
|
|
430
|
+
<select
|
|
431
|
+
value={activeFloor ?? ""}
|
|
432
|
+
onChange={(event) => FloorBridgeService.changeFloor(Number(event.target.value), true)}
|
|
433
|
+
>
|
|
434
|
+
{floors.map((floor) => (
|
|
435
|
+
<option key={floor.index ?? floor.FloorNumber} value={floor.index}>
|
|
436
|
+
{floor.FloorName}
|
|
437
|
+
</option>
|
|
438
|
+
))}
|
|
439
|
+
</select>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
124
442
|
```
|
|
125
443
|
|
|
126
|
-
###
|
|
444
|
+
### `StepNavigationBridgeService`
|
|
445
|
+
|
|
446
|
+
Controls current step in step-by-step navigation.
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
StepNavigationBridgeService.goToStep(index);
|
|
450
|
+
StepNavigationBridgeService.nextStep();
|
|
451
|
+
StepNavigationBridgeService.prevStep();
|
|
452
|
+
StepNavigationBridgeService.finishStep();
|
|
453
|
+
```
|
|
127
454
|
|
|
128
|
-
|
|
455
|
+
Step list example:
|
|
129
456
|
|
|
130
457
|
```tsx
|
|
131
|
-
import {
|
|
458
|
+
import { StepNavigationBridgeService, useNavigationStore } from "wovvmap-webview-bridge/web";
|
|
132
459
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
460
|
+
export function StepList() {
|
|
461
|
+
const result = useNavigationStore((s) => s.navigationResult);
|
|
462
|
+
const currentStepIndex = useNavigationStore((s) => s.currentStepIndex);
|
|
136
463
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
464
|
+
if (!result) return null;
|
|
465
|
+
|
|
466
|
+
return (
|
|
467
|
+
<div>
|
|
468
|
+
{result.steps.map((step, index) => (
|
|
469
|
+
<button
|
|
470
|
+
key={step.id}
|
|
471
|
+
className={currentStepIndex === index ? "active" : ""}
|
|
472
|
+
onClick={() => StepNavigationBridgeService.goToStep(index)}
|
|
473
|
+
>
|
|
474
|
+
{step.instruction}
|
|
475
|
+
</button>
|
|
476
|
+
))}
|
|
477
|
+
</div>
|
|
478
|
+
);
|
|
479
|
+
}
|
|
140
480
|
```
|
|
141
481
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
```tsx
|
|
145
|
-
import { useBridgeStorage } from "wovvmap-webview-bridge";
|
|
146
|
-
|
|
147
|
-
const state = useBridgeStorage((s) => ({
|
|
148
|
-
isBridgeLoaded: s.isBridgeLoaded,
|
|
149
|
-
isMapLoaded: s.isMapLoaded,
|
|
150
|
-
searchablePoints: s.searchablePoints,
|
|
151
|
-
amenities: s.amenities,
|
|
152
|
-
allOffers: s.allOffers,
|
|
153
|
-
activeFloor: s.activeFloor,
|
|
154
|
-
stepByStepList: s.stepByStepList,
|
|
155
|
-
pathSummary: s.pathSummary,
|
|
156
|
-
categories: s.categories,
|
|
157
|
-
subCategories: s.subCategories,
|
|
158
|
-
}));
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
Store fields:
|
|
162
|
-
- isBridgeLoaded
|
|
163
|
-
- isMapLoaded
|
|
164
|
-
- searchablePoints
|
|
165
|
-
- amenities
|
|
166
|
-
- allOffers
|
|
167
|
-
- activeFloor
|
|
168
|
-
- elevator, escalator
|
|
169
|
-
- floorImages
|
|
170
|
-
- stepByStepList
|
|
171
|
-
- pathSummary
|
|
172
|
-
- nextPreState
|
|
173
|
-
- pointsByKey
|
|
174
|
-
- categories
|
|
175
|
-
- subCategories
|
|
176
|
-
- cameraControllerState
|
|
177
|
-
|
|
178
|
-
## Bridge message contracts
|
|
179
|
-
|
|
180
|
-
### IncomingMessage (Web -> RN)
|
|
181
|
-
Keys and payloads:
|
|
182
|
-
- pong: boolean
|
|
183
|
-
- isConnection: boolean
|
|
184
|
-
- mapLoaded: boolean
|
|
185
|
-
- _searchablePoints: NodePoint[]
|
|
186
|
-
- _allAmenities: AmenityWithNodePoint[]
|
|
187
|
-
- _allOffers: string[]
|
|
188
|
-
- _activeFloor: number
|
|
189
|
-
- FloorImg: FloorImage[]
|
|
190
|
-
- categories: Record<ExternalId, Category>
|
|
191
|
-
- subCategories: Record<ExternalId, SubCategory>
|
|
192
|
-
- isSceneClick: no value
|
|
193
|
-
- isShapeClick: NodePoint
|
|
194
|
-
- stepByStepList: StepByStepResult
|
|
195
|
-
- pathNextPreState: NavState
|
|
196
|
-
- cameraControllerState: { cameraPosition: CameraPosition; controlsPosition: ControlsPosition }
|
|
197
|
-
|
|
198
|
-
### OutgoingMessage (RN -> Web)
|
|
199
|
-
Keys and payloads:
|
|
200
|
-
- applyCSS: { selector: string; style: Partial<CSSStyleDeclaration> }
|
|
201
|
-
- ping: boolean
|
|
202
|
-
- setEndPoint: string
|
|
203
|
-
- setStartPoint: string
|
|
204
|
-
- setActiveFloor: number
|
|
205
|
-
- pathNextBtnClick: no value
|
|
206
|
-
- pathPreBtnClick: no value
|
|
207
|
-
- pathFinishBtnClick: no value
|
|
208
|
-
- setSelectCategory: string | string[] | null
|
|
209
|
-
- zoomIn: no value
|
|
210
|
-
- zoomOut: no value
|
|
211
|
-
- clearStartAndEndPoint: no value
|
|
212
|
-
- setPathFilter: filterPath
|
|
213
|
-
- getDirection: no value
|
|
214
|
-
- setMapTheme: Theme | null
|
|
215
|
-
- navigateTo: string
|
|
216
|
-
- editableView: MapApiResponse
|
|
217
|
-
- pathHighlightByStepIndex: number
|
|
218
|
-
|
|
219
|
-
## Exported API
|
|
482
|
+
Next/previous example:
|
|
220
483
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
- sendStartPointToBridge
|
|
227
|
-
- sendEndPointToBridge
|
|
228
|
-
- sendActiveFloorToBridge
|
|
229
|
-
- sendPathNextBtnClick
|
|
230
|
-
- sendPathPreBtnClick
|
|
231
|
-
- sendPathFinishBtnClick
|
|
232
|
-
- sendSelectCategory
|
|
233
|
-
- sendZoomIn
|
|
234
|
-
- sendZoomOut
|
|
235
|
-
- sendClearStartAndEndPoint
|
|
236
|
-
- sendGetDirectionToBridge
|
|
237
|
-
- sendPathFilter
|
|
238
|
-
- sendNavigateToBridge
|
|
239
|
-
- sendMapThemeToBridge
|
|
484
|
+
```tsx
|
|
485
|
+
<button onClick={() => StepNavigationBridgeService.prevStep()}>Previous</button>
|
|
486
|
+
<button onClick={() => StepNavigationBridgeService.nextStep()}>Next</button>
|
|
487
|
+
<button onClick={() => StepNavigationBridgeService.finishStep()}>Finish</button>
|
|
488
|
+
```
|
|
240
489
|
|
|
241
|
-
###
|
|
242
|
-
- registerBridgeHandler
|
|
490
|
+
### `ZoomBridgeService`
|
|
243
491
|
|
|
244
|
-
|
|
245
|
-
- useBridgeStorage (Zustand)
|
|
492
|
+
Controls viewer zoom.
|
|
246
493
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
494
|
+
```ts
|
|
495
|
+
ZoomBridgeService.zoomIn();
|
|
496
|
+
ZoomBridgeService.zoomOut();
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Example:
|
|
500
|
+
|
|
501
|
+
```tsx
|
|
502
|
+
<button onClick={() => ZoomBridgeService.zoomOut()}>Zoom Out</button>
|
|
503
|
+
<button onClick={() => ZoomBridgeService.zoomIn()}>Zoom In</button>
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### `ViewerBridgeService`
|
|
507
|
+
|
|
508
|
+
Controls viewer mode, camera view, and text rendering.
|
|
509
|
+
|
|
510
|
+
```ts
|
|
511
|
+
ViewerBridgeService.setViewMode("2D", true);
|
|
512
|
+
ViewerBridgeService.setViewMode("3D", true);
|
|
513
|
+
ViewerBridgeService.setCameraView("street");
|
|
514
|
+
ViewerBridgeService.setCameraView("sky");
|
|
515
|
+
ViewerBridgeService.setTextType("2D");
|
|
516
|
+
ViewerBridgeService.setTextType("3D");
|
|
517
|
+
ViewerBridgeService.setShow2DIcon(true);
|
|
518
|
+
ViewerBridgeService.setShow2DIcon(false);
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
Toolbar example:
|
|
522
|
+
|
|
523
|
+
```tsx
|
|
524
|
+
import { ViewerBridgeService, useViewerStore } from "wovvmap-webview-bridge/web";
|
|
525
|
+
|
|
526
|
+
export function ViewerToolbar() {
|
|
527
|
+
const viewMode = useViewerStore((s) => s.viewMode);
|
|
528
|
+
const cameraView = useViewerStore((s) => s.cameraView);
|
|
529
|
+
const textType = useViewerStore((s) => s.textType);
|
|
530
|
+
const show2DIcon = useViewerStore((s) => s.show2DIcon);
|
|
531
|
+
|
|
532
|
+
return (
|
|
533
|
+
<div>
|
|
534
|
+
<button
|
|
535
|
+
className={viewMode === "2D" ? "active" : ""}
|
|
536
|
+
onClick={() => ViewerBridgeService.setViewMode("2D", true)}
|
|
537
|
+
>
|
|
538
|
+
2D View
|
|
539
|
+
</button>
|
|
540
|
+
<button
|
|
541
|
+
className={viewMode === "3D" ? "active" : ""}
|
|
542
|
+
onClick={() => ViewerBridgeService.setViewMode("3D", true)}
|
|
543
|
+
>
|
|
544
|
+
3D View
|
|
545
|
+
</button>
|
|
546
|
+
<button
|
|
547
|
+
className={cameraView === "street" ? "active" : ""}
|
|
548
|
+
onClick={() => ViewerBridgeService.setCameraView("street")}
|
|
549
|
+
>
|
|
550
|
+
Street
|
|
551
|
+
</button>
|
|
552
|
+
<button
|
|
553
|
+
className={cameraView === "sky" ? "active" : ""}
|
|
554
|
+
onClick={() => ViewerBridgeService.setCameraView("sky")}
|
|
555
|
+
>
|
|
556
|
+
Sky
|
|
557
|
+
</button>
|
|
558
|
+
<button
|
|
559
|
+
className={textType === "2D" ? "active" : ""}
|
|
560
|
+
onClick={() => ViewerBridgeService.setTextType("2D")}
|
|
561
|
+
>
|
|
562
|
+
2D Text
|
|
563
|
+
</button>
|
|
564
|
+
<button
|
|
565
|
+
className={textType === "3D" ? "active" : ""}
|
|
566
|
+
onClick={() => ViewerBridgeService.setTextType("3D")}
|
|
567
|
+
>
|
|
568
|
+
3D Text
|
|
569
|
+
</button>
|
|
570
|
+
<button
|
|
571
|
+
className={show2DIcon ? "active" : ""}
|
|
572
|
+
onClick={() => ViewerBridgeService.setShow2DIcon(!show2DIcon)}
|
|
573
|
+
>
|
|
574
|
+
2D Icon
|
|
575
|
+
</button>
|
|
576
|
+
</div>
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### `BridgeService`
|
|
582
|
+
|
|
583
|
+
Low-level event sender. Use command services above for normal UI code.
|
|
584
|
+
|
|
585
|
+
Available low-level methods:
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
BridgeService.setViewerConfig(config);
|
|
589
|
+
BridgeService.setDestination(nodeId, cameraAnimate, clearSelection);
|
|
590
|
+
BridgeService.clearDestination();
|
|
591
|
+
BridgeService.setOrigin(nodeId, cameraAnimate, clearSelection);
|
|
592
|
+
BridgeService.clearOrigin();
|
|
593
|
+
BridgeService.swapOriginDestination();
|
|
594
|
+
BridgeService.setActiveSelection(type, code);
|
|
595
|
+
BridgeService.clearActiveSelection();
|
|
596
|
+
BridgeService.setOfferSelected(isSelected);
|
|
597
|
+
BridgeService.generateDirectionsRoute(callbacks);
|
|
598
|
+
BridgeService.clearActivePath();
|
|
599
|
+
BridgeService.changeFloor(floorIndex, animateCamera);
|
|
600
|
+
BridgeService.goToStep(index);
|
|
601
|
+
BridgeService.nextStep();
|
|
602
|
+
BridgeService.prevStep();
|
|
603
|
+
BridgeService.finishStep();
|
|
604
|
+
BridgeService.zoomIn();
|
|
605
|
+
BridgeService.zoomOut();
|
|
606
|
+
BridgeService.setViewMode(viewMode, animate);
|
|
607
|
+
BridgeService.setCameraView(cameraView);
|
|
608
|
+
BridgeService.setTextType(textType);
|
|
609
|
+
BridgeService.setShow2DIcon(show2DIcon);
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
## Synced Stores
|
|
613
|
+
|
|
614
|
+
All stores are Zustand stores. Use them directly in React components.
|
|
615
|
+
|
|
616
|
+
### `useBridgeStorage`
|
|
617
|
+
|
|
618
|
+
Bridge and tenant state.
|
|
619
|
+
|
|
620
|
+
Fields:
|
|
621
|
+
|
|
622
|
+
- `isMapLoaded`: viewer sent map loaded state
|
|
623
|
+
- `isBridgeLoaded`: bridge connection event received
|
|
624
|
+
- `imageBaseUrl`: base URL for category, subcategory, amenity, and store images
|
|
625
|
+
|
|
626
|
+
Example:
|
|
627
|
+
|
|
628
|
+
```tsx
|
|
629
|
+
const isReady = useBridgeStorage((s) => s.isBridgeLoaded && s.isMapLoaded);
|
|
630
|
+
const imageBaseUrl = useBridgeStorage((s) => s.imageBaseUrl);
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### `useViewerStore`
|
|
634
|
+
|
|
635
|
+
Viewer configuration and loading state.
|
|
636
|
+
|
|
637
|
+
Fields:
|
|
638
|
+
|
|
639
|
+
- `apiVersion`: viewer API version, for example `"v2"`
|
|
640
|
+
- `viewMode`: `"2D"` or `"3D"`
|
|
641
|
+
- `cameraView`: `"street"` or `"sky"`
|
|
642
|
+
- `textType`: `"2D"` or `"3D"`
|
|
643
|
+
- `show2DIcon`: boolean
|
|
644
|
+
- `isMapParsed`: true after viewer parsed map data
|
|
645
|
+
- `isLoading`: viewer loading flag
|
|
646
|
+
|
|
647
|
+
Example:
|
|
648
|
+
|
|
649
|
+
```tsx
|
|
650
|
+
const viewMode = useViewerStore((s) => s.viewMode);
|
|
651
|
+
const cameraView = useViewerStore((s) => s.cameraView);
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### `useCategoryStore`
|
|
655
|
+
|
|
656
|
+
Category catalog.
|
|
657
|
+
|
|
658
|
+
Fields:
|
|
659
|
+
|
|
660
|
+
- `categories`: `Record<string, Category>`
|
|
661
|
+
|
|
662
|
+
Example:
|
|
663
|
+
|
|
664
|
+
```tsx
|
|
665
|
+
const categories = useCategoryStore((s) => Object.values(s.categories));
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
Category active state:
|
|
669
|
+
|
|
670
|
+
```tsx
|
|
671
|
+
const activeSelection = useSelectionStore((s) => s.activeSelection);
|
|
672
|
+
const isActiveCategory =
|
|
673
|
+
activeSelection.type === "Category" &&
|
|
674
|
+
activeSelection.id === category.code.toString();
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### `useSubCategoryStore`
|
|
678
|
+
|
|
679
|
+
Subcategory catalog.
|
|
680
|
+
|
|
681
|
+
Fields:
|
|
682
|
+
|
|
683
|
+
- `subCategories`: `Record<string, SubCategory>`
|
|
684
|
+
|
|
685
|
+
Example:
|
|
686
|
+
|
|
687
|
+
```tsx
|
|
688
|
+
const subCategories = useSubCategoryStore((s) => Object.values(s.subCategories));
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
Subcategory active state:
|
|
692
|
+
|
|
693
|
+
```tsx
|
|
694
|
+
const isActiveSubCategory =
|
|
695
|
+
activeSelection.type === "SubCategory" &&
|
|
696
|
+
activeSelection.id === subCategory.code.toString();
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### `useAmenityStore`
|
|
700
|
+
|
|
701
|
+
Amenity catalog.
|
|
702
|
+
|
|
703
|
+
Fields:
|
|
704
|
+
|
|
705
|
+
- `amenities`: `Record<string, Amenity>`
|
|
706
|
+
|
|
707
|
+
Example:
|
|
708
|
+
|
|
709
|
+
```tsx
|
|
710
|
+
const amenities = useAmenityStore((s) => Object.values(s.amenities));
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
Amenity active state:
|
|
714
|
+
|
|
715
|
+
```tsx
|
|
716
|
+
const isActiveAmenity =
|
|
717
|
+
activeSelection.type === "Amenity" &&
|
|
718
|
+
activeSelection.id === amenity.id.toString();
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
### `useNodePointStore`
|
|
722
|
+
|
|
723
|
+
Searchable stores/locations and node maps.
|
|
724
|
+
|
|
725
|
+
Fields:
|
|
726
|
+
|
|
727
|
+
- `searchableNodePointMap`: `NodePoint[]`
|
|
728
|
+
- `locationGroupedNodeMap`: `Record<string, string[]>`
|
|
729
|
+
- `keyNodeMap`: `Record<string, NodePoint>`
|
|
730
|
+
- `offersNodeMap`: `OffersNodeMap`
|
|
731
|
+
|
|
732
|
+
Actions/helpers:
|
|
733
|
+
|
|
734
|
+
- `getNodePointByKey(key)`
|
|
735
|
+
- `clearSearchableNodePoints()`
|
|
736
|
+
|
|
737
|
+
Example: search source data:
|
|
738
|
+
|
|
739
|
+
```tsx
|
|
740
|
+
const nodePoints = useNodePointStore((s) => s.searchableNodePointMap);
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
Example: get store name:
|
|
744
|
+
|
|
745
|
+
```ts
|
|
746
|
+
const node = useNodePointStore.getState().getNodePointByKey(nodeKey);
|
|
747
|
+
const name = node?.assets?.locationName?.text || node?.key || "";
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
Example: group multi-location store:
|
|
751
|
+
|
|
752
|
+
```ts
|
|
753
|
+
const groupKeys = useNodePointStore.getState().locationGroupedNodeMap["Aldo"] || [];
|
|
754
|
+
DirectionBridgeService.setDestination(groupKeys.join("<->"), true, false);
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
Example: offers data:
|
|
758
|
+
|
|
759
|
+
```tsx
|
|
760
|
+
const allOffers = useNodePointStore((s) => s.offersNodeMap.all_offers);
|
|
761
|
+
const hasOffers = (allOffers?.nodePointsKey?.length ?? 0) > 0;
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
### `useSelectionStore`
|
|
765
|
+
|
|
766
|
+
Active category/subcategory/amenity and offers state.
|
|
767
|
+
|
|
768
|
+
Fields:
|
|
769
|
+
|
|
770
|
+
- `activeSelection`: `{ type: "Category" | "SubCategory" | "Amenity" | null; id: string | null }`
|
|
771
|
+
- `activeCategory`: `Category | null`
|
|
772
|
+
- `isOfferSelected`: boolean
|
|
773
|
+
|
|
774
|
+
Examples:
|
|
775
|
+
|
|
776
|
+
```tsx
|
|
777
|
+
const activeSelection = useSelectionStore((s) => s.activeSelection);
|
|
778
|
+
const activeCategory = useSelectionStore((s) => s.activeCategory);
|
|
779
|
+
const isOfferSelected = useSelectionStore((s) => s.isOfferSelected);
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
Offer button active:
|
|
783
|
+
|
|
784
|
+
```tsx
|
|
785
|
+
const isActive = useSelectionStore((s) => s.isOfferSelected);
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
Category tab active:
|
|
789
|
+
|
|
790
|
+
```tsx
|
|
791
|
+
const isActive =
|
|
792
|
+
activeSelection.type === "Category" &&
|
|
793
|
+
activeSelection.id === category.code.toString();
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
Subcategory tab active:
|
|
797
|
+
|
|
798
|
+
```tsx
|
|
799
|
+
const isActive =
|
|
800
|
+
activeSelection.type === "SubCategory" &&
|
|
801
|
+
activeSelection.id === subCategory.code.toString();
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
Amenity active:
|
|
805
|
+
|
|
806
|
+
```tsx
|
|
807
|
+
const isActive =
|
|
808
|
+
activeSelection.type === "Amenity" &&
|
|
809
|
+
activeSelection.id === amenity.id.toString();
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
### `useDirectionStore`
|
|
813
|
+
|
|
814
|
+
Origin/destination and pathfinding state mirrored from the viewer.
|
|
815
|
+
|
|
816
|
+
Fields:
|
|
817
|
+
|
|
818
|
+
- `selectedOriginPointId`: selected start node id or joined node ids
|
|
819
|
+
- `selectedDestinationPointId`: selected end node id or joined node ids
|
|
820
|
+
- `pathfindingError`: route error string or `null`
|
|
821
|
+
- `routingPreferences`: `{ elevator?: boolean; escalator?: boolean; stair?: boolean }`
|
|
822
|
+
|
|
823
|
+
Examples:
|
|
824
|
+
|
|
825
|
+
```tsx
|
|
826
|
+
const origin = useDirectionStore((s) => s.selectedOriginPointId);
|
|
827
|
+
const destination = useDirectionStore((s) => s.selectedDestinationPointId);
|
|
828
|
+
const pathfindingError = useDirectionStore((s) => s.pathfindingError);
|
|
829
|
+
const routingPreferences = useDirectionStore((s) => s.routingPreferences);
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
Show route error:
|
|
833
|
+
|
|
834
|
+
```tsx
|
|
835
|
+
{pathfindingError && <div>No Routes Available: {pathfindingError}</div>}
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
Enable get directions button:
|
|
839
|
+
|
|
840
|
+
```tsx
|
|
841
|
+
const canGenerateRoute = Boolean(origin && destination);
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
Update routing preference from host:
|
|
845
|
+
|
|
846
|
+
```tsx
|
|
847
|
+
useDirectionStore.getState().setRoutingPreferences({
|
|
848
|
+
elevator: true,
|
|
849
|
+
escalator: false,
|
|
850
|
+
});
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
### `useDirectionsRouteRequestStore`
|
|
854
|
+
|
|
855
|
+
Request state for `DirectionBridgeService.generateDirectionsRoute`.
|
|
856
|
+
|
|
857
|
+
Fields:
|
|
858
|
+
|
|
859
|
+
- `status`: `"idle" | "loading" | "success" | "error"`
|
|
860
|
+
- `result`: `DirectionsRouteResult | null`
|
|
861
|
+
- `error`: string or `null`
|
|
862
|
+
- `isLoading`: boolean
|
|
863
|
+
|
|
864
|
+
Example:
|
|
865
|
+
|
|
866
|
+
```tsx
|
|
867
|
+
const routeRequest = useDirectionsRouteRequestStore();
|
|
868
|
+
|
|
869
|
+
if (routeRequest.isLoading) {
|
|
870
|
+
return <div>Generating route...</div>;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (routeRequest.status === "error") {
|
|
874
|
+
return <div>{routeRequest.error}</div>;
|
|
875
|
+
}
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
Navigate to step-by-step only when route succeeds:
|
|
879
|
+
|
|
880
|
+
```tsx
|
|
881
|
+
DirectionBridgeService.generateDirectionsRoute({
|
|
882
|
+
onSuccess: () => {
|
|
883
|
+
navigateToStepByStep();
|
|
884
|
+
},
|
|
885
|
+
onError: (result) => {
|
|
886
|
+
console.log(result.error);
|
|
887
|
+
},
|
|
888
|
+
});
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
### `useFloorStore`
|
|
892
|
+
|
|
893
|
+
Floor list and current floor.
|
|
894
|
+
|
|
895
|
+
Fields:
|
|
896
|
+
|
|
897
|
+
- `floors`: `FloorImage[]`
|
|
898
|
+
- `activeFloor`: number or `null`
|
|
899
|
+
|
|
900
|
+
Example:
|
|
901
|
+
|
|
902
|
+
```tsx
|
|
903
|
+
const floors = useFloorStore((s) => s.floors);
|
|
904
|
+
const activeFloor = useFloorStore((s) => s.activeFloor);
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
### `useNavigationStore`
|
|
908
|
+
|
|
909
|
+
Step-by-step navigation state.
|
|
910
|
+
|
|
911
|
+
Fields:
|
|
912
|
+
|
|
913
|
+
- `activePath`: `NodePoint[] | null`
|
|
914
|
+
- `navigationResult`: `NavigationResult | null`
|
|
915
|
+
- `simulationMode`: `"none" | "full" | "current"`
|
|
916
|
+
- `currentStepIndex`: number
|
|
917
|
+
|
|
918
|
+
Example:
|
|
919
|
+
|
|
920
|
+
```tsx
|
|
921
|
+
const navigationResult = useNavigationStore((s) => s.navigationResult);
|
|
922
|
+
const currentStepIndex = useNavigationStore((s) => s.currentStepIndex);
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
Step data:
|
|
926
|
+
|
|
927
|
+
```tsx
|
|
928
|
+
navigationResult?.steps.map((step) => (
|
|
929
|
+
<div key={step.id}>
|
|
930
|
+
{step.instruction} - {step.distanceText}
|
|
931
|
+
</div>
|
|
932
|
+
));
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
## Viewer Click Events
|
|
936
|
+
|
|
937
|
+
Use `registerBridgeHandler` for viewer interaction events that should trigger host UI behavior.
|
|
938
|
+
|
|
939
|
+
Currently supported handler:
|
|
940
|
+
|
|
941
|
+
- `isShapeClick`: fires when a map shape/store is clicked in the viewer. Value is the clicked node id.
|
|
942
|
+
|
|
943
|
+
Example:
|
|
944
|
+
|
|
945
|
+
```tsx
|
|
946
|
+
import { registerBridgeHandler } from "wovvmap-webview-bridge/web";
|
|
947
|
+
|
|
948
|
+
useEffect(() => {
|
|
949
|
+
registerBridgeHandler("isShapeClick", (message) => {
|
|
950
|
+
console.log("Clicked node id", message.value);
|
|
951
|
+
openStoreDetail(message.value);
|
|
952
|
+
});
|
|
953
|
+
}, []);
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
## Full Kiosk Host Example
|
|
957
|
+
|
|
958
|
+
This example shows the main pieces together.
|
|
959
|
+
|
|
960
|
+
```tsx
|
|
961
|
+
import { useEffect, useRef } from "react";
|
|
962
|
+
import {
|
|
963
|
+
WebIframeScreen,
|
|
964
|
+
DirectionBridgeService,
|
|
965
|
+
SelectionBridgeService,
|
|
966
|
+
ViewerBridgeService,
|
|
967
|
+
ZoomBridgeService,
|
|
968
|
+
useCategoryStore,
|
|
969
|
+
useDirectionStore,
|
|
970
|
+
useNodePointStore,
|
|
971
|
+
registerBridgeHandler,
|
|
972
|
+
} from "wovvmap-webview-bridge/web";
|
|
973
|
+
import { useDirectionInputStore } from "./useDirectionInputStore";
|
|
974
|
+
|
|
975
|
+
export function KioskMap({ mapId }: { mapId: string | null }) {
|
|
976
|
+
const categories = useCategoryStore((s) => Object.values(s.categories));
|
|
977
|
+
const origin = useDirectionStore((s) => s.selectedOriginPointId);
|
|
978
|
+
const destination = useDirectionStore((s) => s.selectedDestinationPointId);
|
|
979
|
+
const configuredRef = useRef(false);
|
|
980
|
+
|
|
981
|
+
useEffect(() => {
|
|
982
|
+
if (configuredRef.current) return;
|
|
983
|
+
configuredRef.current = true;
|
|
984
|
+
|
|
985
|
+
DirectionBridgeService.configure({
|
|
986
|
+
directionInputAdapter: {
|
|
987
|
+
setDestinationState: (query, selectedId) =>
|
|
988
|
+
useDirectionInputStore.getState().setDestinationState(query, selectedId),
|
|
989
|
+
setOriginState: (query, selectedId) =>
|
|
990
|
+
useDirectionInputStore.getState().setOriginState(query, selectedId),
|
|
991
|
+
clearDestinationState: (clearQuery) =>
|
|
992
|
+
useDirectionInputStore.getState().clearDestinationState(clearQuery),
|
|
993
|
+
clearOriginState: () =>
|
|
994
|
+
useDirectionInputStore.getState().clearOriginState(),
|
|
995
|
+
setActiveDirectionField: (field) =>
|
|
996
|
+
useDirectionInputStore.getState().setActiveDirectionField(field),
|
|
997
|
+
getOriginQuery: () => useDirectionInputStore.getState().originQuery,
|
|
998
|
+
getDestinationQuery: () => useDirectionInputStore.getState().destinationQuery,
|
|
999
|
+
},
|
|
1000
|
+
resolveLocationName: (nodeId) => {
|
|
1001
|
+
const nodeKey = nodeId.split("<->")[0];
|
|
1002
|
+
const node = useNodePointStore.getState().getNodePointByKey(nodeKey);
|
|
1003
|
+
return node?.assets?.locationName?.text || nodeKey;
|
|
1004
|
+
},
|
|
1005
|
+
onClearSelection: () => SelectionBridgeService.clearSelection(),
|
|
1006
|
+
});
|
|
1007
|
+
}, []);
|
|
1008
|
+
|
|
1009
|
+
useEffect(() => {
|
|
1010
|
+
registerBridgeHandler("isShapeClick", (message) => {
|
|
1011
|
+
DirectionBridgeService.setDestination(message.value, true, false);
|
|
1012
|
+
});
|
|
1013
|
+
}, []);
|
|
1014
|
+
|
|
1015
|
+
if (!mapId) return null;
|
|
1016
|
+
|
|
1017
|
+
return (
|
|
1018
|
+
<div>
|
|
1019
|
+
<WebIframeScreen
|
|
1020
|
+
url={`https://viewer.example.com/viewer/${mapId}?v=v2&template=embedded`}
|
|
1021
|
+
origin="https://viewer.example.com"
|
|
1022
|
+
/>
|
|
1023
|
+
|
|
1024
|
+
<div>
|
|
1025
|
+
{categories.map((category) => (
|
|
1026
|
+
<button
|
|
1027
|
+
key={category.code}
|
|
1028
|
+
onClick={() => SelectionBridgeService.selectCategory(category)}
|
|
1029
|
+
>
|
|
1030
|
+
{category.name}
|
|
1031
|
+
</button>
|
|
1032
|
+
))}
|
|
1033
|
+
</div>
|
|
1034
|
+
|
|
1035
|
+
<button onClick={() => ViewerBridgeService.setViewMode("2D", true)}>
|
|
1036
|
+
2D View
|
|
1037
|
+
</button>
|
|
1038
|
+
<button onClick={() => ViewerBridgeService.setViewMode("3D", true)}>
|
|
1039
|
+
3D View
|
|
1040
|
+
</button>
|
|
1041
|
+
<button onClick={() => ZoomBridgeService.zoomOut()}>Zoom Out</button>
|
|
1042
|
+
<button onClick={() => ZoomBridgeService.zoomIn()}>Zoom In</button>
|
|
1043
|
+
|
|
1044
|
+
<button
|
|
1045
|
+
disabled={!origin || !destination}
|
|
1046
|
+
onClick={() =>
|
|
1047
|
+
DirectionBridgeService.generateDirectionsRoute({
|
|
1048
|
+
onSuccess: () => console.log("route ready"),
|
|
1049
|
+
})
|
|
1050
|
+
}
|
|
1051
|
+
>
|
|
1052
|
+
Get Directions
|
|
1053
|
+
</button>
|
|
1054
|
+
</div>
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
## Data Flow
|
|
1060
|
+
|
|
1061
|
+
1. Host renders `WebIframeScreen` or `WebViewScreen`.
|
|
1062
|
+
2. Viewer initializes and sends grouped state events.
|
|
1063
|
+
3. Package handlers apply those events into Zustand stores.
|
|
1064
|
+
4. Host UI reads stores and renders categories, amenities, floors, offers, directions, and navigation.
|
|
1065
|
+
5. Host UI calls `*BridgeService` methods.
|
|
1066
|
+
6. Package sends typed bridge event to viewer.
|
|
1067
|
+
7. Viewer updates its internal state/scene and sends fresh grouped state back.
|
|
1068
|
+
|
|
1069
|
+
## Incoming Viewer Events
|
|
1070
|
+
|
|
1071
|
+
These are sent by the viewer to the host package and applied into stores automatically.
|
|
1072
|
+
|
|
1073
|
+
| Key | Store updated | Value |
|
|
1074
|
+
| --- | --- | --- |
|
|
1075
|
+
| `bridgeState` | `useBridgeStorage.isMapLoaded` | `{ isMapLoaded: boolean }` |
|
|
1076
|
+
| `isConnection` | `useBridgeStorage.isBridgeLoaded` | `boolean` |
|
|
1077
|
+
| `tenantState` | `useBridgeStorage.imageBaseUrl` | `{ imageBaseUrl }` |
|
|
1078
|
+
| `viewerState` | `useViewerStore` | `ViewerBridgeState` |
|
|
1079
|
+
| `catalogState` | `useCategoryStore`, `useSubCategoryStore`, `useAmenityStore` | categories, subCategories, amenities |
|
|
1080
|
+
| `floorState` | `useFloorStore` | activeFloor, floors |
|
|
1081
|
+
| `nodePointState` | `useNodePointStore` | searchableNodePointMap, locationGroupedNodeMap, keyNodeMap, offersNodeMap |
|
|
1082
|
+
| `selectionState` | `useSelectionStore` | activeSelection, activeCategory, isOfferSelected |
|
|
1083
|
+
| `directionState` | `useDirectionStore` | selectedOriginPointId, selectedDestinationPointId, pathfindingError, routingPreferences |
|
|
1084
|
+
| `navigationState` | `useNavigationStore` | activePath, navigationResult, simulationMode, currentStepIndex |
|
|
1085
|
+
| `directionsRouteResult` | `useDirectionsRouteRequestStore` | status, navigationResult, error |
|
|
1086
|
+
| `isShapeClick` | event handler | clicked node id |
|
|
1087
|
+
|
|
1088
|
+
## Outgoing Host Commands
|
|
1089
|
+
|
|
1090
|
+
These are sent by command services to the viewer.
|
|
1091
|
+
|
|
1092
|
+
| Service | Message key |
|
|
1093
|
+
| --- | --- |
|
|
1094
|
+
| `DirectionBridgeService.setDestination` | `setDestination` |
|
|
1095
|
+
| `DirectionBridgeService.clearDestination` | `clearDestination` |
|
|
1096
|
+
| `DirectionBridgeService.setOrigin` | `setOrigin` |
|
|
1097
|
+
| `DirectionBridgeService.clearOrigin` | `clearOrigin` |
|
|
1098
|
+
| `DirectionBridgeService.swapOriginDestination` | `swapOriginDestination` |
|
|
1099
|
+
| `DirectionBridgeService.generateDirectionsRoute` | `generateDirectionsRoute` |
|
|
1100
|
+
| `DirectionBridgeService.clearActivePath` | `clearActivePath` |
|
|
1101
|
+
| `SelectionBridgeService.selectCategory` | `setActiveSelection` |
|
|
1102
|
+
| `SelectionBridgeService.selectSubCategory` | `setActiveSelection` |
|
|
1103
|
+
| `SelectionBridgeService.selectAmenity` | `setActiveSelection` plus destination |
|
|
1104
|
+
| `SelectionBridgeService.toggleOffers` | `setOfferSelected` |
|
|
1105
|
+
| `SelectionBridgeService.clearSelection` | `clearActiveSelection` and `setOfferSelected(false)` |
|
|
1106
|
+
| `FloorBridgeService.changeFloor` | `changeFloor` |
|
|
1107
|
+
| `StepNavigationBridgeService.goToStep` | `goToStep` |
|
|
1108
|
+
| `StepNavigationBridgeService.nextStep` | `nextStep` |
|
|
1109
|
+
| `StepNavigationBridgeService.prevStep` | `prevStep` |
|
|
1110
|
+
| `StepNavigationBridgeService.finishStep` | `finishStep` |
|
|
1111
|
+
| `ZoomBridgeService.zoomIn` | `zoomIn` |
|
|
1112
|
+
| `ZoomBridgeService.zoomOut` | `zoomOut` |
|
|
1113
|
+
| `ViewerBridgeService.setViewMode` | `setViewMode` |
|
|
1114
|
+
| `ViewerBridgeService.setCameraView` | `setCameraView` |
|
|
1115
|
+
| `ViewerBridgeService.setTextType` | `setTextType` |
|
|
1116
|
+
| `ViewerBridgeService.setShow2DIcon` | `setShow2DIcon` |
|
|
1117
|
+
|
|
1118
|
+
## Type Exports
|
|
1119
|
+
|
|
1120
|
+
The package exports these core types:
|
|
1121
|
+
|
|
1122
|
+
```ts
|
|
1123
|
+
type IncomingMessage;
|
|
1124
|
+
type OutgoingMessage;
|
|
1125
|
+
type NodePoint;
|
|
1126
|
+
type FloorImage;
|
|
1127
|
+
type Amenity;
|
|
1128
|
+
type Category;
|
|
1129
|
+
type SubCategory;
|
|
1130
|
+
type ViewerBridgeState;
|
|
1131
|
+
type ViewMode;
|
|
1132
|
+
type CameraView;
|
|
1133
|
+
type TextType;
|
|
1134
|
+
type NavigationBridgeState;
|
|
1135
|
+
type DirectionsRouteResult;
|
|
1136
|
+
type NavigationResult;
|
|
1137
|
+
type NavigationStep;
|
|
1138
|
+
type NavigationSummary;
|
|
1139
|
+
type SimulationMode;
|
|
1140
|
+
type DirectionBridgeServiceConfig;
|
|
1141
|
+
type DirectionInputAdapter;
|
|
1142
|
+
type ViewerConfig;
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
## Important Type Shapes
|
|
1146
|
+
|
|
1147
|
+
### `Category`
|
|
1148
|
+
|
|
1149
|
+
```ts
|
|
1150
|
+
type Category = {
|
|
1151
|
+
code: string | number;
|
|
1152
|
+
name: string;
|
|
1153
|
+
image?: string;
|
|
1154
|
+
subCategoryCodes: string[] | number[];
|
|
1155
|
+
nodePointsKey?: string[];
|
|
1156
|
+
nodePoints?: NodePoint[];
|
|
1157
|
+
};
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
### `SubCategory`
|
|
1161
|
+
|
|
1162
|
+
```ts
|
|
1163
|
+
type SubCategory = {
|
|
1164
|
+
code: string | number;
|
|
1165
|
+
name: string;
|
|
1166
|
+
image?: string;
|
|
1167
|
+
description?: string;
|
|
1168
|
+
categoryCode: string | number;
|
|
1169
|
+
nodePointsKey?: string[];
|
|
1170
|
+
nodePoints?: NodePoint[];
|
|
1171
|
+
};
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
### `Amenity`
|
|
1175
|
+
|
|
1176
|
+
```ts
|
|
1177
|
+
type Amenity = {
|
|
1178
|
+
id: string | number;
|
|
1179
|
+
status: number;
|
|
1180
|
+
fieldCode: string | number;
|
|
1181
|
+
name: string;
|
|
1182
|
+
image: string;
|
|
1183
|
+
wayfinding_id?: string | number | null;
|
|
1184
|
+
nodePointsKey?: string[];
|
|
1185
|
+
nodePoints?: NodePoint[];
|
|
1186
|
+
};
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
### `NodePoint`
|
|
1190
|
+
|
|
1191
|
+
Important fields:
|
|
1192
|
+
|
|
1193
|
+
```ts
|
|
1194
|
+
type NodePoint = {
|
|
1195
|
+
key: string;
|
|
1196
|
+
floorIndex: number;
|
|
1197
|
+
floorName: string;
|
|
1198
|
+
assets: {
|
|
1199
|
+
locationName: { text: string } | null;
|
|
1200
|
+
pointImages: unknown | null;
|
|
1201
|
+
logo: string | null;
|
|
1202
|
+
brandLogo: string | null;
|
|
1203
|
+
brandImages: string[];
|
|
1204
|
+
};
|
|
1205
|
+
categoryCode?: string | number;
|
|
1206
|
+
subCategoryCode?: string | number;
|
|
1207
|
+
offers: NodePointOffer[];
|
|
1208
|
+
up: boolean;
|
|
1209
|
+
down: boolean;
|
|
1210
|
+
};
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
### `DirectionsRouteResult`
|
|
1214
|
+
|
|
1215
|
+
```ts
|
|
1216
|
+
type DirectionsRouteResult = {
|
|
1217
|
+
status: "success" | "error";
|
|
1218
|
+
navigationResult: NavigationResult | null;
|
|
1219
|
+
error: string | null;
|
|
1220
|
+
};
|
|
1221
|
+
```
|
|
1222
|
+
|
|
1223
|
+
### `NavigationStep`
|
|
1224
|
+
|
|
1225
|
+
```ts
|
|
1226
|
+
type NavigationStep = {
|
|
1227
|
+
id: string;
|
|
1228
|
+
instruction: string;
|
|
1229
|
+
action:
|
|
1230
|
+
| "straight"
|
|
1231
|
+
| "turn_left"
|
|
1232
|
+
| "turn_right"
|
|
1233
|
+
| "take_elevator"
|
|
1234
|
+
| "take_escalator"
|
|
1235
|
+
| "take_elevator_up"
|
|
1236
|
+
| "take_elevator_down"
|
|
1237
|
+
| "take_escalator_up"
|
|
1238
|
+
| "take_escalator_down"
|
|
1239
|
+
| "exit_elevator"
|
|
1240
|
+
| "exit_escalator"
|
|
1241
|
+
| "start"
|
|
1242
|
+
| "arrive";
|
|
1243
|
+
distanceText: string;
|
|
1244
|
+
timeText: string;
|
|
1245
|
+
floorIndex: number;
|
|
1246
|
+
};
|
|
1247
|
+
```
|
|
1248
|
+
|
|
1249
|
+
## Common UI Recipes
|
|
1250
|
+
|
|
1251
|
+
### Category Footer
|
|
1252
|
+
|
|
1253
|
+
```tsx
|
|
1254
|
+
const categories = useCategoryStore((s) => Object.values(s.categories));
|
|
1255
|
+
const activeSelection = useSelectionStore((s) => s.activeSelection);
|
|
1256
|
+
|
|
1257
|
+
return categories.map((category) => {
|
|
1258
|
+
const active =
|
|
1259
|
+
activeSelection.type === "Category" &&
|
|
1260
|
+
activeSelection.id === category.code.toString();
|
|
1261
|
+
|
|
1262
|
+
return (
|
|
1263
|
+
<button
|
|
1264
|
+
key={category.code}
|
|
1265
|
+
className={active ? "active" : ""}
|
|
1266
|
+
onClick={() => SelectionBridgeService.selectCategory(category)}
|
|
1267
|
+
>
|
|
1268
|
+
{category.name}
|
|
1269
|
+
</button>
|
|
1270
|
+
);
|
|
1271
|
+
});
|
|
1272
|
+
```
|
|
1273
|
+
|
|
1274
|
+
### Subcategory Footer
|
|
1275
|
+
|
|
1276
|
+
```tsx
|
|
1277
|
+
const subCategories = useSubCategoryStore((s) => Object.values(s.subCategories));
|
|
1278
|
+
const activeCategory = useSelectionStore((s) => s.activeCategory);
|
|
1279
|
+
const activeSelection = useSelectionStore((s) => s.activeSelection);
|
|
1280
|
+
|
|
1281
|
+
const visibleSubCategories = subCategories.filter((subCategory) =>
|
|
1282
|
+
activeCategory?.subCategoryCodes.includes(subCategory.code)
|
|
1283
|
+
);
|
|
1284
|
+
|
|
1285
|
+
return visibleSubCategories.map((subCategory) => {
|
|
1286
|
+
const active =
|
|
1287
|
+
activeSelection.type === "SubCategory" &&
|
|
1288
|
+
activeSelection.id === subCategory.code.toString();
|
|
1289
|
+
|
|
1290
|
+
return (
|
|
1291
|
+
<button
|
|
1292
|
+
key={subCategory.code}
|
|
1293
|
+
className={active ? "active" : ""}
|
|
1294
|
+
onClick={() => SelectionBridgeService.selectSubCategory(subCategory)}
|
|
1295
|
+
>
|
|
1296
|
+
{subCategory.name}
|
|
1297
|
+
</button>
|
|
1298
|
+
);
|
|
1299
|
+
});
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
### Amenity List
|
|
1303
|
+
|
|
1304
|
+
```tsx
|
|
1305
|
+
const amenities = useAmenityStore((s) => Object.values(s.amenities));
|
|
1306
|
+
const activeSelection = useSelectionStore((s) => s.activeSelection);
|
|
1307
|
+
|
|
1308
|
+
return amenities.map((amenity) => {
|
|
1309
|
+
const active =
|
|
1310
|
+
activeSelection.type === "Amenity" &&
|
|
1311
|
+
activeSelection.id === amenity.id.toString();
|
|
1312
|
+
|
|
1313
|
+
return (
|
|
1314
|
+
<button
|
|
1315
|
+
key={amenity.id}
|
|
1316
|
+
className={active ? "active" : ""}
|
|
1317
|
+
onClick={() => SelectionBridgeService.selectAmenity(amenity)}
|
|
1318
|
+
>
|
|
1319
|
+
{amenity.name}
|
|
1320
|
+
</button>
|
|
1321
|
+
);
|
|
1322
|
+
});
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
### Store Search Selection
|
|
1326
|
+
|
|
1327
|
+
```tsx
|
|
1328
|
+
function selectStore(nodeKey: string, field: "from" | "to") {
|
|
1329
|
+
const group = useNodePointStore.getState().locationGroupedNodeMap[nodeKey] || [nodeKey];
|
|
1330
|
+
const joinedNodeIds = group.join("<->");
|
|
1331
|
+
|
|
1332
|
+
if (field === "from") {
|
|
1333
|
+
DirectionBridgeService.setOrigin(joinedNodeIds, true, false);
|
|
1334
|
+
} else {
|
|
1335
|
+
DirectionBridgeService.setDestination(joinedNodeIds, true, false);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
```
|
|
1339
|
+
|
|
1340
|
+
### Default Location
|
|
1341
|
+
|
|
1342
|
+
Default location should stay in the host app because only the host knows the map id and user preference.
|
|
1343
|
+
|
|
1344
|
+
```tsx
|
|
1345
|
+
const defaultLocationKey = getDefaultLocationForMap(mapId);
|
|
1346
|
+
|
|
1347
|
+
if (defaultLocationKey) {
|
|
1348
|
+
DirectionBridgeService.setOrigin(defaultLocationKey, false, false);
|
|
1349
|
+
}
|
|
1350
|
+
```
|
|
1351
|
+
|
|
1352
|
+
To show default location badge:
|
|
1353
|
+
|
|
1354
|
+
```tsx
|
|
1355
|
+
const isDefaultLocation = selectedOriginPointId === defaultLocationKey;
|
|
1356
|
+
```
|
|
1357
|
+
|
|
1358
|
+
### Get Directions Button
|
|
1359
|
+
|
|
1360
|
+
```tsx
|
|
1361
|
+
const origin = useDirectionStore((s) => s.selectedOriginPointId);
|
|
1362
|
+
const destination = useDirectionStore((s) => s.selectedDestinationPointId);
|
|
1363
|
+
const isLoading = useDirectionsRouteRequestStore((s) => s.isLoading);
|
|
1364
|
+
|
|
1365
|
+
return (
|
|
1366
|
+
<button
|
|
1367
|
+
disabled={!origin || !destination || isLoading}
|
|
1368
|
+
onClick={() =>
|
|
1369
|
+
DirectionBridgeService.generateDirectionsRoute({
|
|
1370
|
+
onSuccess: () => navigateToStepByStep(),
|
|
1371
|
+
})
|
|
1372
|
+
}
|
|
1373
|
+
>
|
|
1374
|
+
{isLoading ? "Loading..." : "Get Directions"}
|
|
1375
|
+
</button>
|
|
1376
|
+
);
|
|
1377
|
+
```
|
|
1378
|
+
|
|
1379
|
+
### Pathfinding Error
|
|
1380
|
+
|
|
1381
|
+
```tsx
|
|
1382
|
+
const pathfindingError = useDirectionStore((s) => s.pathfindingError);
|
|
1383
|
+
|
|
1384
|
+
return pathfindingError ? (
|
|
1385
|
+
<div>
|
|
1386
|
+
<strong>No Routes Available</strong>
|
|
1387
|
+
<span>{pathfindingError}</span>
|
|
1388
|
+
</div>
|
|
1389
|
+
) : null;
|
|
1390
|
+
```
|
|
1391
|
+
|
|
1392
|
+
### Step-by-Step Panel
|
|
1393
|
+
|
|
1394
|
+
```tsx
|
|
1395
|
+
const navigationResult = useNavigationStore((s) => s.navigationResult);
|
|
1396
|
+
|
|
1397
|
+
if (!navigationResult) return null;
|
|
1398
|
+
|
|
1399
|
+
return (
|
|
1400
|
+
<div>
|
|
1401
|
+
<h3>Total Distance {navigationResult.summary.totalDistance}</h3>
|
|
1402
|
+
<p>Arrive in {navigationResult.summary.totalApproxTime}</p>
|
|
1403
|
+
{navigationResult.steps.map((step, index) => (
|
|
1404
|
+
<button key={step.id} onClick={() => StepNavigationBridgeService.goToStep(index)}>
|
|
1405
|
+
{step.instruction}
|
|
1406
|
+
</button>
|
|
1407
|
+
))}
|
|
1408
|
+
</div>
|
|
1409
|
+
);
|
|
1410
|
+
```
|
|
1411
|
+
|
|
1412
|
+
## Image URLs
|
|
1413
|
+
|
|
1414
|
+
Image base URL comes from tenant state:
|
|
1415
|
+
|
|
1416
|
+
```tsx
|
|
1417
|
+
const imageBaseUrl = useBridgeStorage((s) => s.imageBaseUrl);
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
Category image:
|
|
1421
|
+
|
|
1422
|
+
```tsx
|
|
1423
|
+
const src = category.image && imageBaseUrl ? `${imageBaseUrl}${category.image}` : "";
|
|
1424
|
+
```
|
|
1425
|
+
|
|
1426
|
+
Amenity image:
|
|
1427
|
+
|
|
1428
|
+
```tsx
|
|
1429
|
+
const src = amenity.image && imageBaseUrl ? `${imageBaseUrl}${amenity.image}` : "";
|
|
1430
|
+
```
|
|
1431
|
+
|
|
1432
|
+
## What Should Stay In Host App
|
|
1433
|
+
|
|
1434
|
+
Keep these in the host app:
|
|
1435
|
+
|
|
1436
|
+
- `mapId`
|
|
1437
|
+
- default location per map id
|
|
1438
|
+
- local direction input query store if your UI has text inputs
|
|
1439
|
+
- host-specific flow history
|
|
1440
|
+
- custom UI layout
|
|
1441
|
+
- business-specific filtering that is not part of bridge protocol
|
|
1442
|
+
|
|
1443
|
+
Use the package for:
|
|
1444
|
+
|
|
1445
|
+
- iframe/WebView bridge setup
|
|
1446
|
+
- reading viewer-synced stores
|
|
1447
|
+
- sending viewer commands
|
|
1448
|
+
- route result lifecycle
|
|
1449
|
+
- selection/offers/floor/zoom/viewer controls
|
|
1450
|
+
|
|
1451
|
+
## Troubleshooting
|
|
1452
|
+
|
|
1453
|
+
### Stores are empty after refresh
|
|
1454
|
+
|
|
1455
|
+
Check:
|
|
1456
|
+
|
|
1457
|
+
- iframe/WebView URL is correct
|
|
1458
|
+
- viewer is using embedded template and initializes bridge after map parse
|
|
1459
|
+
- `useBridgeStorage((s) => s.isBridgeLoaded)` becomes `true`
|
|
1460
|
+
- `useBridgeStorage((s) => s.isMapLoaded)` becomes `true`
|
|
1461
|
+
- host is not rendering UI before `mapId` exists
|
|
1462
|
+
|
|
1463
|
+
### Category or amenity images do not show
|
|
1464
|
+
|
|
1465
|
+
Use `imageBaseUrl` from `useBridgeStorage`.
|
|
1466
|
+
|
|
1467
|
+
Correct:
|
|
1468
|
+
|
|
1469
|
+
```tsx
|
|
1470
|
+
const src = imageBaseUrl && item.image ? `${imageBaseUrl}${item.image}` : "";
|
|
1471
|
+
```
|
|
1472
|
+
|
|
1473
|
+
Wrong:
|
|
1474
|
+
|
|
1475
|
+
```tsx
|
|
1476
|
+
const src = imageBaseUrl || "" + item.image;
|
|
1477
|
+
```
|
|
1478
|
+
|
|
1479
|
+
The wrong version returns only `imageBaseUrl` when it exists.
|
|
1480
|
+
|
|
1481
|
+
### Direction input text is not updating
|
|
1482
|
+
|
|
1483
|
+
Call `DirectionBridgeService.configure` and provide `directionInputAdapter`.
|
|
1484
|
+
|
|
1485
|
+
Without adapter:
|
|
1486
|
+
|
|
1487
|
+
- bridge command still works
|
|
1488
|
+
- viewer origin/destination still changes
|
|
1489
|
+
- local input text does not change automatically
|
|
1490
|
+
|
|
1491
|
+
### Route success needs UI navigation
|
|
1492
|
+
|
|
1493
|
+
Use `generateDirectionsRoute` callbacks or `useDirectionsRouteRequestStore`.
|
|
1494
|
+
|
|
1495
|
+
```ts
|
|
1496
|
+
DirectionBridgeService.generateDirectionsRoute({
|
|
1497
|
+
onSuccess: () => navigateToStepByStep(),
|
|
1498
|
+
onError: (result) => showError(result.error),
|
|
1499
|
+
});
|
|
1500
|
+
```
|
|
1501
|
+
|
|
1502
|
+
### Offers button active state
|
|
1503
|
+
|
|
1504
|
+
Use:
|
|
1505
|
+
|
|
1506
|
+
```tsx
|
|
1507
|
+
const isOfferSelected = useSelectionStore((s) => s.isOfferSelected);
|
|
1508
|
+
```
|
|
1509
|
+
|
|
1510
|
+
Do not infer offer active state from category/subcategory.
|
|
1511
|
+
|
|
1512
|
+
### Category active state
|
|
1513
|
+
|
|
1514
|
+
Use:
|
|
1515
|
+
|
|
1516
|
+
```tsx
|
|
1517
|
+
const activeSelection = useSelectionStore((s) => s.activeSelection);
|
|
1518
|
+
const isActive =
|
|
1519
|
+
activeSelection.type === "Category" &&
|
|
1520
|
+
activeSelection.id === category.code.toString();
|
|
1521
|
+
```
|
|
1522
|
+
|
|
1523
|
+
### Subcategory unselect should keep category visible
|
|
1524
|
+
|
|
1525
|
+
Keep `activeCategory` from `useSelectionStore`.
|
|
1526
|
+
|
|
1527
|
+
```tsx
|
|
1528
|
+
const activeCategory = useSelectionStore((s) => s.activeCategory);
|
|
1529
|
+
```
|
|
1530
|
+
|
|
1531
|
+
Use `activeCategory` to render subcategory list, not only `activeSelection`.
|
|
1532
|
+
|
|
1533
|
+
### Text/view controls click but do not change
|
|
1534
|
+
|
|
1535
|
+
Make sure your control overlay is above iframe/canvas layers.
|
|
1536
|
+
|
|
1537
|
+
```tsx
|
|
1538
|
+
<div className="absolute z-[1000] pointer-events-auto">
|
|
1539
|
+
...
|
|
1540
|
+
</div>
|
|
1541
|
+
```
|
|
1542
|
+
|
|
1543
|
+
### Do not duplicate bridge services in host app
|
|
1544
|
+
|
|
1545
|
+
Do not recreate old local services like:
|
|
1546
|
+
|
|
1547
|
+
- `TemplateDirectionService`
|
|
1548
|
+
- `TemplateSelectionService`
|
|
1549
|
+
- `TemplateFloorService`
|
|
1550
|
+
- `TemplateStepNavigationService`
|
|
1551
|
+
- `TemplateZoomService`
|
|
1552
|
+
- `TemplateViewerService`
|
|
1553
|
+
|
|
1554
|
+
Use package exports instead:
|
|
1555
|
+
|
|
1556
|
+
```ts
|
|
1557
|
+
import {
|
|
1558
|
+
DirectionBridgeService,
|
|
1559
|
+
SelectionBridgeService,
|
|
1560
|
+
FloorBridgeService,
|
|
1561
|
+
StepNavigationBridgeService,
|
|
1562
|
+
ZoomBridgeService,
|
|
1563
|
+
ViewerBridgeService,
|
|
1564
|
+
} from "wovvmap-webview-bridge/web";
|
|
1565
|
+
```
|
|
1566
|
+
|
|
1567
|
+
## Export Checklist
|
|
1568
|
+
|
|
1569
|
+
### Components
|
|
1570
|
+
|
|
1571
|
+
- `WebIframeScreen`
|
|
1572
|
+
- `WebViewScreen`
|
|
1573
|
+
- `attachIframeBridge`
|
|
1574
|
+
|
|
1575
|
+
### Services
|
|
1576
|
+
|
|
1577
|
+
- `BridgeService`
|
|
1578
|
+
- `DirectionBridgeService`
|
|
1579
|
+
- `SelectionBridgeService`
|
|
1580
|
+
- `FloorBridgeService`
|
|
1581
|
+
- `StepNavigationBridgeService`
|
|
1582
|
+
- `ZoomBridgeService`
|
|
1583
|
+
- `ViewerBridgeService`
|
|
1584
|
+
|
|
1585
|
+
### Stores
|
|
1586
|
+
|
|
1587
|
+
- `useBridgeStorage`
|
|
1588
|
+
- `useViewerStore`
|
|
1589
|
+
- `useCategoryStore`
|
|
1590
|
+
- `useSubCategoryStore`
|
|
1591
|
+
- `useAmenityStore`
|
|
1592
|
+
- `useNodePointStore`
|
|
1593
|
+
- `useSelectionStore`
|
|
1594
|
+
- `useDirectionStore`
|
|
1595
|
+
- `useFloorStore`
|
|
1596
|
+
- `useNavigationStore`
|
|
1597
|
+
- `useDirectionsRouteRequestStore`
|
|
1598
|
+
|
|
1599
|
+
### Handlers
|
|
1600
|
+
|
|
1601
|
+
- `registerBridgeHandler`
|
|
1602
|
+
|
|
1603
|
+
### Types
|
|
1604
|
+
|
|
1605
|
+
- `IncomingMessage`
|
|
1606
|
+
- `OutgoingMessage`
|
|
1607
|
+
- `NodePoint`
|
|
1608
|
+
- `FloorImage`
|
|
1609
|
+
- `Amenity`
|
|
1610
|
+
- `Category`
|
|
1611
|
+
- `SubCategory`
|
|
1612
|
+
- `ViewerBridgeState`
|
|
1613
|
+
- `ViewMode`
|
|
1614
|
+
- `CameraView`
|
|
1615
|
+
- `TextType`
|
|
1616
|
+
- `NavigationBridgeState`
|
|
1617
|
+
- `DirectionsRouteResult`
|
|
1618
|
+
- `NavigationResult`
|
|
1619
|
+
- `NavigationStep`
|
|
1620
|
+
- `NavigationSummary`
|
|
1621
|
+
- `SimulationMode`
|
|
1622
|
+
- `DirectionBridgeServiceConfig`
|
|
1623
|
+
- `DirectionInputAdapter`
|
|
1624
|
+
- `ViewerConfig`
|