@mappedin/blue-dot 6.14.0-beta.0 → 6.15.0-beta.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
@@ -33,20 +33,26 @@ blueDot.enable();
33
33
  // Listening to device position updates from the browser
34
34
  blueDot.watchDevicePosition(true);
35
35
 
36
- // Send custom position updates from a different source
37
- blueDot.update({
38
- // Latitude of the update
39
- latitude,
40
- // Longitude of the update
41
- longitude,
42
- // Optional accuracy in meters
43
- accuracy,
44
- // Optional heading in degrees from north
45
- heading,
46
- // Optional floor or floor ID
47
- floorOrFloorId,
36
+ // Report a position from an external source (e.g., custom IPS)
37
+ blueDot.reportPosition({
38
+ latitude: 43.6532,
39
+ longitude: -79.3832,
40
+ confidence: 0.8, // 0-1 confidence score
41
+ heading: 90, // Optional heading in degrees
42
+ floorLevel: 0, // Optional floor level
48
43
  });
49
44
 
45
+ // Force position to a known location (overrides all other sources)
46
+ blueDot.forcePosition(
47
+ {
48
+ latitude: 43.6532,
49
+ longitude: -79.3832,
50
+ heading: 90,
51
+ floorLevel: 0,
52
+ },
53
+ 30000, // How long to hold this position in ms
54
+ );
55
+
50
56
  // Attach the camera to the BlueDot
51
57
  blueDot.follow('position-only');
52
58
  ```
@@ -59,7 +65,7 @@ blueDot.follow('position-only');
59
65
 
60
66
  ```ts
61
67
  // Browser will prompt for permission
62
- await blueDot.watchDevicePosition(true);
68
+ blueDot.watchDevicePosition(true);
63
69
 
64
70
  // Listen for errors (including permission denied)
65
71
  blueDot.on('error', error => {
@@ -115,7 +121,24 @@ Disable and hide the BlueDot. Stops all position tracking.
115
121
  blueDot.disable();
116
122
  ```
117
123
 
118
- #### `watchDevicePosition(watch: boolean): Promise<void>`
124
+ #### `getState(): ReadonlyDeep<BlueDotState>`
125
+
126
+ Returns the current BlueDot configuration (radius, colors, timeout, etc.).
127
+
128
+ ```ts
129
+ const state = blueDot.getState();
130
+ console.log(state.radius, state.color);
131
+ ```
132
+
133
+ #### `updateState(options: BlueDotUpdateState): void`
134
+
135
+ Update BlueDot options after it has been enabled (e.g. colors, timeout, accuracy threshold).
136
+
137
+ ```ts
138
+ blueDot.updateState({ color: '#ff0000', timeout: 60000 });
139
+ ```
140
+
141
+ #### `watchDevicePosition(watch: boolean): void`
119
142
 
120
143
  Start or stop listening to device GPS position.
121
144
 
@@ -124,8 +147,8 @@ Start or stop listening to device GPS position.
124
147
  - Emits `error` events on geolocation errors
125
148
 
126
149
  ```ts
127
- await blueDot.watchDevicePosition(true); // Start tracking
128
- await blueDot.watchDevicePosition(false); // Stop tracking
150
+ blueDot.watchDevicePosition(true); // Start tracking
151
+ blueDot.watchDevicePosition(false); // Stop tracking
129
152
  ```
130
153
 
131
154
  #### `watchDeviceOrientation(watch: boolean): Promise<void>`
@@ -143,9 +166,61 @@ button.addEventListener('click', async () => {
143
166
  });
144
167
  ```
145
168
 
146
- #### `update(position, options?)`
169
+ #### `reportPosition(options)`
170
+
171
+ Report a position from an external source (e.g., custom Indoor Positioning System). The position is fed into the fusion engine with a confidence score that influences how much weight it receives relative to other sources like GPS.
172
+
173
+ ```ts
174
+ blueDot.reportPosition({
175
+ latitude: 43.6532,
176
+ longitude: -79.3832,
177
+ confidence: 0.8, // 0-1: higher = more influence on fused position
178
+ heading: 90, // Optional: degrees from north
179
+ floorLevel: 0, // Optional: floor level
180
+ });
181
+ ```
182
+
183
+ Parameters:
184
+
185
+ - `latitude: number` - Latitude coordinate (required)
186
+ - `longitude: number` - Longitude coordinate (required)
187
+ - `confidence?: number` - Confidence score from 0 to 1 (default: 0.5)
188
+ - `accuracy?: number` - Accuracy in meters
189
+ - `heading?: number` - Heading in degrees from north
190
+ - `floorLevel?: number` - Floor level
191
+ - `timestamp?: number` - Timestamp in milliseconds (default: Date.now())
192
+
193
+ #### `forcePosition(position, durationMs?)`
194
+
195
+ Force the BlueDot to a specific position, overriding all other data sources for a specified duration. Use this when you have an authoritative position from a calibration source like Visual Positioning (VPS) or AI Localizer.
196
+
197
+ ```ts
198
+ blueDot.forcePosition(
199
+ {
200
+ latitude: 43.6532,
201
+ longitude: -79.3832,
202
+ heading: 90, // Optional: degrees from north
203
+ floorLevel: 0, // Optional: floor level
204
+ },
205
+ 30000, // How long to hold (default: 30 seconds)
206
+ );
207
+ ```
208
+
209
+ Parameters:
210
+
211
+ - `position.latitude: number` - Latitude coordinate (required)
212
+ - `position.longitude: number` - Longitude coordinate (required)
213
+ - `position.heading?: number` - Heading in degrees from north
214
+ - `position.floorLevel?: number` - Floor level
215
+ - `durationMs?: number` - Duration in milliseconds (default: 30000)
216
+
217
+ During the forced period, GPS and other position sources are ignored. After expiration, the BlueDot transitions back to using fused position data. Emits `anchor-set` when activated and `anchor-expired` when the duration ends.
218
+
219
+ #### `update(position, options?)` _(deprecated)_
220
+
221
+ > **Deprecated**: Use `reportPosition()` for feeding positions into the fusion engine, or `forcePosition()` to set an authoritative position anchor.
147
222
 
148
- Manually set or override position properties.
223
+ Legacy method to manually set or override position properties.
149
224
 
150
225
  ```ts
151
226
  // Set full position
@@ -184,11 +259,13 @@ blueDot.follow('position-and-path-direction');
184
259
  blueDot.follow(false);
185
260
  ```
186
261
 
187
- Camera options:
262
+ Camera options (`FollowCameraOptions`):
188
263
 
189
- - `zoomLevel?: number` - Target zoom level
190
- - `pitch?: number` - Camera pitch angle
191
- - `animationDuration?: number` - Animation duration in ms
264
+ - `zoomLevel?: number` - Target zoom level (default: 21)
265
+ - `pitch?: number` - Camera pitch angle (default: 45)
266
+ - `bearing?: number` - Camera bearing in degrees (position-only mode only)
267
+ - `duration?: number` - Animation duration in ms (default: 1000)
268
+ - `easing?: 'ease-in' | 'ease-out' | 'ease-in-out' | 'linear'` - Animation easing (default: 'ease-in-out')
192
269
 
193
270
  #### `setPositionProcessor(processor?)`
194
271
 
@@ -272,8 +349,8 @@ Fired when BlueDot status changes.
272
349
 
273
350
  ```ts
274
351
  blueDot.on('status-change', event => {
275
- console.log(event.status); // New status
276
- console.log(event.previousStatus); // Previous status
352
+ console.log(event.status); // New status: 'hidden' | 'active' | 'inactive' | 'disabled'
353
+ console.log(event.action); // Action that caused the change: 'timeout' | 'error' | 'position-update' | 'enable' | 'disable' | 'initialize'
277
354
  });
278
355
  ```
279
356
 
@@ -303,7 +380,8 @@ Fired when follow mode changes.
303
380
 
304
381
  ```ts
305
382
  blueDot.on('follow-change', event => {
306
- console.log(event.mode); // Current follow mode or false
383
+ console.log(event.following); // Whether the camera is following the BlueDot
384
+ console.log(event.mode); // Follow mode when following: 'position-only' | 'position-and-heading' | 'position-and-path-direction'
307
385
  });
308
386
  ```
309
387
 
@@ -327,10 +405,38 @@ blueDot.on('hover', event => {
327
405
  });
328
406
  ```
329
407
 
408
+ ### `anchor-set`
409
+
410
+ Fired when a position anchor is set via `forcePosition()` or a custom sensor.
411
+
412
+ ```ts
413
+ blueDot.on('anchor-set', event => {
414
+ console.log('Anchor set:', event.anchor);
415
+ console.log('Position:', event.anchor.latitude, event.anchor.longitude);
416
+ console.log('From sensor:', event.anchor.sensorId);
417
+ });
418
+ ```
419
+
420
+ ### `anchor-expired`
421
+
422
+ Fired when a position anchor expires after its duration.
423
+
424
+ ```ts
425
+ blueDot.on('anchor-expired', event => {
426
+ console.log('Anchor expired:', event.anchor.sensorId);
427
+ // BlueDot will now use fused position data again
428
+ });
429
+ ```
430
+
330
431
  ## Options
331
432
 
433
+ Options for `enable()` and `updateState()`. All properties are optional. The type is exported as `BlueDotUpdateState` from `@mappedin/blue-dot`.
434
+
332
435
  ```ts
333
- export type BlueDotOptions = {
436
+ import type { BlueDotUpdateState } from '@mappedin/blue-dot';
437
+
438
+ // BlueDotUpdateState structure (all properties optional)
439
+ interface Options {
334
440
  /**
335
441
  * The radius of the BlueDot in pixels. The BlueDot will maintain this size clamped to a minimum of 0.35 metres.
336
442
  * @default 10
@@ -399,12 +505,12 @@ export type BlueDotOptions = {
399
505
  initialState?: 'hidden' | 'inactive';
400
506
  /**
401
507
  * Whether to prevent position updates outside the map bounds.
402
- * @default false
508
+ * @default true
403
509
  */
404
510
  preventOutOfBounds?: boolean;
405
511
  /**
406
- * Maximum accuracy (in meters) to accept. Updates with lower accuracy are ignored.
407
- * @default Infinity
512
+ * Maximum accuracy (in meters) to accept. Updates exceeding this value are dropped.
513
+ * @default 50
408
514
  */
409
515
  accuracyThreshold?: number;
410
516
  /**
@@ -412,9 +518,114 @@ export type BlueDotOptions = {
412
518
  * @default false
413
519
  */
414
520
  debug?: boolean;
415
- };
521
+ }
522
+ ```
523
+
524
+ ## Custom Sensors
525
+
526
+ You can create custom sensors to provide position data from your own sources (e.g., BLE beacons, Wi-Fi triangulation, visual positioning). Custom sensors extend `BaseSensor` and can publish position updates or set authoritative position anchors.
527
+
528
+ ### Creating a Custom Sensor
529
+
530
+ ```ts
531
+ import { BaseSensor } from '@mappedin/blue-dot';
532
+
533
+ class MyPositioningSensor extends BaseSensor {
534
+ readonly id = 'my-ips';
535
+ readonly requiresPermission = false;
536
+
537
+ protected async start(): Promise<void> {
538
+ // Start your positioning system
539
+ this.myIps.onPosition((lat, lng, confidence) => {
540
+ // Publish absolute position updates via the internal event system
541
+ this.publish('absolute-update', {
542
+ sensorId: this.id,
543
+ update: {
544
+ latitude: lat,
545
+ longitude: lng,
546
+ confidence, // 0-1
547
+ timestamp: Date.now(),
548
+ },
549
+ });
550
+ });
551
+ }
552
+
553
+ protected stop(): void {
554
+ // Clean up your positioning system
555
+ this.myIps.stop();
556
+ }
557
+
558
+ async checkPermission(): Promise<'granted' | 'denied' | 'prompt' | 'unavailable'> {
559
+ return 'granted';
560
+ }
561
+
562
+ async requestPermission(): Promise<'granted' | 'denied' | 'prompt' | 'unavailable'> {
563
+ return 'granted';
564
+ }
565
+ }
566
+ ```
567
+
568
+ ### Setting Position Anchors from Custom Sensors
569
+
570
+ Custom sensors can set authoritative position anchors using `this.setAnchor()`. This is useful for calibration sources like Visual Positioning (VPS) or AI localization.
571
+
572
+ ```ts
573
+ class MyLocalizerSensor extends BaseSensor {
574
+ readonly id = 'my-localizer';
575
+ readonly requiresPermission = false;
576
+
577
+ async localize(imageData: Blob): Promise<void> {
578
+ const result = await this.callLocalizerApi(imageData);
579
+
580
+ // Set an anchor - this overrides other position sources
581
+ this.setAnchor({
582
+ latitude: result.latitude,
583
+ longitude: result.longitude,
584
+ heading: result.bearing,
585
+ floorLevel: result.floor,
586
+ ttl: 60000, // Anchor valid for 60 seconds
587
+ confidence: 0.95, // High confidence
588
+ anchorOnlyPeriodMs: 5000, // Ignore GPS for first 5 seconds
589
+ });
590
+ }
591
+
592
+ clearLocalization(): void {
593
+ this.clearAnchor();
594
+ }
595
+
596
+ // ... other required methods
597
+ }
598
+ ```
599
+
600
+ ### Registering Custom Sensors
601
+
602
+ ```ts
603
+ const blueDot = new BlueDot(mapView);
604
+
605
+ // Create and register your sensor
606
+ const mySensor = new MyPositioningSensor();
607
+ blueDot.Sensors.register(mySensor);
608
+
609
+ // Enable the sensor
610
+ mySensor.enable();
611
+
612
+ // Access built-in sensors via .get()
613
+ blueDot.Sensors.get('geolocation').enable();
614
+ blueDot.Sensors.get('deviceorientation').enable();
416
615
  ```
417
616
 
617
+ ### Anchor Options
618
+
619
+ When calling `setAnchor()`, you can configure:
620
+
621
+ - `latitude: number` - Latitude coordinate (required)
622
+ - `longitude: number` - Longitude coordinate (required)
623
+ - `heading?: number` - Heading in degrees from north
624
+ - `floorLevel?: number` - Floor level
625
+ - `ttl?: number` - Time-to-live in milliseconds (default: 30000)
626
+ - `confidence?: number` - Confidence score 0-1 (default: 1.0)
627
+ - `anchorOnlyPeriodMs?: number` - Duration to ignore other sources (default: 5000)
628
+
418
629
  ## React Native
419
630
 
420
631
  ```tsx
@@ -442,15 +653,17 @@ function BlueDotDisplay() {
442
653
  const {
443
654
  isReady,
444
655
  isEnabled,
445
- state,
446
- position,
656
+ status,
657
+ coordinate,
447
658
  floor,
448
659
  isFollowing,
449
660
  accuracy,
450
661
  heading,
451
662
  enable,
452
663
  disable,
453
- update,
664
+ update, // Deprecated: use reportPosition or forcePosition
665
+ reportPosition,
666
+ forcePosition,
454
667
  follow,
455
668
  } = useBlueDot();
456
669
 
@@ -462,11 +675,11 @@ function BlueDotDisplay() {
462
675
  }, []),
463
676
  );
464
677
 
465
- // Listen for state changes
678
+ // Listen for status changes
466
679
  useBlueDotEvent(
467
- 'state-change',
680
+ 'status-change',
468
681
  useCallback(event => {
469
- console.log('State changed:', event.state);
682
+ console.log('Status changed:', event.status);
470
683
  }, []),
471
684
  );
472
685
 
@@ -478,6 +691,21 @@ function BlueDotDisplay() {
478
691
  }, []),
479
692
  );
480
693
 
694
+ // Listen for anchor events (from forcePosition or custom sensors)
695
+ useBlueDotEvent(
696
+ 'anchor-set',
697
+ useCallback(event => {
698
+ console.log('Anchor set from:', event.anchor.sensorId);
699
+ }, []),
700
+ );
701
+
702
+ useBlueDotEvent(
703
+ 'anchor-expired',
704
+ useCallback(event => {
705
+ console.log('Anchor expired:', event.anchor.sensorId);
706
+ }, []),
707
+ );
708
+
481
709
  useEffect(() => {
482
710
  if (isReady && !isEnabled) {
483
711
  // All methods are async - use await or .then()
@@ -489,15 +717,14 @@ function BlueDotDisplay() {
489
717
  }
490
718
  }, [isReady, isEnabled, enable]);
491
719
 
492
- const handleUpdatePosition = useCallback(async () => {
720
+ const handleReportPosition = useCallback(async () => {
493
721
  try {
494
- // Update position manually
495
- await update({
722
+ // Report position from external IPS
723
+ await reportPosition({
496
724
  latitude: 43.6532,
497
725
  longitude: -79.3832,
498
- accuracy: 5,
726
+ confidence: 0.8,
499
727
  heading: 90,
500
- floorOrFloorId: floor,
501
728
  });
502
729
 
503
730
  // Enable follow mode
@@ -505,25 +732,45 @@ function BlueDotDisplay() {
505
732
  zoomLevel: 19,
506
733
  });
507
734
  } catch (error) {
508
- console.error('Failed to update position:', error);
735
+ console.error('Failed to report position:', error);
736
+ }
737
+ }, [reportPosition, follow]);
738
+
739
+ const handleForcePosition = useCallback(async () => {
740
+ try {
741
+ // Force position from calibration source
742
+ await forcePosition(
743
+ {
744
+ latitude: 43.6532,
745
+ longitude: -79.3832,
746
+ heading: 90,
747
+ floorLevel: 0,
748
+ },
749
+ 30000,
750
+ );
751
+ } catch (error) {
752
+ console.error('Failed to force position:', error);
509
753
  }
510
- }, [update, follow, floor]);
754
+ }, [forcePosition]);
511
755
 
512
756
  return (
513
757
  <View>
514
758
  <Text>Is Ready: {isReady ? 'Yes' : 'No'}</Text>
515
759
  <Text>Is Enabled: {isEnabled ? 'Yes' : 'No'}</Text>
516
- <Text>State: {state}</Text>
760
+ <Text>Status: {status}</Text>
517
761
  <Text>Following: {isFollowing ? 'Yes' : 'No'}</Text>
518
- {position && (
762
+ {coordinate && (
519
763
  <Text>
520
- Position: {position.latitude.toFixed(4)}, {position.longitude.toFixed(4)}
764
+ Position: {coordinate.latitude.toFixed(4)}, {coordinate.longitude.toFixed(4)}
521
765
  </Text>
522
766
  )}
523
767
  {accuracy && <Text>Accuracy: {accuracy.toFixed(1)}m</Text>}
524
768
  {heading && <Text>Heading: {heading.toFixed(0)}°</Text>}
525
- <TouchableOpacity onPress={handleUpdatePosition}>
526
- <Text>Update Position & Follow</Text>
769
+ <TouchableOpacity onPress={handleReportPosition}>
770
+ <Text>Report Position & Follow</Text>
771
+ </TouchableOpacity>
772
+ <TouchableOpacity onPress={handleForcePosition}>
773
+ <Text>Force Position (Calibration)</Text>
527
774
  </TouchableOpacity>
528
775
  </View>
529
776
  );
@@ -532,6 +779,6 @@ function BlueDotDisplay() {
532
779
 
533
780
  **Key Differences from Vanilla JS:**
534
781
 
535
- - **All methods are async**: `enable()`, `disable()`, `update()`, and `follow()` return Promises
782
+ - **All methods are async**: `enable()`, `disable()`, `reportPosition()`, `forcePosition()`, and `follow()` return Promises
536
783
  - **Rich state**: Hook returns `isReady`, `state`, `position`, `floor`, `isFollowing`, `accuracy`, `heading` for real-time updates
537
- - **Separate event hook**: Use `useBlueDotEvent` for listening to position-update, state-change, and follow-change events
784
+ - **Separate event hook**: Use `useBlueDotEvent` for listening to position-update, state-change, follow-change, anchor-set, and anchor-expired events