@ateam-ai-mobile/expo 0.1.7 → 0.1.8
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/dist/bootstrap.js +841 -323
- package/dist/bootstrap.js.map +1 -1
- package/dist/bootstrap.mjs +9 -7
- package/dist/bootstrap.mjs.map +1 -1
- package/dist/chunk-7FWYEHN5.mjs +203 -0
- package/dist/chunk-7FWYEHN5.mjs.map +1 -0
- package/dist/{chunk-4XYXJYN5.mjs → chunk-C7M5WAHA.mjs} +583 -298
- package/dist/chunk-C7M5WAHA.mjs.map +1 -0
- package/dist/{chunk-5HM6UA2K.mjs → chunk-GF2MPTAB.mjs} +83 -7
- package/dist/chunk-GF2MPTAB.mjs.map +1 -0
- package/dist/chunk-J6XRNX7P.mjs +19 -0
- package/dist/chunk-J6XRNX7P.mjs.map +1 -0
- package/dist/{chunk-TZXWVYYR.mjs → chunk-PCAEUX2N.mjs} +1 -1
- package/dist/chunk-PCAEUX2N.mjs.map +1 -0
- package/dist/{chunk-VQJPSLHP.mjs → chunk-Z5AI2SXX.mjs} +13 -4
- package/dist/chunk-Z5AI2SXX.mjs.map +1 -0
- package/dist/createBridge.js +41 -10
- package/dist/createBridge.js.map +1 -1
- package/dist/createBridge.mjs +5 -1
- package/dist/createBridge.mjs.map +1 -1
- package/dist/device/createDeviceProvider.js +278 -17
- package/dist/device/createDeviceProvider.js.map +1 -1
- package/dist/device/createDeviceProvider.mjs +5 -3
- package/dist/device-bridge/index.d.mts +13 -1
- package/dist/device-bridge/index.d.ts +13 -1
- package/dist/device-bridge/index.js +49 -12
- package/dist/device-bridge/index.js.map +1 -1
- package/dist/device-bridge/index.mjs +3 -1
- package/dist/index.d.mts +19 -3
- package/dist/index.d.ts +19 -3
- package/dist/index.js +594 -297
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/maps/index.d.mts +56 -0
- package/dist/maps/index.d.ts +56 -0
- package/dist/maps/index.js +95 -0
- package/dist/maps/index.js.map +1 -0
- package/dist/maps/index.mjs +17 -0
- package/dist/maps/index.mjs.map +1 -0
- package/dist/vision/index.d.mts +4 -2
- package/dist/vision/index.d.ts +4 -2
- package/dist/vision/index.js.map +1 -1
- package/dist/vision/index.mjs +1 -1
- package/expo-module.config.json +1 -1
- package/ios/AdaMaps/AdaMapsModule.swift +180 -0
- package/ios/AdaMaps.podspec +25 -0
- package/ios/AdaVision.podspec +4 -0
- package/ios/AdaVisionModule.swift +63 -9
- package/package.json +10 -4
- package/src/args.ts +56 -0
- package/src/createBridge.ts +3 -1
- package/src/device/createDeviceProvider.ts +88 -3
- package/src/device-bridge/collectors/location.ts +21 -9
- package/src/device-state/collectors.ts +5 -1
- package/src/device-state/domains/deviceContext.ts +5 -3
- package/src/device-state/domains/interaction.ts +2 -1
- package/src/device-state/domains/spatial.ts +200 -116
- package/src/device-state/domains/visual.ts +34 -13
- package/src/device-state/events.ts +49 -0
- package/src/device-state/headingSource.ts +43 -8
- package/src/device-state/index.ts +3 -2
- package/src/device-state/replay.ts +76 -11
- package/src/device-state/spatialEvents.ts +300 -0
- package/src/geo.ts +23 -0
- package/src/maps/index.ts +81 -0
- package/src/maps/normalize.ts +225 -0
- package/src/vision/index.ts +4 -2
- package/dist/chunk-4XYXJYN5.mjs.map +0 -1
- package/dist/chunk-5HM6UA2K.mjs.map +0 -1
- package/dist/chunk-TZXWVYYR.mjs.map +0 -1
- package/dist/chunk-VQJPSLHP.mjs.map +0 -1
package/dist/bootstrap.js
CHANGED
|
@@ -94,7 +94,7 @@ var init_cookies = __esm({
|
|
|
94
94
|
var nfc_exports = {};
|
|
95
95
|
__export(nfc_exports, {
|
|
96
96
|
isEnabled: () => isEnabled,
|
|
97
|
-
isSupported: () =>
|
|
97
|
+
isSupported: () => isSupported3,
|
|
98
98
|
readOnce: () => readOnce
|
|
99
99
|
});
|
|
100
100
|
async function ensureInit() {
|
|
@@ -109,7 +109,7 @@ async function ensureInit() {
|
|
|
109
109
|
return false;
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
-
async function
|
|
112
|
+
async function isSupported3() {
|
|
113
113
|
try {
|
|
114
114
|
return await import_react_native_nfc_manager.default.isSupported();
|
|
115
115
|
} catch {
|
|
@@ -384,6 +384,30 @@ function unsupported(what) {
|
|
|
384
384
|
return err;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
// src/device-state/events.ts
|
|
388
|
+
function stateEvent(domain, name, payload, timestamp = Date.now()) {
|
|
389
|
+
return payload === void 0 ? { domain, name, timestamp } : { domain, name, timestamp, payload };
|
|
390
|
+
}
|
|
391
|
+
function listenerSet() {
|
|
392
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
393
|
+
return {
|
|
394
|
+
add(fn) {
|
|
395
|
+
listeners.add(fn);
|
|
396
|
+
return { unsubscribe: () => {
|
|
397
|
+
listeners.delete(fn);
|
|
398
|
+
} };
|
|
399
|
+
},
|
|
400
|
+
emit(event) {
|
|
401
|
+
for (const fn of listeners) {
|
|
402
|
+
try {
|
|
403
|
+
fn(event);
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
387
411
|
// src/device-state/domains/deviceContext.ts
|
|
388
412
|
function createDeviceContextDomain() {
|
|
389
413
|
return {
|
|
@@ -419,11 +443,12 @@ function createDeviceContextDomain() {
|
|
|
419
443
|
},
|
|
420
444
|
// Real events, not a placeholder: expo-battery exposes native listeners,
|
|
421
445
|
// so a plugin that subscribes genuinely hears about a change rather than
|
|
422
|
-
// sitting on a subscription that can never fire.
|
|
446
|
+
// sitting on a subscription that can never fire. Nudges without a payload:
|
|
447
|
+
// no Device Context payload schema is decided, so the subscriber re-reads.
|
|
423
448
|
subscribe(listener) {
|
|
424
449
|
const subs = [
|
|
425
|
-
ExpoBattery.addBatteryLevelListener(() => listener(
|
|
426
|
-
ExpoBattery.addBatteryStateListener(() => listener(
|
|
450
|
+
ExpoBattery.addBatteryLevelListener(() => listener(stateEvent("deviceContext", "batteryLevelChanged"))),
|
|
451
|
+
ExpoBattery.addBatteryStateListener(() => listener(stateEvent("deviceContext", "chargingChanged")))
|
|
427
452
|
];
|
|
428
453
|
return { unsubscribe: () => subs.forEach((x) => x?.remove?.()) };
|
|
429
454
|
}
|
|
@@ -559,13 +584,523 @@ function createInteractionDomain() {
|
|
|
559
584
|
// which is the one part of this domain that works.
|
|
560
585
|
subscribe(listener) {
|
|
561
586
|
const sub2 = import_react_native.AppState.addEventListener("change", (next) => {
|
|
562
|
-
listener(
|
|
587
|
+
listener(stateEvent("interaction", "appStateChanged", { appState: mapState(next) }));
|
|
563
588
|
});
|
|
564
589
|
return { unsubscribe: () => sub2.remove() };
|
|
565
590
|
}
|
|
566
591
|
};
|
|
567
592
|
}
|
|
568
593
|
|
|
594
|
+
// src/device-state/headingSource.ts
|
|
595
|
+
var ExpoLocation = __toESM(require("expo-location"));
|
|
596
|
+
|
|
597
|
+
// src/device-state/turnDetector.ts
|
|
598
|
+
function signedDelta(a, b) {
|
|
599
|
+
return (b - a + 540) % 360 - 180;
|
|
600
|
+
}
|
|
601
|
+
var TurnDetector = class {
|
|
602
|
+
constructor(opts = {}) {
|
|
603
|
+
/** The heading currently treated as "straight ahead". */
|
|
604
|
+
this.refDeg = null;
|
|
605
|
+
this.stableSince = 0;
|
|
606
|
+
this.lastTurn = null;
|
|
607
|
+
this.minTurnDeg = opts.minTurnDeg ?? 35;
|
|
608
|
+
this.stableDeg = opts.stableDeg ?? 12;
|
|
609
|
+
this.stableMs = opts.stableMs ?? 2500;
|
|
610
|
+
}
|
|
611
|
+
/** Feed one compass reading. `null` degrees means the compass said nothing. */
|
|
612
|
+
push(deg, now) {
|
|
613
|
+
if (deg === null) return null;
|
|
614
|
+
if (this.refDeg === null) {
|
|
615
|
+
this.refDeg = deg;
|
|
616
|
+
this.stableSince = now;
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
const delta = signedDelta(this.refDeg, deg);
|
|
620
|
+
if (Math.abs(delta) >= this.minTurnDeg) {
|
|
621
|
+
this.lastTurn = { deg: Math.round(delta), timestamp: now };
|
|
622
|
+
this.refDeg = deg;
|
|
623
|
+
this.stableSince = now;
|
|
624
|
+
return this.lastTurn;
|
|
625
|
+
}
|
|
626
|
+
if (Math.abs(delta) > this.stableDeg) {
|
|
627
|
+
this.stableSince = now;
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
if (now - this.stableSince >= this.stableMs) {
|
|
631
|
+
this.refDeg = deg;
|
|
632
|
+
this.stableSince = now;
|
|
633
|
+
}
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* The most recent completed turn, or null if none this session.
|
|
638
|
+
*
|
|
639
|
+
* It does NOT expire on its own: the age is derivable from `timestamp`, and
|
|
640
|
+
* "recent enough to mention" is a product judgement this layer must not make
|
|
641
|
+
* on the caller's behalf.
|
|
642
|
+
*/
|
|
643
|
+
last() {
|
|
644
|
+
return this.lastTurn;
|
|
645
|
+
}
|
|
646
|
+
reset() {
|
|
647
|
+
this.refDeg = null;
|
|
648
|
+
this.stableSince = 0;
|
|
649
|
+
this.lastTurn = null;
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
// src/device-state/headingSource.ts
|
|
654
|
+
var latest = null;
|
|
655
|
+
var sub = null;
|
|
656
|
+
var opening = null;
|
|
657
|
+
var owners = /* @__PURE__ */ new Set();
|
|
658
|
+
var turns = new TurnDetector();
|
|
659
|
+
var turnListeners = listenerSet();
|
|
660
|
+
function startHeading(owner) {
|
|
661
|
+
owners.add(owner);
|
|
662
|
+
if (sub) return Promise.resolve();
|
|
663
|
+
opening ?? (opening = open().finally(() => {
|
|
664
|
+
opening = null;
|
|
665
|
+
}));
|
|
666
|
+
return opening;
|
|
667
|
+
}
|
|
668
|
+
async function open() {
|
|
669
|
+
try {
|
|
670
|
+
const s = await ExpoLocation.watchHeadingAsync((h) => {
|
|
671
|
+
if (!owners.size) return;
|
|
672
|
+
const raw = h.trueHeading >= 0 ? h.trueHeading : h.magHeading;
|
|
673
|
+
const deg = typeof raw === "number" && raw >= 0 ? raw : null;
|
|
674
|
+
const cal = typeof h.accuracy === "number" && h.accuracy >= 0 ? h.accuracy : null;
|
|
675
|
+
const now = Date.now();
|
|
676
|
+
latest = { headingDeg: deg, calibration: cal, timestamp: now };
|
|
677
|
+
const turn = turns.push(deg, now);
|
|
678
|
+
if (turn) turnListeners.emit(turn);
|
|
679
|
+
});
|
|
680
|
+
if (!owners.size) {
|
|
681
|
+
s.remove();
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
sub = s;
|
|
685
|
+
} catch {
|
|
686
|
+
sub = null;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
function stopHeading(owner) {
|
|
690
|
+
owners.delete(owner);
|
|
691
|
+
if (owners.size) return;
|
|
692
|
+
sub?.remove();
|
|
693
|
+
sub = null;
|
|
694
|
+
latest = null;
|
|
695
|
+
turns.reset();
|
|
696
|
+
}
|
|
697
|
+
function onTurn(fn) {
|
|
698
|
+
return turnListeners.add(fn);
|
|
699
|
+
}
|
|
700
|
+
function currentHeading() {
|
|
701
|
+
return latest ? { headingDeg: latest.headingDeg } : null;
|
|
702
|
+
}
|
|
703
|
+
function currentHeadingDetail() {
|
|
704
|
+
return latest;
|
|
705
|
+
}
|
|
706
|
+
function lastMeaningfulTurn() {
|
|
707
|
+
return turns.last();
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/geo.ts
|
|
711
|
+
function distanceOf(path) {
|
|
712
|
+
const R = 6371e3;
|
|
713
|
+
let total = 0;
|
|
714
|
+
for (let i = 1; i < path.length; i++) {
|
|
715
|
+
const a = path[i - 1], b = path[i];
|
|
716
|
+
const dLat = (b.latitude - a.latitude) * Math.PI / 180;
|
|
717
|
+
const dLon = (b.longitude - a.longitude) * Math.PI / 180;
|
|
718
|
+
const la1 = a.latitude * Math.PI / 180, la2 = b.latitude * Math.PI / 180;
|
|
719
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(la1) * Math.cos(la2) * Math.sin(dLon / 2) ** 2;
|
|
720
|
+
total += 2 * R * Math.asin(Math.sqrt(h));
|
|
721
|
+
}
|
|
722
|
+
return Math.round(total);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/device-state/spatialEvents.ts
|
|
726
|
+
var STATIONARY_MPS = 0.4;
|
|
727
|
+
function movementStateOf(speedMps) {
|
|
728
|
+
return speedMps === null ? "unknown" : speedMps < STATIONARY_MPS ? "stationary" : "moving";
|
|
729
|
+
}
|
|
730
|
+
function fixConfidence(accuracyM) {
|
|
731
|
+
return accuracyM === null ? "unknown" : accuracyM <= 10 ? "high" : accuracyM <= 50 ? "medium" : "low";
|
|
732
|
+
}
|
|
733
|
+
var SPATIAL_EVENT_PROFILES = {
|
|
734
|
+
navigation: { movingHoldMs: 3e3, stoppedHoldMs: 5e3, confidenceHoldMs: 3e3, minMoveM: 10 },
|
|
735
|
+
fitness: { movingHoldMs: 3e3, stoppedHoldMs: 5e3, confidenceHoldMs: 5e3, minMoveM: 10 },
|
|
736
|
+
preciseLocate: { movingHoldMs: 2e3, stoppedHoldMs: 3e3, confidenceHoldMs: 2e3, minMoveM: 3 },
|
|
737
|
+
geofence: { movingHoldMs: 1e4, stoppedHoldMs: 3e4, confidenceHoldMs: 1e4, minMoveM: 25 },
|
|
738
|
+
lowPowerTracking: { movingHoldMs: 1e4, stoppedHoldMs: 3e4, confidenceHoldMs: 1e4, minMoveM: 50 }
|
|
739
|
+
};
|
|
740
|
+
function moveThresholdM(anchor, fix) {
|
|
741
|
+
const a = anchor.accuracyM ?? 0, b = fix.accuracyM ?? 0;
|
|
742
|
+
return 2 * Math.sqrt(a * a + b * b);
|
|
743
|
+
}
|
|
744
|
+
function spatialEventProfile(intent) {
|
|
745
|
+
return SPATIAL_EVENT_PROFILES[intent] ?? SPATIAL_EVENT_PROFILES.navigation;
|
|
746
|
+
}
|
|
747
|
+
var RANK = { high: 3, medium: 2, low: 1 };
|
|
748
|
+
var MIN_FIXES = 2;
|
|
749
|
+
function extend(run, state, fixAt2, now) {
|
|
750
|
+
if (!run || run.state !== state) return { state, since: now, fixAt: fixAt2, fixes: fixAt2 === null ? 0 : 1 };
|
|
751
|
+
if (fixAt2 !== null && fixAt2 !== run.fixAt) {
|
|
752
|
+
run.fixAt = fixAt2;
|
|
753
|
+
run.fixes++;
|
|
754
|
+
}
|
|
755
|
+
return run;
|
|
756
|
+
}
|
|
757
|
+
var settled = (run, now, holdMs) => run.fixes >= MIN_FIXES && now - run.since >= holdMs;
|
|
758
|
+
var fixAt = (s) => s.position?.timestamp ?? null;
|
|
759
|
+
var SpatialEventDetector = class {
|
|
760
|
+
constructor(profile = SPATIAL_EVENT_PROFILES.navigation) {
|
|
761
|
+
/** The movement state last settled on. Only MEASURED states settle. */
|
|
762
|
+
this.movement = "unknown";
|
|
763
|
+
this.run = null;
|
|
764
|
+
/** The completed turn already on record — reported turns, and the one the
|
|
765
|
+
* session opened onto. Only a turn with a new timestamp is news. */
|
|
766
|
+
this.lastTurnAt = null;
|
|
767
|
+
/** The GPS band last settled on; null until the session's first band. */
|
|
768
|
+
this.band = null;
|
|
769
|
+
this.bandRun = null;
|
|
770
|
+
/** The last position REPORTED — distance is measured from here, not from
|
|
771
|
+
* the previous fix, so a slow walk still adds up to a change. */
|
|
772
|
+
this.anchor = null;
|
|
773
|
+
this.staleArmed = true;
|
|
774
|
+
this.profile = profile;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Forget everything — a new session, or replay seeking backwards. Emits
|
|
778
|
+
* nothing. `turnOnRecordAt` is the timestamp of the turn already completed
|
|
779
|
+
* when the session opens (a visual session shares the compass): history, not
|
|
780
|
+
* news. Seeded from the turn itself rather than compared against a clock, so
|
|
781
|
+
* it holds whichever clock the turn was stamped with — epoch live, the
|
|
782
|
+
* recording's epoch in replay, where `now` is virtual.
|
|
783
|
+
*/
|
|
784
|
+
reset(profile, turnOnRecordAt = null) {
|
|
785
|
+
if (profile) this.profile = profile;
|
|
786
|
+
this.movement = "unknown";
|
|
787
|
+
this.run = null;
|
|
788
|
+
this.lastTurnAt = turnOnRecordAt;
|
|
789
|
+
this.band = null;
|
|
790
|
+
this.bandRun = null;
|
|
791
|
+
this.anchor = null;
|
|
792
|
+
this.staleArmed = true;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Feed the newest snapshot. Returns what changed since the last one, in this
|
|
796
|
+
* order: movement, turn, position, confidence, staleness. Fields are read
|
|
797
|
+
* defensively — replay hands over recorded snapshots verbatim, and a recording
|
|
798
|
+
* may lack a block the live engine always fills.
|
|
799
|
+
*/
|
|
800
|
+
push(snapshot, now) {
|
|
801
|
+
const out = [];
|
|
802
|
+
const emit = (name, payload) => {
|
|
803
|
+
out.push(stateEvent("spatial", name, payload, now));
|
|
804
|
+
};
|
|
805
|
+
this.movementFrom(snapshot, now, emit);
|
|
806
|
+
this.turnFrom(snapshot, emit);
|
|
807
|
+
this.positionFrom(snapshot, emit);
|
|
808
|
+
this.confidenceFrom(snapshot, now, emit);
|
|
809
|
+
this.staleFrom(snapshot, emit);
|
|
810
|
+
return out;
|
|
811
|
+
}
|
|
812
|
+
movementFrom(s, now, emit) {
|
|
813
|
+
const state = s.movement?.state ?? "unknown";
|
|
814
|
+
this.run = extend(this.run, state, fixAt(s), now);
|
|
815
|
+
if (state === this.movement) return;
|
|
816
|
+
if (state === "moving" && settled(this.run, now, this.profile.movingHoldMs)) {
|
|
817
|
+
emit("movementStarted", {
|
|
818
|
+
previous: this.movement === "stationary" ? "stationary" : "unknown",
|
|
819
|
+
speedMps: s.movement?.speedMps ?? null,
|
|
820
|
+
bearingDeg: s.movement?.bearingDeg ?? null,
|
|
821
|
+
since: this.run.since
|
|
822
|
+
});
|
|
823
|
+
this.movement = "moving";
|
|
824
|
+
} else if (state === "stationary" && settled(this.run, now, this.profile.stoppedHoldMs)) {
|
|
825
|
+
if (this.movement === "moving") emit("movementStopped", { previous: "moving", since: this.run.since });
|
|
826
|
+
this.movement = "stationary";
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
turnFrom(s, emit) {
|
|
830
|
+
const at = s.changes?.timestamp ?? null;
|
|
831
|
+
const deg = s.changes?.lastMeaningfulTurnDeg ?? null;
|
|
832
|
+
if (at === null || deg === null || at === this.lastTurnAt) return;
|
|
833
|
+
this.lastTurnAt = at;
|
|
834
|
+
emit("lastMeaningfulTurnChanged", { lastMeaningfulTurnDeg: deg, timestamp: at });
|
|
835
|
+
}
|
|
836
|
+
positionFrom(s, emit) {
|
|
837
|
+
const p = s.position ?? null;
|
|
838
|
+
if (!p) return;
|
|
839
|
+
if (!this.anchor) {
|
|
840
|
+
emit("positionChanged", { position: { ...p }, previous: null, distanceM: null });
|
|
841
|
+
this.anchor = p;
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const d = distanceOf([this.anchor, p]);
|
|
845
|
+
if (d < Math.max(this.profile.minMoveM, moveThresholdM(this.anchor, p))) {
|
|
846
|
+
const aNew = p.accuracyM ?? null, aOld = this.anchor.accuracyM ?? null;
|
|
847
|
+
if (aNew != null && aOld != null && aNew * 2 < aOld) this.anchor = p;
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
emit("positionChanged", { position: { ...p }, previous: { ...this.anchor }, distanceM: d });
|
|
851
|
+
this.anchor = p;
|
|
852
|
+
}
|
|
853
|
+
confidenceFrom(s, now, emit) {
|
|
854
|
+
const p = s.position ?? null;
|
|
855
|
+
if (!p) return;
|
|
856
|
+
const level = fixConfidence(p.accuracyM ?? null);
|
|
857
|
+
if (level === "unknown") return;
|
|
858
|
+
if (this.band === null) {
|
|
859
|
+
this.band = level;
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (level === this.band) {
|
|
863
|
+
this.bandRun = null;
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
this.bandRun = extend(this.bandRun, level, fixAt(s), now);
|
|
867
|
+
if (!settled(this.bandRun, now, this.profile.confidenceHoldMs)) return;
|
|
868
|
+
emit("positionConfidenceChanged", {
|
|
869
|
+
level,
|
|
870
|
+
previous: this.band,
|
|
871
|
+
accuracyM: p.accuracyM ?? null,
|
|
872
|
+
direction: RANK[level] < RANK[this.band] ? "dropped" : "recovered"
|
|
873
|
+
});
|
|
874
|
+
this.band = level;
|
|
875
|
+
this.bandRun = null;
|
|
876
|
+
}
|
|
877
|
+
staleFrom(s, emit) {
|
|
878
|
+
const f = s.quality?.freshness;
|
|
879
|
+
if (!f) return;
|
|
880
|
+
if (f.state === "fresh") {
|
|
881
|
+
this.staleArmed = true;
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (f.state !== "stale" || !this.staleArmed) return;
|
|
885
|
+
emit("spatialStateStale", { observedAt: f.observedAt, ageMs: f.ageMs });
|
|
886
|
+
this.staleArmed = false;
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
// src/device-state/domains/spatial.ts
|
|
891
|
+
var WATCH_ACCURACY = {
|
|
892
|
+
navigation: "navigation",
|
|
893
|
+
preciseLocate: "high",
|
|
894
|
+
fitness: "high",
|
|
895
|
+
geofence: "balanced",
|
|
896
|
+
lowPowerTracking: "low"
|
|
897
|
+
};
|
|
898
|
+
function createSpatialDomain(collectors, speedRef) {
|
|
899
|
+
const RECENT_PATH_MAX = 60;
|
|
900
|
+
let recentPath = [];
|
|
901
|
+
let latest2 = null;
|
|
902
|
+
let latestSpeed = null;
|
|
903
|
+
let latestCourse = null;
|
|
904
|
+
let stopWatch = null;
|
|
905
|
+
let session = null;
|
|
906
|
+
const detector = new SpatialEventDetector();
|
|
907
|
+
const listeners = listenerSet();
|
|
908
|
+
let ticker = null;
|
|
909
|
+
let turnSub = null;
|
|
910
|
+
const nonNegative2 = (v) => typeof v === "number" && v >= 0 ? v : null;
|
|
911
|
+
const derivedSpeed = () => {
|
|
912
|
+
if (recentPath.length < 2) return null;
|
|
913
|
+
const b = recentPath[recentPath.length - 1];
|
|
914
|
+
const a = recentPath[recentPath.length - 2];
|
|
915
|
+
const dtS = (b.timestamp - a.timestamp) / 1e3;
|
|
916
|
+
if (dtS <= 0 || dtS > 10) return null;
|
|
917
|
+
return distanceOf([a, b]) / dtS;
|
|
918
|
+
};
|
|
919
|
+
const derivedCourse = () => {
|
|
920
|
+
if (recentPath.length < 2) return null;
|
|
921
|
+
const b = recentPath[recentPath.length - 1];
|
|
922
|
+
const a = recentPath[recentPath.length - 2];
|
|
923
|
+
if (b.timestamp - a.timestamp > 1e4) return null;
|
|
924
|
+
if (distanceOf([a, b]) < 3) return null;
|
|
925
|
+
return bearingBetween(a, b);
|
|
926
|
+
};
|
|
927
|
+
const snapshotFrom = (point2, measuredSpeed, measuredCourse) => {
|
|
928
|
+
let speedMps = measuredSpeed;
|
|
929
|
+
let courseDeg = measuredCourse;
|
|
930
|
+
let derived = false;
|
|
931
|
+
if (speedMps === null) {
|
|
932
|
+
const d = derivedSpeed();
|
|
933
|
+
if (d !== null) {
|
|
934
|
+
speedMps = d;
|
|
935
|
+
derived = true;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (courseDeg === null) {
|
|
939
|
+
const d = derivedCourse();
|
|
940
|
+
if (d !== null) {
|
|
941
|
+
courseDeg = d;
|
|
942
|
+
derived = true;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
const heading = currentHeadingDetail();
|
|
946
|
+
const turn = lastMeaningfulTurn();
|
|
947
|
+
return {
|
|
948
|
+
position: point2,
|
|
949
|
+
movement: {
|
|
950
|
+
speedMps,
|
|
951
|
+
bearingDeg: courseDeg,
|
|
952
|
+
// Needs two points from the SAME session; without a stream there is
|
|
953
|
+
// nothing to measure against, and a distance of 0 would read as
|
|
954
|
+
// "the user has not moved".
|
|
955
|
+
distanceRecentM: recentPath.length >= 2 ? distanceOf(recentPath) : null,
|
|
956
|
+
state: movementStateOf(speedMps)
|
|
957
|
+
},
|
|
958
|
+
orientation: {
|
|
959
|
+
headingDeg: heading?.headingDeg ?? null,
|
|
960
|
+
// NOT the calibration rank. iOS reports 0–3 (none…high), which is not
|
|
961
|
+
// a number of degrees, and converting one to the other would be an
|
|
962
|
+
// invented error bar. The rank drives `confidence` below instead.
|
|
963
|
+
headingAccuracyDeg: null
|
|
964
|
+
},
|
|
965
|
+
changes: {
|
|
966
|
+
lastMeaningfulTurnDeg: turn ? turn.deg : null,
|
|
967
|
+
timestamp: turn ? turn.timestamp : null
|
|
968
|
+
},
|
|
969
|
+
recentPath: [...recentPath],
|
|
970
|
+
quality: qualityFrom(
|
|
971
|
+
point2.timestamp,
|
|
972
|
+
spatialConfidence(point2.accuracyM ?? null, heading?.calibration ?? null, derived)
|
|
973
|
+
)
|
|
974
|
+
};
|
|
975
|
+
};
|
|
976
|
+
const noFix = () => ({
|
|
977
|
+
position: null,
|
|
978
|
+
movement: { speedMps: null, bearingDeg: null, distanceRecentM: null, state: "unknown" },
|
|
979
|
+
orientation: { headingDeg: currentHeadingDetail()?.headingDeg ?? null },
|
|
980
|
+
recentPath: [],
|
|
981
|
+
quality: qualityUnknown()
|
|
982
|
+
});
|
|
983
|
+
const warmSnapshot = () => latest2 ? snapshotFrom(latest2, latestSpeed, latestCourse) : null;
|
|
984
|
+
const observe = () => {
|
|
985
|
+
const snapshot = warmSnapshot();
|
|
986
|
+
if (!snapshot) return;
|
|
987
|
+
for (const event of detector.push(snapshot, Date.now())) listeners.emit(event);
|
|
988
|
+
};
|
|
989
|
+
const startTicking = () => {
|
|
990
|
+
ticker = setInterval(observe, 1e3);
|
|
991
|
+
};
|
|
992
|
+
const stopTicking = () => {
|
|
993
|
+
if (ticker) clearInterval(ticker);
|
|
994
|
+
ticker = null;
|
|
995
|
+
};
|
|
996
|
+
return {
|
|
997
|
+
async getSnapshot() {
|
|
998
|
+
const warm = warmSnapshot();
|
|
999
|
+
if (warm) return warm;
|
|
1000
|
+
if (session) return noFix();
|
|
1001
|
+
let point2;
|
|
1002
|
+
let speedMps;
|
|
1003
|
+
let courseDeg;
|
|
1004
|
+
try {
|
|
1005
|
+
if (!collectors.location) throw unsupported("location collector");
|
|
1006
|
+
const fix = await collectors.location.getPosition({ accuracy: "low" });
|
|
1007
|
+
point2 = {
|
|
1008
|
+
latitude: fix.latitude,
|
|
1009
|
+
longitude: fix.longitude,
|
|
1010
|
+
altitude: fix.altitude ?? null,
|
|
1011
|
+
accuracyM: nonNegative2(fix.accuracy),
|
|
1012
|
+
timestamp: fix.timestamp ?? Date.now()
|
|
1013
|
+
};
|
|
1014
|
+
speedMps = nonNegative2(fix.speed);
|
|
1015
|
+
courseDeg = nonNegative2(fix.course);
|
|
1016
|
+
} catch {
|
|
1017
|
+
return noFix();
|
|
1018
|
+
}
|
|
1019
|
+
return snapshotFrom(point2, speedMps, courseDeg);
|
|
1020
|
+
},
|
|
1021
|
+
/**
|
|
1022
|
+
* Open the warm window: position stream + compass + event feed.
|
|
1023
|
+
*
|
|
1024
|
+
* The intent selects how eager the EVENTS are (spatialEvents.ts profiles)
|
|
1025
|
+
* and the GPS TIER the stream runs at (WATCH_ACCURACY): navigation pays the
|
|
1026
|
+
* battery for BestForNavigation; low-power tracking does not.
|
|
1027
|
+
*
|
|
1028
|
+
* A second `start()` while one is open joins the running session and is
|
|
1029
|
+
* told ITS intent, not handed back the one it asked for: answering with the
|
|
1030
|
+
* caller's intent would claim a profile that is not in force.
|
|
1031
|
+
*/
|
|
1032
|
+
async start(options) {
|
|
1033
|
+
if (session) return { ...session };
|
|
1034
|
+
if (!collectors.location) throw unsupported("location collector");
|
|
1035
|
+
const startedAt = Date.now();
|
|
1036
|
+
session = { id: `spatial_${startedAt}`, intent: options.intent, startedAt };
|
|
1037
|
+
detector.reset(spatialEventProfile(options.intent), lastMeaningfulTurn()?.timestamp ?? null);
|
|
1038
|
+
void startHeading("spatial");
|
|
1039
|
+
turnSub = onTurn(observe);
|
|
1040
|
+
stopWatch = collectors.location.watchPosition((pos) => {
|
|
1041
|
+
const p = {
|
|
1042
|
+
latitude: pos.latitude,
|
|
1043
|
+
longitude: pos.longitude,
|
|
1044
|
+
altitude: pos.altitude ?? null,
|
|
1045
|
+
accuracyM: nonNegative2(pos.accuracy),
|
|
1046
|
+
timestamp: pos.timestamp ?? Date.now()
|
|
1047
|
+
};
|
|
1048
|
+
latest2 = p;
|
|
1049
|
+
latestSpeed = nonNegative2(pos.speed);
|
|
1050
|
+
latestCourse = nonNegative2(pos.course);
|
|
1051
|
+
speedRef.mps = latestSpeed;
|
|
1052
|
+
speedRef.at = p.timestamp;
|
|
1053
|
+
recentPath = [...recentPath, p].slice(-RECENT_PATH_MAX);
|
|
1054
|
+
observe();
|
|
1055
|
+
}, { accuracy: WATCH_ACCURACY[options.intent] ?? "balanced" });
|
|
1056
|
+
startTicking();
|
|
1057
|
+
return { ...session };
|
|
1058
|
+
},
|
|
1059
|
+
async stop() {
|
|
1060
|
+
stopWatch?.();
|
|
1061
|
+
stopWatch = null;
|
|
1062
|
+
session = null;
|
|
1063
|
+
stopTicking();
|
|
1064
|
+
turnSub?.unsubscribe();
|
|
1065
|
+
turnSub = null;
|
|
1066
|
+
detector.reset();
|
|
1067
|
+
recentPath = [];
|
|
1068
|
+
latest2 = null;
|
|
1069
|
+
latestSpeed = null;
|
|
1070
|
+
latestCourse = null;
|
|
1071
|
+
speedRef.mps = null;
|
|
1072
|
+
speedRef.at = 0;
|
|
1073
|
+
stopHeading("spatial");
|
|
1074
|
+
},
|
|
1075
|
+
/**
|
|
1076
|
+
* The six spatial events (see `SpatialEvent`). Subscribing opens no sensor:
|
|
1077
|
+
* events flow only while a session is open, and a subscription made before
|
|
1078
|
+
* `start()` hears that session from its first fix.
|
|
1079
|
+
*/
|
|
1080
|
+
subscribe(listener) {
|
|
1081
|
+
return listeners.add(listener);
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
function spatialConfidence(accuracyM, calibration, derived) {
|
|
1086
|
+
const rank = (v) => v === "high" ? 3 : v === "medium" ? 2 : v === "low" ? 1 : 0;
|
|
1087
|
+
const fromFix = fixConfidence(accuracyM);
|
|
1088
|
+
const fromCompass = calibration === null ? "unknown" : calibration >= 3 ? "high" : calibration >= 2 ? "medium" : "low";
|
|
1089
|
+
const parts = [fromFix, fromCompass, ...derived ? ["low"] : []];
|
|
1090
|
+
const known = parts.filter((p) => p !== "unknown");
|
|
1091
|
+
if (!known.length) return { level: "unknown" };
|
|
1092
|
+
const worst = known.reduce((a, b) => rank(a) <= rank(b) ? a : b);
|
|
1093
|
+
return { level: worst };
|
|
1094
|
+
}
|
|
1095
|
+
function bearingBetween(a, b) {
|
|
1096
|
+
const toRad = (d) => d * Math.PI / 180;
|
|
1097
|
+
const la1 = toRad(a.latitude), la2 = toRad(b.latitude);
|
|
1098
|
+
const dLon = toRad(b.longitude - a.longitude);
|
|
1099
|
+
const y = Math.sin(dLon) * Math.cos(la2);
|
|
1100
|
+
const x = Math.cos(la1) * Math.sin(la2) - Math.sin(la1) * Math.cos(la2) * Math.cos(dLon);
|
|
1101
|
+
return (Math.atan2(y, x) * 180 / Math.PI + 360) % 360;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
569
1104
|
// src/device-state/visualEngine.ts
|
|
570
1105
|
var PROFILES = {
|
|
571
1106
|
navigation: { intervalMs: 1e3, windowMs: 5e3 },
|
|
@@ -779,300 +1314,26 @@ function frameQuality(frame) {
|
|
|
779
1314
|
};
|
|
780
1315
|
}
|
|
781
1316
|
|
|
782
|
-
// src/device-state/
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
// src/device-state/turnDetector.ts
|
|
786
|
-
function signedDelta(a, b) {
|
|
787
|
-
return (b - a + 540) % 360 - 180;
|
|
788
|
-
}
|
|
789
|
-
var TurnDetector = class {
|
|
790
|
-
constructor(opts = {}) {
|
|
791
|
-
/** The heading currently treated as "straight ahead". */
|
|
792
|
-
this.refDeg = null;
|
|
793
|
-
this.stableSince = 0;
|
|
794
|
-
this.lastTurn = null;
|
|
795
|
-
this.minTurnDeg = opts.minTurnDeg ?? 35;
|
|
796
|
-
this.stableDeg = opts.stableDeg ?? 12;
|
|
797
|
-
this.stableMs = opts.stableMs ?? 2500;
|
|
798
|
-
}
|
|
799
|
-
/** Feed one compass reading. `null` degrees means the compass said nothing. */
|
|
800
|
-
push(deg, now) {
|
|
801
|
-
if (deg === null) return null;
|
|
802
|
-
if (this.refDeg === null) {
|
|
803
|
-
this.refDeg = deg;
|
|
804
|
-
this.stableSince = now;
|
|
805
|
-
return null;
|
|
806
|
-
}
|
|
807
|
-
const delta = signedDelta(this.refDeg, deg);
|
|
808
|
-
if (Math.abs(delta) >= this.minTurnDeg) {
|
|
809
|
-
this.lastTurn = { deg: Math.round(delta), timestamp: now };
|
|
810
|
-
this.refDeg = deg;
|
|
811
|
-
this.stableSince = now;
|
|
812
|
-
return this.lastTurn;
|
|
813
|
-
}
|
|
814
|
-
if (Math.abs(delta) > this.stableDeg) {
|
|
815
|
-
this.stableSince = now;
|
|
816
|
-
return null;
|
|
817
|
-
}
|
|
818
|
-
if (now - this.stableSince >= this.stableMs) {
|
|
819
|
-
this.refDeg = deg;
|
|
820
|
-
this.stableSince = now;
|
|
821
|
-
}
|
|
822
|
-
return null;
|
|
823
|
-
}
|
|
824
|
-
/**
|
|
825
|
-
* The most recent completed turn, or null if none this session.
|
|
826
|
-
*
|
|
827
|
-
* It does NOT expire on its own: the age is derivable from `timestamp`, and
|
|
828
|
-
* "recent enough to mention" is a product judgement this layer must not make
|
|
829
|
-
* on the caller's behalf.
|
|
830
|
-
*/
|
|
831
|
-
last() {
|
|
832
|
-
return this.lastTurn;
|
|
833
|
-
}
|
|
834
|
-
reset() {
|
|
835
|
-
this.refDeg = null;
|
|
836
|
-
this.stableSince = 0;
|
|
837
|
-
this.lastTurn = null;
|
|
838
|
-
}
|
|
839
|
-
};
|
|
840
|
-
|
|
841
|
-
// src/device-state/headingSource.ts
|
|
842
|
-
var latest = null;
|
|
843
|
-
var sub = null;
|
|
844
|
-
var turns = new TurnDetector();
|
|
845
|
-
async function startHeading() {
|
|
846
|
-
if (sub) return;
|
|
847
|
-
try {
|
|
848
|
-
sub = await ExpoLocation.watchHeadingAsync((h) => {
|
|
849
|
-
const raw = h.trueHeading >= 0 ? h.trueHeading : h.magHeading;
|
|
850
|
-
const deg = typeof raw === "number" && raw >= 0 ? raw : null;
|
|
851
|
-
const cal = typeof h.accuracy === "number" && h.accuracy >= 0 ? h.accuracy : null;
|
|
852
|
-
latest = { headingDeg: deg, calibration: cal, timestamp: Date.now() };
|
|
853
|
-
turns.push(deg, Date.now());
|
|
854
|
-
});
|
|
855
|
-
} catch {
|
|
856
|
-
sub = null;
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
function stopHeading() {
|
|
860
|
-
sub?.remove();
|
|
861
|
-
sub = null;
|
|
862
|
-
latest = null;
|
|
863
|
-
turns.reset();
|
|
864
|
-
}
|
|
865
|
-
function currentHeading() {
|
|
866
|
-
return latest ? { headingDeg: latest.headingDeg } : null;
|
|
867
|
-
}
|
|
868
|
-
function currentHeadingDetail() {
|
|
869
|
-
return latest;
|
|
870
|
-
}
|
|
871
|
-
function lastMeaningfulTurn() {
|
|
872
|
-
return turns.last();
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
// src/device-state/domains/spatial.ts
|
|
876
|
-
function createSpatialDomain(collectors, speedRef) {
|
|
877
|
-
const RECENT_PATH_MAX = 60;
|
|
878
|
-
const STATIONARY_MPS = 0.4;
|
|
879
|
-
let recentPath = [];
|
|
880
|
-
let latest2 = null;
|
|
881
|
-
let latestSpeed = null;
|
|
882
|
-
let latestCourse = null;
|
|
883
|
-
let stopWatch = null;
|
|
884
|
-
let sessionId = null;
|
|
885
|
-
const nonNegative2 = (v) => typeof v === "number" && v >= 0 ? v : null;
|
|
886
|
-
const derivedSpeed = () => {
|
|
887
|
-
if (recentPath.length < 2) return null;
|
|
888
|
-
const b = recentPath[recentPath.length - 1];
|
|
889
|
-
const a = recentPath[recentPath.length - 2];
|
|
890
|
-
const dtS = (b.timestamp - a.timestamp) / 1e3;
|
|
891
|
-
if (dtS <= 0 || dtS > 10) return null;
|
|
892
|
-
return distanceOf([a, b]) / dtS;
|
|
893
|
-
};
|
|
894
|
-
const derivedCourse = () => {
|
|
895
|
-
if (recentPath.length < 2) return null;
|
|
896
|
-
const b = recentPath[recentPath.length - 1];
|
|
897
|
-
const a = recentPath[recentPath.length - 2];
|
|
898
|
-
if (b.timestamp - a.timestamp > 1e4) return null;
|
|
899
|
-
if (distanceOf([a, b]) < 3) return null;
|
|
900
|
-
return bearingBetween(a, b);
|
|
901
|
-
};
|
|
1317
|
+
// src/device-state/domains/visual.ts
|
|
1318
|
+
function cameraState() {
|
|
1319
|
+
const session = visualEngine.getSession();
|
|
902
1320
|
return {
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
if (!collectors.location) throw unsupported("location collector");
|
|
911
|
-
const fix = await collectors.location.getPosition({ accuracy: "low" });
|
|
912
|
-
point = {
|
|
913
|
-
latitude: fix.latitude,
|
|
914
|
-
longitude: fix.longitude,
|
|
915
|
-
altitude: fix.altitude ?? null,
|
|
916
|
-
accuracyM: nonNegative2(fix.accuracy),
|
|
917
|
-
timestamp: fix.timestamp ?? Date.now()
|
|
918
|
-
};
|
|
919
|
-
speedMps = nonNegative2(fix.speed);
|
|
920
|
-
courseDeg = nonNegative2(fix.course);
|
|
921
|
-
} catch {
|
|
922
|
-
return {
|
|
923
|
-
position: null,
|
|
924
|
-
movement: { speedMps: null, bearingDeg: null, distanceRecentM: null, state: "unknown" },
|
|
925
|
-
orientation: { headingDeg: null },
|
|
926
|
-
recentPath: [],
|
|
927
|
-
quality: qualityUnknown()
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
if (speedMps === null) {
|
|
932
|
-
const d = derivedSpeed();
|
|
933
|
-
if (d !== null) {
|
|
934
|
-
speedMps = d;
|
|
935
|
-
derived = true;
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
if (courseDeg === null) {
|
|
939
|
-
const d = derivedCourse();
|
|
940
|
-
if (d !== null) {
|
|
941
|
-
courseDeg = d;
|
|
942
|
-
derived = true;
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
const heading = currentHeadingDetail();
|
|
946
|
-
const turn = lastMeaningfulTurn();
|
|
947
|
-
return {
|
|
948
|
-
position: point,
|
|
949
|
-
movement: {
|
|
950
|
-
speedMps,
|
|
951
|
-
bearingDeg: courseDeg,
|
|
952
|
-
// Needs two points from the SAME session; without a stream there is
|
|
953
|
-
// nothing to measure against, and a distance of 0 would read as
|
|
954
|
-
// "the user has not moved".
|
|
955
|
-
distanceRecentM: recentPath.length >= 2 ? distanceOf(recentPath) : null,
|
|
956
|
-
// Only claim stationary/moving when a speed was actually obtained.
|
|
957
|
-
// With no speed the truthful answer is `unknown`, not `stationary` —
|
|
958
|
-
// "you have stopped" is a false statement, not a neutral default.
|
|
959
|
-
state: speedMps === null ? "unknown" : speedMps < STATIONARY_MPS ? "stationary" : "moving"
|
|
960
|
-
},
|
|
961
|
-
orientation: {
|
|
962
|
-
headingDeg: heading?.headingDeg ?? null,
|
|
963
|
-
// NOT the calibration rank. iOS reports 0–3 (none…high), which is not
|
|
964
|
-
// a number of degrees, and converting one to the other would be an
|
|
965
|
-
// invented error bar. The rank drives `confidence` below instead.
|
|
966
|
-
headingAccuracyDeg: null
|
|
967
|
-
},
|
|
968
|
-
changes: {
|
|
969
|
-
lastMeaningfulTurnDeg: turn ? turn.deg : null,
|
|
970
|
-
timestamp: turn ? turn.timestamp : null
|
|
971
|
-
},
|
|
972
|
-
recentPath: [...recentPath],
|
|
973
|
-
quality: qualityFrom(
|
|
974
|
-
point.timestamp,
|
|
975
|
-
spatialConfidence(point.accuracyM ?? null, heading?.calibration ?? null, derived)
|
|
976
|
-
)
|
|
977
|
-
};
|
|
978
|
-
},
|
|
979
|
-
/**
|
|
980
|
-
* Open the warm window: position stream + compass.
|
|
981
|
-
*
|
|
982
|
-
* The intent is recorded but does NOT yet change sampling — no native
|
|
983
|
-
* strategy layer exists — so it is accepted rather than refused only
|
|
984
|
-
* because the streams it opens are real and useful.
|
|
985
|
-
*/
|
|
986
|
-
async start(options) {
|
|
987
|
-
if (stopWatch) return { id: sessionId, intent: options.intent, startedAt: Date.now() };
|
|
988
|
-
sessionId = `spatial_${Date.now()}`;
|
|
989
|
-
if (!collectors.location) throw unsupported("location collector");
|
|
990
|
-
void startHeading();
|
|
991
|
-
stopWatch = collectors.location.watchPosition((pos) => {
|
|
992
|
-
const p = {
|
|
993
|
-
latitude: pos.latitude,
|
|
994
|
-
longitude: pos.longitude,
|
|
995
|
-
altitude: pos.altitude ?? null,
|
|
996
|
-
accuracyM: nonNegative2(pos.accuracy),
|
|
997
|
-
timestamp: pos.timestamp ?? Date.now()
|
|
998
|
-
};
|
|
999
|
-
latest2 = p;
|
|
1000
|
-
latestSpeed = nonNegative2(pos.speed);
|
|
1001
|
-
latestCourse = nonNegative2(pos.course);
|
|
1002
|
-
speedRef.mps = latestSpeed;
|
|
1003
|
-
speedRef.at = p.timestamp;
|
|
1004
|
-
recentPath = [...recentPath, p].slice(-RECENT_PATH_MAX);
|
|
1005
|
-
});
|
|
1006
|
-
return { id: sessionId, intent: options.intent, startedAt: Date.now() };
|
|
1007
|
-
},
|
|
1008
|
-
async stop() {
|
|
1009
|
-
stopWatch?.();
|
|
1010
|
-
stopWatch = null;
|
|
1011
|
-
sessionId = null;
|
|
1012
|
-
recentPath = [];
|
|
1013
|
-
latest2 = null;
|
|
1014
|
-
latestSpeed = null;
|
|
1015
|
-
latestCourse = null;
|
|
1016
|
-
speedRef.mps = null;
|
|
1017
|
-
speedRef.at = 0;
|
|
1018
|
-
if (!visualEngine.getSession()) stopHeading();
|
|
1019
|
-
},
|
|
1020
|
-
// No event vocabulary is frozen for spatial, and inventing names here would
|
|
1021
|
-
// put them in plugins before the freeze can contradict them.
|
|
1022
|
-
subscribe() {
|
|
1023
|
-
throw unsupported("spatial.subscribe");
|
|
1024
|
-
}
|
|
1321
|
+
// `available` = the app could use a camera at all. `active` = one is
|
|
1322
|
+
// running RIGHT NOW. They differ exactly when it matters: a pocketed
|
|
1323
|
+
// phone is available but not active, and answering `true` to both
|
|
1324
|
+
// would let a guide speak about a scene nothing is watching.
|
|
1325
|
+
available: !!session,
|
|
1326
|
+
active: visualEngine.isCameraReady(),
|
|
1327
|
+
facing: session ? "back" : "unknown"
|
|
1025
1328
|
};
|
|
1026
1329
|
}
|
|
1027
|
-
function spatialConfidence(accuracyM, calibration, derived) {
|
|
1028
|
-
const rank = (v) => v === "high" ? 3 : v === "medium" ? 2 : v === "low" ? 1 : 0;
|
|
1029
|
-
const fromFix = accuracyM === null ? "unknown" : accuracyM <= 10 ? "high" : accuracyM <= 50 ? "medium" : "low";
|
|
1030
|
-
const fromCompass = calibration === null ? "unknown" : calibration >= 3 ? "high" : calibration >= 2 ? "medium" : "low";
|
|
1031
|
-
const parts = [fromFix, fromCompass, ...derived ? ["low"] : []];
|
|
1032
|
-
const known = parts.filter((p) => p !== "unknown");
|
|
1033
|
-
if (!known.length) return { level: "unknown" };
|
|
1034
|
-
const worst = known.reduce((a, b) => rank(a) <= rank(b) ? a : b);
|
|
1035
|
-
return { level: worst };
|
|
1036
|
-
}
|
|
1037
|
-
function bearingBetween(a, b) {
|
|
1038
|
-
const toRad = (d) => d * Math.PI / 180;
|
|
1039
|
-
const la1 = toRad(a.latitude), la2 = toRad(b.latitude);
|
|
1040
|
-
const dLon = toRad(b.longitude - a.longitude);
|
|
1041
|
-
const y = Math.sin(dLon) * Math.cos(la2);
|
|
1042
|
-
const x = Math.cos(la1) * Math.sin(la2) - Math.sin(la1) * Math.cos(la2) * Math.cos(dLon);
|
|
1043
|
-
return (Math.atan2(y, x) * 180 / Math.PI + 360) % 360;
|
|
1044
|
-
}
|
|
1045
|
-
function distanceOf(path) {
|
|
1046
|
-
const R = 6371e3;
|
|
1047
|
-
let total = 0;
|
|
1048
|
-
for (let i = 1; i < path.length; i++) {
|
|
1049
|
-
const a = path[i - 1], b = path[i];
|
|
1050
|
-
const dLat = (b.latitude - a.latitude) * Math.PI / 180;
|
|
1051
|
-
const dLon = (b.longitude - a.longitude) * Math.PI / 180;
|
|
1052
|
-
const la1 = a.latitude * Math.PI / 180, la2 = b.latitude * Math.PI / 180;
|
|
1053
|
-
const h = Math.sin(dLat / 2) ** 2 + Math.cos(la1) * Math.cos(la2) * Math.sin(dLon / 2) ** 2;
|
|
1054
|
-
total += 2 * R * Math.asin(Math.sqrt(h));
|
|
1055
|
-
}
|
|
1056
|
-
return Math.round(total);
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
// src/device-state/domains/visual.ts
|
|
1060
1330
|
function createVisualDomain() {
|
|
1061
1331
|
return {
|
|
1062
1332
|
async getSnapshot() {
|
|
1063
|
-
const session = visualEngine.getSession();
|
|
1064
1333
|
const latest2 = visualEngine.latest();
|
|
1065
1334
|
const timeline = visualEngine.timeline();
|
|
1066
1335
|
return {
|
|
1067
|
-
camera:
|
|
1068
|
-
// `available` = the app could use a camera at all. `active` = one is
|
|
1069
|
-
// running RIGHT NOW. They differ exactly when it matters: a pocketed
|
|
1070
|
-
// phone is available but not active, and answering `true` to both
|
|
1071
|
-
// would let a guide speak about a scene nothing is watching.
|
|
1072
|
-
available: !!session,
|
|
1073
|
-
active: visualEngine.isCameraReady(),
|
|
1074
|
-
facing: session ? "back" : "unknown"
|
|
1075
|
-
},
|
|
1336
|
+
camera: cameraState(),
|
|
1076
1337
|
// null, not a stale frame dressed as current. A caller with no usable
|
|
1077
1338
|
// frame must see nothing rather than something old.
|
|
1078
1339
|
latest: latest2,
|
|
@@ -1083,13 +1344,13 @@ function createVisualDomain() {
|
|
|
1083
1344
|
async start(options) {
|
|
1084
1345
|
const s = visualEngine.startSession(options?.intent || "navigation");
|
|
1085
1346
|
visualEngine.setContextProvider({ heading: currentHeading });
|
|
1086
|
-
void startHeading();
|
|
1347
|
+
void startHeading("visual");
|
|
1087
1348
|
return { id: s.id, intent: s.intent, startedAt: s.startedAt };
|
|
1088
1349
|
},
|
|
1089
1350
|
async stop() {
|
|
1090
1351
|
visualEngine.stopSession();
|
|
1091
1352
|
visualEngine.setContextProvider(null);
|
|
1092
|
-
stopHeading();
|
|
1353
|
+
stopHeading("visual");
|
|
1093
1354
|
},
|
|
1094
1355
|
/**
|
|
1095
1356
|
* A deliberate one-shot. Distinct from getSnapshot because it is an
|
|
@@ -1120,9 +1381,20 @@ function createVisualDomain() {
|
|
|
1120
1381
|
err.code = "TIMEOUT";
|
|
1121
1382
|
throw err;
|
|
1122
1383
|
},
|
|
1384
|
+
/**
|
|
1385
|
+
* Fires when the CAMERA block changes — a session opening or closing, the
|
|
1386
|
+
* camera coming up or going down. The engine notifies on every frame too,
|
|
1387
|
+
* and forwarding that made this a raw frame stream, which the freeze's core
|
|
1388
|
+
* rule 4 forbids. A frame arriving is what `getSnapshot()` is for.
|
|
1389
|
+
*/
|
|
1123
1390
|
subscribe(listener) {
|
|
1391
|
+
let last = JSON.stringify(cameraState());
|
|
1124
1392
|
const off = visualEngine.subscribe(() => {
|
|
1125
|
-
|
|
1393
|
+
const camera = cameraState();
|
|
1394
|
+
const key = JSON.stringify(camera);
|
|
1395
|
+
if (key === last) return;
|
|
1396
|
+
last = key;
|
|
1397
|
+
listener(stateEvent("visual", "visualStateChanged", { camera }));
|
|
1126
1398
|
});
|
|
1127
1399
|
return { unsubscribe: off };
|
|
1128
1400
|
}
|
|
@@ -1539,7 +1811,7 @@ function setupExpoProviders(opts) {
|
|
|
1539
1811
|
if (opts?.deviceProvider) (0, import_plugin_runtime2.setPluginNativeProvider)(opts.deviceProvider);
|
|
1540
1812
|
(0, import_plugin_runtime2.setBundleStorage)(new ExpoBundleStorage({ indexKey: opts?.bundleIndexKey }));
|
|
1541
1813
|
(0, import_ui.setClipboardProvider)({
|
|
1542
|
-
setString: (
|
|
1814
|
+
setString: (text2) => ExpoClipboard.setStringAsync(text2).then(() => {
|
|
1543
1815
|
})
|
|
1544
1816
|
});
|
|
1545
1817
|
(0, import_plugin_runtime2.registerBundleModules)({
|
|
@@ -1556,7 +1828,7 @@ function setupExpoProviders(opts) {
|
|
|
1556
1828
|
|
|
1557
1829
|
// src/device/createDeviceProvider.ts
|
|
1558
1830
|
var Haptics2 = __toESM(require("expo-haptics"));
|
|
1559
|
-
var
|
|
1831
|
+
var ExpoLocation4 = __toESM(require("expo-location"));
|
|
1560
1832
|
var ImagePicker = __toESM(require("expo-image-picker"));
|
|
1561
1833
|
var Notifications = __toESM(require("expo-notifications"));
|
|
1562
1834
|
var DocumentPicker = __toESM(require("expo-document-picker"));
|
|
@@ -1598,6 +1870,187 @@ async function detectObjects(id, maxItems = 10, minConfidence = 0.25) {
|
|
|
1598
1870
|
return native.detectObjects(id, maxItems, minConfidence);
|
|
1599
1871
|
}
|
|
1600
1872
|
|
|
1873
|
+
// src/maps/index.ts
|
|
1874
|
+
var import_react_native3 = require("react-native");
|
|
1875
|
+
var import_expo_modules_core2 = require("expo-modules-core");
|
|
1876
|
+
var ExpoLocation2 = __toESM(require("expo-location"));
|
|
1877
|
+
|
|
1878
|
+
// src/args.ts
|
|
1879
|
+
var shown = (v) => {
|
|
1880
|
+
let s;
|
|
1881
|
+
try {
|
|
1882
|
+
s = JSON.stringify(v, (_k, x) => typeof x === "number" && !Number.isFinite(x) ? `${x}` : x)?.replace(/"\\u0001(NaN|-?Infinity)\\u0001"/g, "$1");
|
|
1883
|
+
} catch {
|
|
1884
|
+
}
|
|
1885
|
+
s = s ?? String(v);
|
|
1886
|
+
return s.length > 80 ? `${s.slice(0, 77)}...` : s;
|
|
1887
|
+
};
|
|
1888
|
+
function refuseArgument(capability, verb, problem, nothing) {
|
|
1889
|
+
const e = new Error(`${capability}.${verb}: ${problem}; ${nothing}.`);
|
|
1890
|
+
e.code = "INVALID_ARGUMENT";
|
|
1891
|
+
return e;
|
|
1892
|
+
}
|
|
1893
|
+
function countArg(capability, verb, name, value, def, max, nothing) {
|
|
1894
|
+
const v = value ?? def;
|
|
1895
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 1) {
|
|
1896
|
+
throw refuseArgument(capability, verb, `${name} must be a number from 1 (at most ${max} are used), not ${shown(value)}`, nothing);
|
|
1897
|
+
}
|
|
1898
|
+
return Math.min(max, Math.floor(v));
|
|
1899
|
+
}
|
|
1900
|
+
function fractionArg(capability, verb, name, value, def, nothing) {
|
|
1901
|
+
const v = value ?? def;
|
|
1902
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 1) {
|
|
1903
|
+
throw refuseArgument(capability, verb, `${name} must be a number from 0 to 1, not ${shown(value)}`, nothing);
|
|
1904
|
+
}
|
|
1905
|
+
return v;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// src/maps/normalize.ts
|
|
1909
|
+
var SEARCH_RADIUS_DEFAULT_M = 3e3;
|
|
1910
|
+
var SEARCH_RADIUS_MAX_M = 1e5;
|
|
1911
|
+
var SEARCH_LIMIT_DEFAULT = 8;
|
|
1912
|
+
var SEARCH_LIMIT_MAX = 25;
|
|
1913
|
+
var text = (v) => typeof v === "string" && v.trim() !== "" ? v : null;
|
|
1914
|
+
var refuseArgument2 = (verb, problem, nothing) => refuseArgument("maps", verb, problem, nothing);
|
|
1915
|
+
function coordinateArg(verb, name, value, nothing) {
|
|
1916
|
+
const c = value;
|
|
1917
|
+
const latitude = c?.latitude, longitude = c?.longitude;
|
|
1918
|
+
if (typeof latitude === "number" && Number.isFinite(latitude) && Math.abs(latitude) <= 90 && typeof longitude === "number" && Number.isFinite(longitude) && Math.abs(longitude) <= 180) {
|
|
1919
|
+
return { latitude, longitude };
|
|
1920
|
+
}
|
|
1921
|
+
throw refuseArgument2(
|
|
1922
|
+
verb,
|
|
1923
|
+
`${name} must be { latitude, longitude } numbers (latitude -90..90, longitude -180..180), not ${shown(value)}`,
|
|
1924
|
+
nothing
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
function searchArgs(query, opts) {
|
|
1928
|
+
const nothing = "nothing was searched";
|
|
1929
|
+
if (typeof query !== "string" || query.trim() === "") {
|
|
1930
|
+
throw refuseArgument2("search", `query must be a non-empty string, not ${shown(query)}`, nothing);
|
|
1931
|
+
}
|
|
1932
|
+
const near = opts?.near == null ? null : coordinateArg("search", "near", opts.near, nothing);
|
|
1933
|
+
const radiusM = opts?.radiusM ?? SEARCH_RADIUS_DEFAULT_M;
|
|
1934
|
+
if (typeof radiusM !== "number" || !(radiusM > 0 && radiusM <= SEARCH_RADIUS_MAX_M)) {
|
|
1935
|
+
throw refuseArgument2(
|
|
1936
|
+
"search",
|
|
1937
|
+
`radiusM must be metres above 0 and at most ${SEARCH_RADIUS_MAX_M}, not ${shown(radiusM)}`,
|
|
1938
|
+
nothing
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
const limit = countArg("maps", "search", "limit", opts?.limit, SEARCH_LIMIT_DEFAULT, SEARCH_LIMIT_MAX, nothing);
|
|
1942
|
+
return { query, near, radiusM, limit };
|
|
1943
|
+
}
|
|
1944
|
+
var point = (c) => ({
|
|
1945
|
+
latitude: Number(c.latitude),
|
|
1946
|
+
longitude: Number(c.longitude)
|
|
1947
|
+
});
|
|
1948
|
+
function toMapsAddress(raw) {
|
|
1949
|
+
const r = raw ?? {};
|
|
1950
|
+
return {
|
|
1951
|
+
name: text(r.name),
|
|
1952
|
+
street: text(r.street),
|
|
1953
|
+
streetNumber: text(r.streetNumber),
|
|
1954
|
+
district: text(r.district),
|
|
1955
|
+
city: text(r.city),
|
|
1956
|
+
region: text(r.region),
|
|
1957
|
+
postalCode: text(r.postalCode),
|
|
1958
|
+
country: text(r.country),
|
|
1959
|
+
isoCountryCode: text(r.isoCountryCode)
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
function toMapsCategory(raw) {
|
|
1963
|
+
const c = text(raw);
|
|
1964
|
+
return c ? c.replace(/^MKPOICategory/, "") || null : null;
|
|
1965
|
+
}
|
|
1966
|
+
function toMapsPlaces(raw, near) {
|
|
1967
|
+
return (raw ?? []).map((r) => {
|
|
1968
|
+
const coordinate = point(r);
|
|
1969
|
+
return {
|
|
1970
|
+
name: text(r.name),
|
|
1971
|
+
coordinate,
|
|
1972
|
+
address: toMapsAddress(r.address),
|
|
1973
|
+
category: toMapsCategory(r.category),
|
|
1974
|
+
distanceM: near ? distanceOf([near, coordinate]) : null
|
|
1975
|
+
};
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
function toReverseGeocodedPlace(at, address) {
|
|
1979
|
+
return {
|
|
1980
|
+
name: address.name,
|
|
1981
|
+
coordinate: point(at),
|
|
1982
|
+
address,
|
|
1983
|
+
category: null,
|
|
1984
|
+
distanceM: null
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
function toMapsRoute(raw) {
|
|
1988
|
+
const steps = (raw.steps ?? []).map((s) => ({
|
|
1989
|
+
instructions: typeof s.instructions === "string" ? s.instructions : "",
|
|
1990
|
+
distanceM: Number(s.distance),
|
|
1991
|
+
notice: text(s.notice),
|
|
1992
|
+
polyline: (s.polyline ?? []).map(point)
|
|
1993
|
+
}));
|
|
1994
|
+
return {
|
|
1995
|
+
mode: "walking",
|
|
1996
|
+
distanceM: Number(raw.distance),
|
|
1997
|
+
expectedTravelTimeMs: Math.round(Number(raw.expectedTravelTime) * 1e3),
|
|
1998
|
+
steps,
|
|
1999
|
+
polyline: (raw.polyline ?? []).map(point)
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
function mapsFailure(verb, err) {
|
|
2003
|
+
const nativeCode = String(err?.code ?? "");
|
|
2004
|
+
const detail = String(err?.message ?? err ?? "").trim();
|
|
2005
|
+
let code;
|
|
2006
|
+
let why;
|
|
2007
|
+
if (nativeCode === "THROTTLED") {
|
|
2008
|
+
code = "THROTTLED";
|
|
2009
|
+
why = "Apple is rate-limiting map requests from this app \u2014 wait before retrying";
|
|
2010
|
+
} else if (nativeCode === "OFFLINE") {
|
|
2011
|
+
code = "OFFLINE";
|
|
2012
|
+
why = "the map service could not be reached \u2014 the phone may be offline";
|
|
2013
|
+
} else if (nativeCode === "ERR_GEOCODING_NETWORK") {
|
|
2014
|
+
code = "OFFLINE";
|
|
2015
|
+
why = "the geocoder could not be reached \u2014 the phone may be offline, or Apple is rate-limiting requests (iOS reports both the same way)";
|
|
2016
|
+
} else {
|
|
2017
|
+
code = "UNAVAILABLE";
|
|
2018
|
+
why = "the map service failed";
|
|
2019
|
+
}
|
|
2020
|
+
const failure = new Error(
|
|
2021
|
+
`maps.${verb} could not look: ${why}. This is not "found nothing".` + (detail ? ` (${nativeCode ? nativeCode + ": " : ""}${detail})` : "")
|
|
2022
|
+
);
|
|
2023
|
+
failure.code = code;
|
|
2024
|
+
return failure;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// src/maps/index.ts
|
|
2028
|
+
var native2 = (0, import_expo_modules_core2.requireOptionalNativeModule)("AdaMaps");
|
|
2029
|
+
function missingReason() {
|
|
2030
|
+
if (import_react_native3.Platform.OS !== "ios") return `iOS (Apple MapKit) \u2014 ${import_react_native3.Platform.OS} has no implementation yet`;
|
|
2031
|
+
if (!native2) return "the app build with the AdaMaps module (this build predates it \u2014 a native rebuild is needed)";
|
|
2032
|
+
return null;
|
|
2033
|
+
}
|
|
2034
|
+
function isSupported2() {
|
|
2035
|
+
try {
|
|
2036
|
+
return missingReason() === null && (native2?.isSupported() ?? false);
|
|
2037
|
+
} catch {
|
|
2038
|
+
return false;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
async function search(query, near, radiusM, limit) {
|
|
2042
|
+
if (!native2) throw new Error("ada-maps native module not present in this build");
|
|
2043
|
+
return near ? native2.search(query, near.latitude, near.longitude, true, radiusM, limit) : native2.search(query, 0, 0, false, radiusM, limit);
|
|
2044
|
+
}
|
|
2045
|
+
async function route(from, to) {
|
|
2046
|
+
if (!native2) throw new Error("ada-maps native module not present in this build");
|
|
2047
|
+
return native2.route(from.latitude, from.longitude, to.latitude, to.longitude);
|
|
2048
|
+
}
|
|
2049
|
+
async function reverseGeocode(at) {
|
|
2050
|
+
const [first] = await ExpoLocation2.reverseGeocodeAsync({ latitude: at.latitude, longitude: at.longitude });
|
|
2051
|
+
return first ? toMapsAddress(first) : null;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
1601
2054
|
// src/device-bridge/actions/messaging.ts
|
|
1602
2055
|
var SMS = __toESM(require("expo-sms"));
|
|
1603
2056
|
|
|
@@ -1606,10 +2059,10 @@ var ExpoCalendar = __toESM(require("expo-calendar"));
|
|
|
1606
2059
|
|
|
1607
2060
|
// src/device-bridge/actions/navigation.ts
|
|
1608
2061
|
var Linking = __toESM(require("expo-linking"));
|
|
1609
|
-
var
|
|
2062
|
+
var import_react_native4 = require("react-native");
|
|
1610
2063
|
|
|
1611
2064
|
// src/device-bridge/collectors/health.ts
|
|
1612
|
-
var
|
|
2065
|
+
var import_react_native5 = require("react-native");
|
|
1613
2066
|
|
|
1614
2067
|
// src/device-bridge/background.ts
|
|
1615
2068
|
var TaskManager = __toESM(require("expo-task-manager"));
|
|
@@ -1631,29 +2084,29 @@ __export(location_exports, {
|
|
|
1631
2084
|
requestPermission: () => requestPermission,
|
|
1632
2085
|
watchPosition: () => watchPosition
|
|
1633
2086
|
});
|
|
1634
|
-
var
|
|
2087
|
+
var ExpoLocation3 = __toESM(require("expo-location"));
|
|
1635
2088
|
async function requestPermission() {
|
|
1636
|
-
const { status: fg } = await
|
|
2089
|
+
const { status: fg } = await ExpoLocation3.requestForegroundPermissionsAsync();
|
|
1637
2090
|
if (fg !== "granted") return false;
|
|
1638
2091
|
try {
|
|
1639
|
-
await
|
|
2092
|
+
await ExpoLocation3.requestBackgroundPermissionsAsync();
|
|
1640
2093
|
} catch {
|
|
1641
2094
|
}
|
|
1642
2095
|
return fg === "granted";
|
|
1643
2096
|
}
|
|
1644
2097
|
async function hasPermission() {
|
|
1645
|
-
const { status } = await
|
|
2098
|
+
const { status } = await ExpoLocation3.getForegroundPermissionsAsync();
|
|
1646
2099
|
return status === "granted";
|
|
1647
2100
|
}
|
|
1648
2101
|
async function collectCurrent() {
|
|
1649
|
-
const location = await
|
|
1650
|
-
accuracy:
|
|
2102
|
+
const location = await ExpoLocation3.getCurrentPositionAsync({
|
|
2103
|
+
accuracy: ExpoLocation3.Accuracy.Balanced
|
|
1651
2104
|
});
|
|
1652
2105
|
let place = "Current Location";
|
|
1653
2106
|
let address = `${location.coords.latitude.toFixed(4)}, ${location.coords.longitude.toFixed(4)}`;
|
|
1654
2107
|
let locationType = "unknown";
|
|
1655
2108
|
try {
|
|
1656
|
-
const
|
|
2109
|
+
const geocoded = await reverseGeocode({
|
|
1657
2110
|
latitude: location.coords.latitude,
|
|
1658
2111
|
longitude: location.coords.longitude
|
|
1659
2112
|
});
|
|
@@ -1692,8 +2145,8 @@ async function ensurePermission() {
|
|
|
1692
2145
|
}
|
|
1693
2146
|
async function getPosition(opts = {}) {
|
|
1694
2147
|
await ensurePermission();
|
|
1695
|
-
const fix = await
|
|
1696
|
-
accuracy: opts.accuracy === "high" ?
|
|
2148
|
+
const fix = await ExpoLocation3.getCurrentPositionAsync({
|
|
2149
|
+
accuracy: opts.accuracy === "high" ? ExpoLocation3.Accuracy.High : ExpoLocation3.Accuracy.Balanced
|
|
1697
2150
|
});
|
|
1698
2151
|
return toPosition(fix);
|
|
1699
2152
|
}
|
|
@@ -1711,15 +2164,21 @@ function toPosition(fix) {
|
|
|
1711
2164
|
function nonNegative(v) {
|
|
1712
2165
|
return typeof v === "number" && v >= 0 ? v : void 0;
|
|
1713
2166
|
}
|
|
1714
|
-
|
|
2167
|
+
var WATCH_TIER = {
|
|
2168
|
+
navigation: ExpoLocation3.Accuracy.BestForNavigation,
|
|
2169
|
+
high: ExpoLocation3.Accuracy.High,
|
|
2170
|
+
balanced: ExpoLocation3.Accuracy.Balanced,
|
|
2171
|
+
low: ExpoLocation3.Accuracy.Low
|
|
2172
|
+
};
|
|
2173
|
+
function watchPosition(cb, opts) {
|
|
1715
2174
|
let subscription = null;
|
|
1716
2175
|
let cancelled = false;
|
|
1717
2176
|
(async () => {
|
|
1718
2177
|
try {
|
|
1719
2178
|
await ensurePermission();
|
|
1720
2179
|
if (cancelled) return;
|
|
1721
|
-
subscription = await
|
|
1722
|
-
{ accuracy:
|
|
2180
|
+
subscription = await ExpoLocation3.watchPositionAsync(
|
|
2181
|
+
{ accuracy: WATCH_TIER[opts?.accuracy ?? "balanced"] },
|
|
1723
2182
|
(fix) => cb(toPosition(fix))
|
|
1724
2183
|
);
|
|
1725
2184
|
if (cancelled) {
|
|
@@ -1794,7 +2253,7 @@ function getCellularGeneration() {
|
|
|
1794
2253
|
var ExpoNotifications = __toESM(require("expo-notifications"));
|
|
1795
2254
|
|
|
1796
2255
|
// src/device-bridge/collectors/appEngagement.ts
|
|
1797
|
-
var
|
|
2256
|
+
var import_react_native6 = require("react-native");
|
|
1798
2257
|
var import_async_storage5 = __toESM(require("@react-native-async-storage/async-storage"));
|
|
1799
2258
|
|
|
1800
2259
|
// src/device-bridge/collectors/camera.ts
|
|
@@ -1909,9 +2368,9 @@ function stubApi(capability) {
|
|
|
1909
2368
|
}
|
|
1910
2369
|
function createLocationAPI() {
|
|
1911
2370
|
const ensurePermission2 = async () => {
|
|
1912
|
-
const existing = await
|
|
2371
|
+
const existing = await ExpoLocation4.getForegroundPermissionsAsync();
|
|
1913
2372
|
if (existing.status === "granted") return;
|
|
1914
|
-
const asked = await
|
|
2373
|
+
const asked = await ExpoLocation4.requestForegroundPermissionsAsync();
|
|
1915
2374
|
if (asked.status !== "granted") {
|
|
1916
2375
|
throw new Error(
|
|
1917
2376
|
"Location permission was not granted. Enable it for this app in Settings > Privacy > Location Services."
|
|
@@ -1921,8 +2380,8 @@ function createLocationAPI() {
|
|
|
1921
2380
|
return {
|
|
1922
2381
|
async getCurrent(opts) {
|
|
1923
2382
|
await ensurePermission2();
|
|
1924
|
-
const fix = await
|
|
1925
|
-
accuracy: opts?.accuracy === "high" ?
|
|
2383
|
+
const fix = await ExpoLocation4.getCurrentPositionAsync({
|
|
2384
|
+
accuracy: opts?.accuracy === "high" ? ExpoLocation4.Accuracy.High : ExpoLocation4.Accuracy.Balanced
|
|
1926
2385
|
});
|
|
1927
2386
|
return {
|
|
1928
2387
|
latitude: fix.coords.latitude,
|
|
@@ -1937,8 +2396,8 @@ function createLocationAPI() {
|
|
|
1937
2396
|
try {
|
|
1938
2397
|
await ensurePermission2();
|
|
1939
2398
|
if (cancelled) return;
|
|
1940
|
-
subscription = await
|
|
1941
|
-
{ accuracy:
|
|
2399
|
+
subscription = await ExpoLocation4.watchPositionAsync(
|
|
2400
|
+
{ accuracy: ExpoLocation4.Accuracy.Balanced },
|
|
1942
2401
|
(fix) => cb({ latitude: fix.coords.latitude, longitude: fix.coords.longitude })
|
|
1943
2402
|
);
|
|
1944
2403
|
if (cancelled) {
|
|
@@ -2312,18 +2771,27 @@ function createVisionAPI() {
|
|
|
2312
2771
|
// pixels). [] = looked, found no text (a real finding); throws = could not
|
|
2313
2772
|
// look (unknown/aged-out id). describe (VNClassifyImage) is the next pass.
|
|
2314
2773
|
async readText(frameId, opts) {
|
|
2315
|
-
|
|
2774
|
+
const langs = opts?.languages;
|
|
2775
|
+
if (langs != null && (!Array.isArray(langs) || langs.some((l) => typeof l !== "string"))) {
|
|
2776
|
+
throw refuseArgument("vision", "readText", `languages must be an array of language codes, not ${shown(langs)}`, "nothing was read");
|
|
2777
|
+
}
|
|
2778
|
+
return readText(frameId, langs);
|
|
2316
2779
|
},
|
|
2317
2780
|
// describe: on-device labels (VNClassifyImage), not prose — the sentence is
|
|
2318
2781
|
// the skill's job. [] = looked, recognised nothing.
|
|
2319
2782
|
async describe(frameId, opts) {
|
|
2320
|
-
return describe(frameId, opts?.maxLabels
|
|
2783
|
+
return describe(frameId, countArg("vision", "describe", "maxLabels", opts?.maxLabels, 5, 50, "nothing was labelled"));
|
|
2321
2784
|
},
|
|
2322
2785
|
// detectObjects: on-device located objects (bundled detector), each with a
|
|
2323
2786
|
// box — what a guidance loop reads to say WHERE a thing is, not just that the
|
|
2324
2787
|
// scene contains it. [] = looked, found nothing; throws = could not look.
|
|
2325
2788
|
async detectObjects(frameId, opts) {
|
|
2326
|
-
|
|
2789
|
+
const nothing = "nothing was detected";
|
|
2790
|
+
return detectObjects(
|
|
2791
|
+
frameId,
|
|
2792
|
+
countArg("vision", "detectObjects", "maxItems", opts?.maxItems, 10, 50, nothing),
|
|
2793
|
+
fractionArg("vision", "detectObjects", "minConfidence", opts?.minConfidence, 0.25, nothing)
|
|
2794
|
+
);
|
|
2327
2795
|
},
|
|
2328
2796
|
// assessQuality: resolve the frame id to its LOCAL file, measure on-device,
|
|
2329
2797
|
// and record the result back onto the frame so a later read of
|
|
@@ -2341,6 +2809,55 @@ function createVisionAPI() {
|
|
|
2341
2809
|
}
|
|
2342
2810
|
};
|
|
2343
2811
|
}
|
|
2812
|
+
function createMapsAPI() {
|
|
2813
|
+
const refuseIfMissing = (verb, nothing) => {
|
|
2814
|
+
const missing = missingReason();
|
|
2815
|
+
if (missing) {
|
|
2816
|
+
throw new CapabilityUnavailableError(`maps.${verb}`, `maps.${verb} needs ${missing}; ${nothing}.`);
|
|
2817
|
+
}
|
|
2818
|
+
};
|
|
2819
|
+
return {
|
|
2820
|
+
async isSupported() {
|
|
2821
|
+
return isSupported2();
|
|
2822
|
+
},
|
|
2823
|
+
// [] = Apple looked and found nothing (a real finding); throws = could not
|
|
2824
|
+
// look. The defaults (radiusM 3000, limit 8) live in searchArgs.
|
|
2825
|
+
async search(query, opts) {
|
|
2826
|
+
const args = searchArgs(query, opts);
|
|
2827
|
+
refuseIfMissing("search", "nothing was searched");
|
|
2828
|
+
const raw = await search(args.query, args.near, args.radiusM, args.limit).catch((err) => {
|
|
2829
|
+
throw mapsFailure("search", err);
|
|
2830
|
+
});
|
|
2831
|
+
return toMapsPlaces(raw, args.near);
|
|
2832
|
+
},
|
|
2833
|
+
// null = nothing at that point; throws = could not look.
|
|
2834
|
+
async reverseGeocode(at) {
|
|
2835
|
+
const point2 = coordinateArg("reverseGeocode", "at", at, "nothing was looked up");
|
|
2836
|
+
const address = await reverseGeocode(point2).catch((err) => {
|
|
2837
|
+
throw mapsFailure("reverseGeocode", err);
|
|
2838
|
+
});
|
|
2839
|
+
return address ? toReverseGeocodedPlace(point2, address) : null;
|
|
2840
|
+
},
|
|
2841
|
+
// null = Apple found no route. Never an empty 0 m route, which would read as
|
|
2842
|
+
// "you have arrived". A remote bundle can pass any string as `mode`, so a
|
|
2843
|
+
// mode this build cannot honour refuses rather than silently walking.
|
|
2844
|
+
async route(from, to, opts) {
|
|
2845
|
+
if (opts?.mode !== void 0 && opts.mode !== "walking") {
|
|
2846
|
+
throw new CapabilityUnavailableError(
|
|
2847
|
+
"maps.route",
|
|
2848
|
+
`maps.route supports walking only, not "${String(opts.mode)}"; no route was requested.`
|
|
2849
|
+
);
|
|
2850
|
+
}
|
|
2851
|
+
const a = coordinateArg("route", "from", from, "no route was requested");
|
|
2852
|
+
const b = coordinateArg("route", "to", to, "no route was requested");
|
|
2853
|
+
refuseIfMissing("route", "no route was requested");
|
|
2854
|
+
const raw = await route(a, b).catch((err) => {
|
|
2855
|
+
throw mapsFailure("route", err);
|
|
2856
|
+
});
|
|
2857
|
+
return raw ? toMapsRoute(raw) : null;
|
|
2858
|
+
}
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2344
2861
|
function createDeviceProvider(config) {
|
|
2345
2862
|
const connectivity = createConnectivityAPI(config?.reachabilityUrl);
|
|
2346
2863
|
return {
|
|
@@ -2363,12 +2880,13 @@ function createDeviceProvider(config) {
|
|
|
2363
2880
|
scanner: createScannerAPI(),
|
|
2364
2881
|
nfc: createNfcAPI(),
|
|
2365
2882
|
bluetooth: createBluetoothAPI(),
|
|
2366
|
-
vision: createVisionAPI()
|
|
2883
|
+
vision: createVisionAPI(),
|
|
2884
|
+
maps: createMapsAPI()
|
|
2367
2885
|
};
|
|
2368
2886
|
}
|
|
2369
2887
|
|
|
2370
2888
|
// src/oauth/expoOAuth.tsx
|
|
2371
|
-
var
|
|
2889
|
+
var import_react_native7 = require("react-native");
|
|
2372
2890
|
var import_google_signin = require("@react-native-google-signin/google-signin");
|
|
2373
2891
|
var AppleAuthentication = __toESM(require("expo-apple-authentication"));
|
|
2374
2892
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
@@ -2409,7 +2927,7 @@ function createExpoOAuthProvider(host = {}) {
|
|
|
2409
2927
|
}
|
|
2410
2928
|
},
|
|
2411
2929
|
async isAppleAvailable() {
|
|
2412
|
-
if (
|
|
2930
|
+
if (import_react_native7.Platform.OS !== "ios") return false;
|
|
2413
2931
|
try {
|
|
2414
2932
|
return await AppleAuthentication.isAvailableAsync();
|
|
2415
2933
|
} catch {
|