@aapicoits/powermap-web-sdk 1.0.0 → 1.1.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 CHANGED
@@ -945,16 +945,42 @@ map.on('geometrySelectionChanged', ({ features }) => {
945
945
 
946
946
  ## 16. 🏠 Indoor Mapping
947
947
 
948
- The PowerMap SDK provides a robust API for managing indoor environments, including floor switching, category filtering, and specialized geometry handling.
948
+ The PowerMap SDK provides a robust API for managing indoor environments with **OGC IMDF 1.0.0 Full Compliance** — supporting all 16 standard feature types, ZIP archive import/export, and schema validation.
949
949
 
950
- ### 16.1 Enabling Indoor Mode
950
+ > [!TIP]
951
+ > For the complete IMDF reference including all feature types, properties, archive format, and validation — see [GEOMETRY.md](./GEOMETRY.md#ogc-imdf-100-compliance)
952
+
953
+ ### 16.1 Indoor Venue Selection (NEW)
951
954
 
952
- Switch to indoor mode to enable building-specific logic and specialized drawing tools (e.g., Beacons, Walkways).
955
+ The new `loadIndoorMap()` API lets you select and activate a venue from pre-loaded data. It handles mode switching, floor detection, and camera fitting automatically.
953
956
 
954
957
  ```javascript
955
- map.setMode('indoor');
958
+ // 1. Load data from your API
959
+ const data = await fetch('/api/venues/siam-paragon/features').then(r => r.json());
960
+ map.geometry.import(data);
961
+
962
+ // 2. Select venue → auto enters indoor mode
963
+ const result = map.loadIndoorMap('siam_paragon');
964
+ console.log(result.floors); // [0, 1, 2, 3, 4]
965
+ console.log(result.currentFloor); // 0
966
+
967
+ // 3. Switch floors
968
+ map.setFloor(2); // Convenience alias for map.indoor.setFloor()
969
+
970
+ // 4. Query venue metadata
971
+ map.indoor.getVenueInfo(); // { id, name, floors, bounds, buildingCount }
972
+ map.indoor.getFloorInfo(2); // { name, unitCount, poiCount, beaconCount }
973
+
974
+ // 5. Exit
975
+ map.unloadIndoorMap();
956
976
  ```
957
977
 
978
+ | Method | Returns | Description |
979
+ | :--- | :--- | :--- |
980
+ | `loadIndoorMap(venueId, opts)` | `{ venue, floors, currentFloor }` | Select venue, enter indoor mode, fit bounds |
981
+ | `setFloor(level, opts)` | `PowerMap` | Convenience alias for `map.indoor.setFloor()` |
982
+ | `unloadIndoorMap()` | `PowerMap` | Exit indoor mode, clear selection |
983
+
958
984
  ### 16.2 Floor Management (`map.indoor`)
959
985
 
960
986
  When in indoor mode, use the `map.indoor` facade to control the vertical context of the map.
@@ -964,6 +990,11 @@ When in indoor mode, use the `map.indoor` facade to control the vertical context
964
990
  | `setFloor(ordinal, opts)` | `number, Object` | Switch to a specific floor level (e.g., 1, 2, -1). |
965
991
  | `getAvailableFloors()` | - | Returns a sorted array of all unique floor ordinals found in the map data. |
966
992
  | `getCurrentFloor()` | - | Returns the ordinal of the currently active floor. |
993
+ | `getActiveVenue()` | - | Returns the currently selected venue feature or `null`. |
994
+ | `getVenues()` | - | Lists all venue features in loaded data. |
995
+ | `getVenueInfo()` | - | Summary of active venue (name, floors, bounds, building count). |
996
+ | `getFloorInfo(ordinal)` | `number` | Floor details (unit count, POI count, beacon count). |
997
+ | `getBuildings()` | - | Lists all building features in loaded data. |
967
998
 
968
999
  #### Switching Floors with Animation
969
1000
 
@@ -992,19 +1023,58 @@ map.indoor.setCategoryVisible('walkway', true); // Navigation Paths
992
1023
  map.indoor.setCategoryVisible('obstacle', true); // 3D Obstacles/Buildings
993
1024
  ```
994
1025
 
995
- ### 16.4 Indoor Data Kinds
1026
+ ### 16.4 Indoor Data Kinds (IMDF Compliant)
996
1027
 
997
- The SDK categorizes indoor features using the `user_kind` (or `kind`) property:
1028
+ The SDK categorizes indoor features using `user_kind` (or `kind`) mapped to OGC IMDF `feature_type`:
998
1029
 
999
- - **floor**: The walkable surface area of a floor (Polygon).
1000
- - **obstacle**: Non-walkable areas. Can be extruded into 3D.
1001
- - **walkway**: Navigable path lines for indoor routing.
1002
- - **poi**: Indoor points of interest with custom icons.
1003
- - **beacon**: Positioning hardware or logical points.
1030
+ | SDK Kind | IMDF Feature Type | Geometry | Description |
1031
+ | :--- | :--- | :--- | :--- |
1032
+ | `venue` | venue | Polygon | Main venue (mall, airport) |
1033
+ | `building` | building | Polygon | Building within venue |
1034
+ | `floor` | level | Polygon | Floor plan surface |
1035
+ | `unit` | unit | Polygon | Room, corridor, area |
1036
+ | `opening` | opening | LineString | Door, gate, passage |
1037
+ | `amenity` | amenity | Point | ATM, restroom, etc. |
1038
+ | `anchor` | anchor | Point | Reference point |
1039
+ | `footprint` | footprint | Polygon | Building shadow |
1040
+ | `fixture` | fixture | Polygon | Furniture, permanent |
1041
+ | `section` | section | Polygon | Zone, terminal |
1042
+ | `geofence` | geofence | Polygon | Virtual fence |
1043
+ | `kiosk` | kiosk | Polygon | Service booth |
1044
+ | `detail` | detail | LineString | Crosswalk, railing |
1045
+ | `beacon` | anchor (beacon) | Point | BLE beacon device |
1046
+ | `poi` | anchor (poi) | Point | Point of interest |
1047
+ | `walkway` | pathway | LineString | Indoor navigation path |
1048
+ | `obstacle` | unit (restricted) | Polygon | Non-walkable area |
1049
+
1050
+ ### 16.5 IMDF Archive & Validation
1004
1051
 
1005
- ### 16.5 Event Synchronization
1052
+ ```javascript
1053
+ import { IMDFArchive, IMDFValidator } from '@powermap/web-sdk';
1054
+
1055
+ // Export as IMDF ZIP archive
1056
+ const blob = await IMDFArchive.toArchive(map.geometry.export());
1057
+
1058
+ // Import IMDF ZIP archive
1059
+ const fc = await IMDFArchive.fromArchive(zipFile);
1060
+ map.geometry.import(fc);
1061
+
1062
+ // Validate IMDF data
1063
+ const result = IMDFValidator.validate(map.geometry.export());
1064
+ if (!result.valid) console.error(result.errors);
1065
+ ```
1066
+
1067
+ > [!NOTE]
1068
+ > IMDF Archive requires [JSZip](https://stuk.github.io/jszip/) as an optional dependency.
1069
+
1070
+ ### 16.6 Event Synchronization
1006
1071
 
1007
1072
  ```javascript
1073
+ // Listen for venue selection
1074
+ map.on('indoorMapLoaded', ({ venue, floors, currentFloor }) => {
1075
+ console.log(`Entered: ${venue.properties?.name?.en}`);
1076
+ });
1077
+
1008
1078
  // Listen for mode changes
1009
1079
  map.on('onModeChanged', ({ mode }) => {
1010
1080
  console.log(`Mode switched to: ${mode}`); // 'standard' or 'indoor'
@@ -1114,6 +1184,9 @@ The SDK emits the following events through its central `EventBus`. You can subsc
1114
1184
  | Event | Payload | Description |
1115
1185
  |---|---|---|
1116
1186
  | `floorChanged` | `ordinal (number)` | Fired when the active indoor floor changes. |
1187
+ | `venueChanged` | `VenueInfo` | Fired when the active venue selection changes. |
1188
+ | `indoorMapLoaded` | `{ venue, floors, currentFloor }` | Fired when a venue is successfully selected via `loadIndoorMap()`. |
1189
+ | `indoorMapUnloaded` | `-` | Fired when indoor mode is exited via `unloadIndoorMap()`. |
1117
1190
 
1118
1191
  ### 🗺️ Routing & Navigation
1119
1192
  | Event | Payload | Description |
@@ -0,0 +1,409 @@
1
+ // ============================================================
2
+ // PowerMap Web SDK — TypeScript Declaration File
3
+ // Auto-generated from src/index.js JSDoc
4
+ // ============================================================
5
+
6
+ export interface PowerMapAuthOptions {
7
+ clientId: string;
8
+ clientSecret: string;
9
+ }
10
+
11
+ export interface PowerMapOptions {
12
+ /** The HTML element ID or element reference to mount the map into */
13
+ container: string | HTMLElement;
14
+ /** Initial map center as [longitude, latitude] */
15
+ center?: [number, number];
16
+ /** Initial zoom level */
17
+ zoom?: number;
18
+ /** Minimum zoom level */
19
+ minZoom?: number;
20
+ /** Maximum zoom level */
21
+ maxZoom?: number;
22
+ /** Map style name: 'th' | 'en' | 'dark' | 'gray' or a custom style URL */
23
+ style?: 'th' | 'en' | 'dark' | 'gray' | string;
24
+ /** API Key for map tiles */
25
+ mapApiKey: string;
26
+ /** Authentication credentials for services */
27
+ auth: PowerMapAuthOptions;
28
+ /** Disable all map interactions (zoom, pan, rotate) — preview mode */
29
+ previewOnly?: boolean;
30
+ }
31
+
32
+ export interface MarkerOptions {
33
+ /** [longitude, latitude] */
34
+ position: [number, number];
35
+ /** Marker type */
36
+ type?: 'start' | 'destination' | 'waypoint' | string;
37
+ /** Custom image URL */
38
+ iconUrl?: string;
39
+ /** Custom hex color for pinpoint marker (e.g. '#ff4444') */
40
+ color?: string;
41
+ /** FontAwesome icon class (e.g. 'fas fa-car') */
42
+ icon?: string;
43
+ /** Enable marker dragging */
44
+ draggable?: boolean;
45
+ /** HTML content or structured popup object */
46
+ popupContent?: string | { title?: string; body?: string; tag?: string; footer?: string };
47
+ /** Unique identifier for the marker */
48
+ id?: string;
49
+ }
50
+
51
+ export interface MarkerInstance {
52
+ id: string;
53
+ position: [number, number];
54
+ remove(): void;
55
+ setLngLat(lngLat: [number, number]): this;
56
+ getLngLat(): { lng: number; lat: number };
57
+ [key: string]: unknown;
58
+ }
59
+
60
+ export interface RouteOptions {
61
+ start: [number, number];
62
+ destination: [number, number];
63
+ waypoints?: [number, number][];
64
+ optimize?: boolean;
65
+ [key: string]: unknown;
66
+ }
67
+
68
+ export interface RouteStep {
69
+ distance: number;
70
+ distanceStr: string;
71
+ duration: number;
72
+ durationStr: string;
73
+ instruction?: string;
74
+ [key: string]: unknown;
75
+ }
76
+
77
+ export interface RouteDetails {
78
+ distance: number;
79
+ distanceStr: string;
80
+ duration: number;
81
+ durationStr: string;
82
+ steps: RouteStep[];
83
+ }
84
+
85
+ export interface RouteSummary {
86
+ distanceStr: string;
87
+ durationStr: string;
88
+ }
89
+
90
+ export interface SearchResult {
91
+ id: string;
92
+ name: string;
93
+ address?: string;
94
+ position: [number, number];
95
+ [key: string]: unknown;
96
+ }
97
+
98
+ export interface SearchOptions {
99
+ location?: [number, number];
100
+ lang?: string;
101
+ [key: string]: unknown;
102
+ }
103
+
104
+ export interface FlyToOptions {
105
+ center?: [number, number];
106
+ zoom?: number;
107
+ pitch?: number;
108
+ bearing?: number;
109
+ speed?: number;
110
+ curve?: number;
111
+ [key: string]: unknown;
112
+ }
113
+
114
+ export interface FitBoundsOptions {
115
+ padding?: number | { top: number; bottom: number; left: number; right: number };
116
+ maxZoom?: number;
117
+ [key: string]: unknown;
118
+ }
119
+
120
+ export type PowerMapEventName =
121
+ | 'mapLoaded'
122
+ | 'mapClick'
123
+ | 'mapLongClick'
124
+ | 'markerClick'
125
+ | 'markerDrag'
126
+ | 'markerDragEnd'
127
+ | 'poiClick'
128
+ | 'routeCalculated'
129
+ | 'modeChanged'
130
+ | 'floorChanged'
131
+ | 'venueChanged'
132
+ | 'indoorMapLoaded'
133
+ | 'indoorMapUnloaded'
134
+ | 'error'
135
+ | string;
136
+
137
+ export type PowerMapMode = 'standard' | 'indoor';
138
+
139
+ export interface SetModeOptions {
140
+ buildingId?: string;
141
+ floorId?: string;
142
+ center?: [number, number];
143
+ [key: string]: unknown;
144
+ }
145
+
146
+ // ── IMDF Types ──────────────────────────────────────────────────────────────────
147
+
148
+ /** OGC IMDF 1.0.0 Feature Types */
149
+ export type IMDFFeatureType =
150
+ | 'venue' | 'building' | 'level' | 'unit' | 'opening' | 'amenity'
151
+ | 'anchor' | 'footprint' | 'fixture' | 'section' | 'geofence'
152
+ | 'kiosk' | 'detail' | 'occupant' | 'address' | 'relationship'
153
+ | 'pathway';
154
+
155
+ /** SDK internal kind names */
156
+ export type GeometryKind =
157
+ | 'venue' | 'building' | 'floor' | 'unit' | 'opening' | 'amenity'
158
+ | 'anchor' | 'footprint' | 'fixture' | 'section' | 'geofence'
159
+ | 'kiosk' | 'detail' | 'occupant' | 'address' | 'relationship'
160
+ | 'beacon' | 'poi' | 'walkway' | 'obstacle';
161
+
162
+ /** Category visibility toggles */
163
+ export type CategoryType = 'poi' | 'beacon' | 'walkway' | 'obstacle';
164
+
165
+ /** IMDF LABELS object (i18n) */
166
+ export interface IMDFLabels {
167
+ [languageCode: string]: string;
168
+ }
169
+
170
+ // ── Indoor Types ────────────────────────────────────────────────────────────────
171
+
172
+ export interface VenueInfo {
173
+ id: string;
174
+ name: string;
175
+ category: string;
176
+ floors: number[];
177
+ bounds: [[number, number], [number, number]] | null;
178
+ buildingCount: number;
179
+ }
180
+
181
+ export interface VenueListItem {
182
+ id: string;
183
+ name: string;
184
+ category: string;
185
+ feature: object;
186
+ }
187
+
188
+ export interface FloorInfo {
189
+ id: string;
190
+ name: string;
191
+ shortName: string;
192
+ ordinal: number;
193
+ outdoor: boolean;
194
+ unitCount: number;
195
+ poiCount: number;
196
+ beaconCount: number;
197
+ }
198
+
199
+ export interface BuildingInfo {
200
+ id: string;
201
+ name: string;
202
+ category: string;
203
+ }
204
+
205
+ export interface LoadIndoorMapOptions {
206
+ initialFloor?: number;
207
+ fitBounds?: boolean;
208
+ fitBoundsOptions?: FitBoundsOptions;
209
+ animate?: boolean;
210
+ }
211
+
212
+ export interface LoadIndoorMapResult {
213
+ venue: object;
214
+ floors: number[];
215
+ currentFloor: number;
216
+ }
217
+
218
+ export interface FloorSwitchOptions {
219
+ animate?: boolean;
220
+ duration?: number;
221
+ }
222
+
223
+ // ── FloorManager ────────────────────────────────────────────────────────────────
224
+
225
+ export interface FloorManager {
226
+ /** Set the current floor ordinal with optional animation */
227
+ setFloor(ordinal: number, options?: FloorSwitchOptions): Promise<FloorManager>;
228
+ /** Get all unique floor ordinals present in the map */
229
+ getAvailableFloors(): number[];
230
+ /** Get the current floor ordinal */
231
+ getCurrentFloor(): number;
232
+ /** Set visibility for a category of indoor objects */
233
+ setCategoryVisible(type: CategoryType, visible: boolean): FloorManager;
234
+ /** Get the currently active venue feature */
235
+ getActiveVenue(): object | null;
236
+ /** List all venue features available in loaded geometry */
237
+ getVenues(): VenueListItem[];
238
+ /** Get summary information about the currently active venue */
239
+ getVenueInfo(): VenueInfo | null;
240
+ /** Get detailed information about a specific floor */
241
+ getFloorInfo(ordinal: number): FloorInfo | null;
242
+ /** List all building features in loaded geometry */
243
+ getBuildings(): BuildingInfo[];
244
+ }
245
+
246
+ // ── GeometryQuery ───────────────────────────────────────────────────────────────
247
+
248
+ export interface GeometryQuery {
249
+ byKind(kind: GeometryKind): GeometryQuery;
250
+ byFloor(ordinal: number): GeometryQuery;
251
+ byType(type: 'Point' | 'Polygon' | 'LineString'): GeometryQuery;
252
+ where(prop: string, op: 'eq' | 'neq' | 'contains' | 'gt' | 'lt' | 'exists', value?: unknown): GeometryQuery;
253
+ byFeatureType(featureType: IMDFFeatureType): GeometryQuery;
254
+ byVenueId(venueId: string): GeometryQuery;
255
+ byBuildingId(buildingId: string): GeometryQuery;
256
+ byLevelId(levelId: string): GeometryQuery;
257
+ toArray(): object[];
258
+ first(): object | null;
259
+ count(): number;
260
+ ids(): string[];
261
+ }
262
+
263
+ // ── Validation ──────────────────────────────────────────────────────────────────
264
+
265
+ export interface ValidationError {
266
+ code: string;
267
+ message: string;
268
+ featureId?: string;
269
+ }
270
+
271
+ export interface ValidationResult {
272
+ valid: boolean;
273
+ errors: ValidationError[];
274
+ warnings: ValidationError[];
275
+ }
276
+
277
+ // ── GeometryManager ─────────────────────────────────────────────────────────────
278
+
279
+ export interface GeometryManager {
280
+ query(): GeometryQuery;
281
+ create(kind: GeometryKind, options?: object): string | null;
282
+ add(geojson: object): void;
283
+ update(id: string, patch: object): void;
284
+ remove(id: string): void;
285
+ getById(id: string): object | null;
286
+ getByKind(kind: GeometryKind): object[];
287
+ getGeoJSON(id: string, options?: { format: 'imdf' | 'standard' }): object | null;
288
+ export(options?: { format: 'imdf' | 'standard' }): object;
289
+ import(geojson: object, options?: { format: 'imdf' | 'standard' }): GeometryManager;
290
+ exportArchive?(): Promise<Blob>;
291
+ importArchive?(file: Blob | File): Promise<void>;
292
+ clear(): void;
293
+ }
294
+
295
+ // ── PowerMap Main Class ─────────────────────────────────────────────────────────
296
+
297
+ /**
298
+ * PowerMap SDK Main Class
299
+ *
300
+ * Provides a high-level API for map rendering, markers, routing,
301
+ * indoor navigation, and vehicle simulation.
302
+ *
303
+ * @example
304
+ * const map = new PowerMap({
305
+ * container: 'map',
306
+ * mapApiKey: 'YOUR_KEY',
307
+ * auth: { clientId: '...', clientSecret: '...' }
308
+ * })
309
+ * .on('mapLoaded', () => console.log('Ready!'))
310
+ * .setStyle('dark');
311
+ */
312
+ export declare class PowerMap {
313
+ constructor(options: PowerMapOptions);
314
+
315
+ // --- Events ---
316
+ on(event: PowerMapEventName, callback: (...args: unknown[]) => void): this;
317
+ off(event: PowerMapEventName, callback: (...args: unknown[]) => void): this;
318
+ onMapLoaded(callback: (data: { map: PowerMap }) => void): this;
319
+ onClick(callback: (event: unknown) => void): this;
320
+ onMapLongClick(callback: (event: unknown) => void): this;
321
+ onMarkerClick(callback: (data: { marker: MarkerInstance; originalEvent: unknown; options: unknown }) => void): this;
322
+ onPoiClick(callback: (data: { feature: unknown; lngLat: unknown; point: unknown }) => void): this;
323
+ onMarkerDrag(callback: (data: { marker: MarkerInstance; lngLat: [number, number] }) => void): this;
324
+ onMarkerDragEnd(callback: (data: { marker: MarkerInstance; lngLat: [number, number] }) => void): this;
325
+ onRouteCalculated(callback: (route: unknown) => void): this;
326
+ onError(callback: (error: Error) => void): this;
327
+
328
+ // --- Map Style & Mode ---
329
+ setStyle(name: 'th' | 'en' | 'dark' | 'gray' | string): this;
330
+ setMode(mode: PowerMapMode, options?: SetModeOptions): this;
331
+
332
+ // --- Controls ---
333
+ setNavigationControl(options: boolean | object): this;
334
+ setFullscreenControl(options: boolean | object): this;
335
+
336
+ // --- Camera ---
337
+ flyTo(options: FlyToOptions): this;
338
+ easeTo(options: FlyToOptions): this;
339
+ fitBounds(bounds: [[number, number], [number, number]], options?: FitBoundsOptions): this;
340
+ jumpTo(options: FlyToOptions): this;
341
+ setPitch(pitch: number, animate?: boolean): this;
342
+ setBearing(bearing: number, animate?: boolean): this;
343
+ zoomIn(): this;
344
+ zoomOut(): this;
345
+ rotateLeft(): this;
346
+ rotateRight(): this;
347
+ getCenter(): { lng: number; lat: number };
348
+ getZoom(): number;
349
+ getPitch(): number;
350
+ getBearing(): number;
351
+
352
+ // --- Markers ---
353
+ addMarker(options: MarkerOptions): MarkerInstance;
354
+ getMarker(id: string): MarkerInstance | null;
355
+ removeMarker(markerOrId: MarkerInstance | string): this;
356
+ updatePopup(marker: MarkerInstance, content: string | object): this;
357
+ clearMarkers(): this;
358
+
359
+ // --- Routing ---
360
+ addRoute(options: RouteOptions): Promise<unknown>;
361
+ clearRoutes(): this;
362
+ getRouteDetails(): RouteDetails | null;
363
+ getRouteSummary(): RouteSummary | null;
364
+ formatDistance(meters: number): string;
365
+ formatDuration(seconds: number): string;
366
+
367
+ // --- Search ---
368
+ search(query: string, options?: SearchOptions): Promise<SearchResult[]>;
369
+
370
+ // --- Layers ---
371
+ showLayer(layerId: string): Promise<this>;
372
+ hideLayer(layerId: string): this;
373
+ toggleLayer(layerId: string): Promise<this>;
374
+
375
+ // --- Indoor Venue Selection ---
376
+ /** Select and activate an indoor venue from already-loaded geometry data */
377
+ loadIndoorMap(venueId: string, options?: LoadIndoorMapOptions): LoadIndoorMapResult;
378
+ /** Convenience alias for map.indoor.setFloor() */
379
+ setFloor(level: number, options?: FloorSwitchOptions): this;
380
+ /** Exit indoor mode and clear the active venue selection */
381
+ unloadIndoorMap(): this;
382
+
383
+ // --- Managers (advanced access) ---
384
+ readonly layers: unknown;
385
+ readonly geometry: GeometryManager;
386
+ readonly indoor: FloorManager;
387
+ readonly editor: unknown;
388
+ debug(): unknown;
389
+ }
390
+
391
+ // ── IMDF Utilities ──────────────────────────────────────────────────────────────
392
+
393
+ export declare class IMDFConverter {
394
+ static MAPPING: Record<GeometryKind, { feature_type: string; category: string }>;
395
+ static IMDF_CATEGORIES: Record<string, string[]>;
396
+ static REQUIRED_PROPERTIES: Record<string, string[]>;
397
+ static toIMDF(featureCollection: object): object;
398
+ static fromIMDF(imdfCollection: object): object;
399
+ }
400
+
401
+ export declare class IMDFArchive {
402
+ static toArchive(featureCollection: object, options?: { JSZip?: unknown }): Promise<Blob>;
403
+ static fromArchive(archiveData: Blob | File | ArrayBuffer, options?: { JSZip?: unknown }): Promise<object>;
404
+ static generateManifest(featureCollection: object): object;
405
+ }
406
+
407
+ export declare class IMDFValidator {
408
+ static validate(featureCollection: object, options?: { strict?: boolean }): ValidationResult;
409
+ }