@bindimaps/hyperlocal-web-sdk 0.0.1

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 ADDED
@@ -0,0 +1,162 @@
1
+ # @bindimaps/hyperlocal-web-sdk
2
+
3
+ Core SDK for image-based indoor localisation using the BindiMaps Landseer API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @bindimaps/hyperlocal-web-sdk
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { estimatePosition, type CapturedFrame } from "@bindimaps/hyperlocal-web-sdk"
15
+
16
+ const frames: CapturedFrame[] = capturedFrames // Array of { timestamp, imageData: Blob }
17
+
18
+ const result = await estimatePosition(
19
+ frames,
20
+ "your-location-id",
21
+ { latitude: -33.8688, longitude: 151.2093 },
22
+ )
23
+
24
+ if (result.type === "success") {
25
+ console.log("Position:", result.position, "Floor:", result.floorId)
26
+ } else {
27
+ console.error("Failed:", result.code, result.message)
28
+ }
29
+ ```
30
+
31
+ ## API Reference
32
+
33
+ ### `estimatePosition(frames, locationId, gpsPosition, options?)`
34
+
35
+ Estimate a user's indoor position from captured camera frames and GPS.
36
+
37
+ #### Parameters
38
+
39
+ | Parameter | Type | Description |
40
+ |-----------|------|-------------|
41
+ | `frames` | `CapturedFrame[]` | Captured camera frames (minimum 6) |
42
+ | `locationId` | `string` | Bindi location ID to localise within |
43
+ | `gpsPosition` | `GeoCoordinate` | Approximate GPS coordinates |
44
+ | `options` | `EstimatePositionOptions` | Optional config (environment, mock mode) |
45
+
46
+ #### Returns `Promise<PositionResult>`
47
+
48
+ Success:
49
+ ```typescript
50
+ {
51
+ type: "success"
52
+ position: { latitude: number; longitude: number }
53
+ floorId: string
54
+ headingDegrees: number
55
+ }
56
+ ```
57
+
58
+ Error:
59
+ ```typescript
60
+ {
61
+ type: "error"
62
+ code: "invalid_input" | "insufficient_frames" | "no_match" | "location_mismatch" | "api_error"
63
+ message: string
64
+ details?: string
65
+ }
66
+ ```
67
+
68
+ ## Types
69
+
70
+ ### `CapturedFrame`
71
+
72
+ ```typescript
73
+ type CapturedFrame = {
74
+ timestamp: Date
75
+ imageData: Blob // JPEG image data
76
+ }
77
+ ```
78
+
79
+ ### `GeoCoordinate`
80
+
81
+ ```typescript
82
+ type GeoCoordinate = {
83
+ latitude: number
84
+ longitude: number
85
+ }
86
+ ```
87
+
88
+ ### `EstimatePositionOptions`
89
+
90
+ ```typescript
91
+ type EstimatePositionOptions = {
92
+ environment?: HyperlocalEnvironment
93
+ mock?: MockPositionConfig
94
+ }
95
+ ```
96
+
97
+ ### `MockPositionConfig`
98
+
99
+ ```typescript
100
+ type MockPositionConfig =
101
+ | true
102
+ | {
103
+ position?: GeoCoordinate
104
+ floorId?: string
105
+ headingDegrees?: number
106
+ }
107
+ ```
108
+
109
+ ## Mock Mode
110
+
111
+ Use mock mode for local development and testing without a real camera or API connection:
112
+
113
+ ```typescript
114
+ // Simple mock — returns default position based on GPS input
115
+ const result = await estimatePosition(frames, "loc-id", gps, { mock: true })
116
+
117
+ // Custom mock — specify exact values
118
+ const result = await estimatePosition(frames, "loc-id", gps, {
119
+ mock: {
120
+ position: { latitude: -33.87, longitude: 151.21 },
121
+ floorId: "level-2",
122
+ headingDegrees: 180,
123
+ }
124
+ })
125
+ ```
126
+
127
+ Mock mode:
128
+ - Skips all validation, protobuf, and gRPC calls
129
+ - Returns after a 500ms simulated delay
130
+ - Always returns `type: "success"`
131
+
132
+ ## Error Codes
133
+
134
+ | Code | Description |
135
+ |------|-------------|
136
+ | `invalid_input` | Invalid `locationId`, GPS input, or frame payload shape |
137
+ | `insufficient_frames` | Fewer than 6 frames provided |
138
+ | `no_match` | Images didn't match any known location features |
139
+ | `location_mismatch` | Matched a different location than the requested `locationId` |
140
+ | `api_error` | Network or transport failure |
141
+
142
+ ## Integration with React
143
+
144
+ For React applications, use `@bindimaps/hyperlocal-react` which provides hooks for camera stream management and frame capture.
145
+
146
+ ```bash
147
+ npm install @bindimaps/hyperlocal-react
148
+ ```
149
+
150
+ See the [@bindimaps/hyperlocal-react README](https://www.npmjs.com/package/@bindimaps/hyperlocal-react) for React-specific documentation.
151
+
152
+ ## Development
153
+
154
+ ```bash
155
+ yarn build # Build the library
156
+ yarn start # Watch mode
157
+ yarn type-check # Type check
158
+ ```
159
+
160
+ ## License
161
+
162
+ MIT
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @bindimaps/hyperlocal-web-sdk
3
+ *
4
+ * SDK for image-based indoor localisation using the Landseer API.
5
+ */
6
+ type GeoCoordinate = {
7
+ latitude: number;
8
+ longitude: number;
9
+ };
10
+ type CapturedFrame = {
11
+ timestamp: Date;
12
+ imageData: Blob;
13
+ };
14
+ type PositionEstimate = {
15
+ type: "success";
16
+ position: GeoCoordinate;
17
+ floorId: string;
18
+ headingDegrees: number;
19
+ };
20
+ type PositionErrorCode = "invalid_input" | "insufficient_frames" | "no_match" | "location_mismatch" | "api_error";
21
+ type PositionError = {
22
+ type: "error";
23
+ code: PositionErrorCode;
24
+ message: string;
25
+ details?: string;
26
+ };
27
+ type PositionResult = PositionEstimate | PositionError;
28
+ type MockPositionConfig = true | {
29
+ position?: GeoCoordinate;
30
+ floorId?: string;
31
+ headingDegrees?: number;
32
+ };
33
+ type EstimatePositionOptions = {
34
+ environment?: HyperlocalEnvironment;
35
+ mock?: MockPositionConfig;
36
+ };
37
+ declare const MIN_LOCALISATION_IMAGE_COUNT = 6;
38
+ declare enum HyperlocalEnvironment {
39
+ UNSPECIFIED = 0,
40
+ DEV_PREVIEW = 1,
41
+ DEV_PUBLIC = 2,
42
+ PROD_PREVIEW = 3,
43
+ PROD_PUBLIC = 4
44
+ }
45
+ /**
46
+ * Estimate a user's indoor position from captured camera frames and GPS.
47
+ *
48
+ * @param frames - Captured camera frames (minimum 6)
49
+ * @param locationId - The Bindi location ID to localise within
50
+ * @param gpsPosition - Approximate GPS coordinates
51
+ * @param options - Optional configuration (environment, mock mode)
52
+ */
53
+ declare const estimatePosition: (frames: CapturedFrame[], locationId: string, gpsPosition: GeoCoordinate, options?: EstimatePositionOptions) => Promise<PositionResult>;
54
+
55
+ export { type CapturedFrame, type EstimatePositionOptions, type GeoCoordinate, HyperlocalEnvironment, MIN_LOCALISATION_IMAGE_COUNT, type MockPositionConfig, type PositionError, type PositionErrorCode, type PositionEstimate, type PositionResult, estimatePosition };
package/dist/index.js ADDED
@@ -0,0 +1,248 @@
1
+ var __async = (__this, __arguments, generator) => {
2
+ return new Promise((resolve, reject) => {
3
+ var fulfilled = (value) => {
4
+ try {
5
+ step(generator.next(value));
6
+ } catch (e) {
7
+ reject(e);
8
+ }
9
+ };
10
+ var rejected = (value) => {
11
+ try {
12
+ step(generator.throw(value));
13
+ } catch (e) {
14
+ reject(e);
15
+ }
16
+ };
17
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
18
+ step((generator = generator.apply(__this, __arguments)).next());
19
+ });
20
+ };
21
+
22
+ // ../../../node_modules/@bindimaps/landseer/bmproto/landseer/landseer_pb.js
23
+ import { enumDesc, fileDesc, messageDesc, serviceDesc, tsEnum } from "@bufbuild/protobuf/codegenv2";
24
+ import { file_google_protobuf_wrappers } from "@bufbuild/protobuf/wkt";
25
+ var file_bmproto_landseer_landseer = /* @__PURE__ */ fileDesc("Ch9ibXByb3RvL2xhbmRzZWVyL2xhbmRzZWVyLnByb3RvEhBibXByb3RvLmxhbmRzZWVyIhAKDkhlYWx0aHpSZXF1ZXN0IiQKD0hlYWx0aHpSZXNwb25zZRIRCgl0aW1lc3RhbXAYASABKAMiIgoEUG9zZRIMCgR0dmVjGAEgAygCEgwKBHF2ZWMYAiADKAIiSAoRUGluaG9sZUludHJpbnNpY3MSCQoBZhgBIAMoAhIJCgFjGAIgAygCEg4KBmhlaWdodBgDIAEoDRINCgV3aWR0aBgEIAEoDSJDCg1EZWx0YUVuY29kaW5nEg8KB2luaXRpYWwYASABKAESDgoGZGVsdGFzGAIgAygREhEKCXByZWNpc2lvbhgDIAEoDSLUAQoQQWNjZWxlcm9tZXRlckxvZxIzCgp1bml4TWlsbGlzGAEgASgLMh8uYm1wcm90by5sYW5kc2Vlci5EZWx0YUVuY29kaW5nEi0KBHhNczIYAiABKAsyHy5ibXByb3RvLmxhbmRzZWVyLkRlbHRhRW5jb2RpbmcSLQoEeU1zMhgDIAEoCzIfLmJtcHJvdG8ubGFuZHNlZXIuRGVsdGFFbmNvZGluZxItCgR6TXMyGAQgASgLMh8uYm1wcm90by5sYW5kc2Vlci5EZWx0YUVuY29kaW5nIt0BCgdHeXJvTG9nEjMKCnVuaXhNaWxsaXMYASABKAsyHy5ibXByb3RvLmxhbmRzZWVyLkRlbHRhRW5jb2RpbmcSMwoKeFJhZFBlclNlYxgCIAEoCzIfLmJtcHJvdG8ubGFuZHNlZXIuRGVsdGFFbmNvZGluZxIzCgp5UmFkUGVyU2VjGAMgASgLMh8uYm1wcm90by5sYW5kc2Vlci5EZWx0YUVuY29kaW5nEjMKCnpSYWRQZXJTZWMYBCABKAsyHy5ibXByb3RvLmxhbmRzZWVyLkRlbHRhRW5jb2Rpbmci8gEKC1JvdGF0aW9uTG9nEjMKCnVuaXhNaWxsaXMYASABKAsyHy5ibXByb3RvLmxhbmRzZWVyLkRlbHRhRW5jb2RpbmcSKgoBeBgCIAEoCzIfLmJtcHJvdG8ubGFuZHNlZXIuRGVsdGFFbmNvZGluZxIqCgF5GAMgASgLMh8uYm1wcm90by5sYW5kc2Vlci5EZWx0YUVuY29kaW5nEioKAXoYBCABKAsyHy5ibXByb3RvLmxhbmRzZWVyLkRlbHRhRW5jb2RpbmcSKgoBdxgFIAEoCzIfLmJtcHJvdG8ubGFuZHNlZXIuRGVsdGFFbmNvZGluZyLKAwoFRnJhbWUSCgoCdHMYASABKAMSEQoJaW1hZ2VEYXRhGAIgASgMEigKCHNsYW1Qb3NlGAMgASgLMhYuYm1wcm90by5sYW5kc2Vlci5Qb3NlEjcKCmludHJpbnNpY3MYBCABKAsyIy5ibXByb3RvLmxhbmRzZWVyLlBpbmhvbGVJbnRyaW5zaWNzEhgKDG5vcnRoRGVncmVlcxgFIAEoAkICGAESOQoUbm9ydGhEZWdyZWVzRXN0aW1hdGUYBiABKAsyGy5nb29nbGUucHJvdG9idWYuRmxvYXRWYWx1ZRIwCgtwcmVzc3VyZUhQYRgHIAEoCzIbLmdvb2dsZS5wcm90b2J1Zi5GbG9hdFZhbHVlEjYKEHByZWRpY3RlZEZsb29ySWQYCCABKAsyHC5nb29nbGUucHJvdG9idWYuU3RyaW5nVmFsdWUSPQoYcHJlZGljdGVkRmxvb3JDb25maWRlbmNlGAkgASgLMhsuZ29vZ2xlLnByb3RvYnVmLkZsb2F0VmFsdWUSEgoKZnJhbWVJbmRleBgKIAEoDRItCgtncHNMb2NhdGlvbhgLIAEoCzIYLmJtcHJvdG8ubGFuZHNlZXIuTGF0TG9uIrkBCgZMYXRMb24SCwoDbGF0GAEgASgCEgsKA2xvbhgCIAEoAhIgChhob3Jpem9udGFsQWNjdXJhY3lNZXRlcnMYAyABKAISNgoRbXNsQWx0aXR1ZGVNZXRlcnMYBCABKAsyGy5nb29nbGUucHJvdG9idWYuRmxvYXRWYWx1ZRI7ChZ2ZXJ0aWNhbEFjY3VyYWN5TWV0ZXJzGAUgASgLMhsuZ29vZ2xlLnByb3RvYnVmLkZsb2F0VmFsdWUiwAEKCFNsYW1Qb3NlEgoKAnRzGAEgASgEEiQKBHBvc2UYAiABKAsyFi5ibXByb3RvLmxhbmRzZWVyLlBvc2USMQoMbm9ydGhEZWdyZWVzGAMgASgLMhsuZ29vZ2xlLnByb3RvYnVmLkZsb2F0VmFsdWUSMAoLcHJlc3N1cmVIUGEYBCABKAsyGy5nb29nbGUucHJvdG9idWYuRmxvYXRWYWx1ZRIdChVzbGFtQ29vcmRpbmF0ZVNwYWNlSWQYBSABKA0ifgoORGV2aWNlUG9zaXRpb24SCQoBeBgBIAEoAhIJCgF5GAIgASgCEhQKDG5vcnRoRGVncmVlcxgDIAEoAhIgChhob3Jpem9udGFsQWNjdXJhY3lNZXRlcnMYBCABKAISDwoHZmxvb3JJZBgFIAEoCRINCgVsb2NJZBgGIAEoCSJOChFEZXZpY2VQb3NpdGlvbkxvZxIKCgJ0cxgBIAEoBBItCgNwb3MYAiABKAsyIC5ibXByb3RvLmxhbmRzZWVyLkRldmljZVBvc2l0aW9uIkgKC0dwc0xvZ0VudHJ5EgoKAnRzGAEgASgEEi0KC2dwc0xvY2F0aW9uGAIgASgLMhguYm1wcm90by5sYW5kc2Vlci5MYXRMb24i6wUKD0xvY2FsaXNlUmVxdWVzdBIRCglzZXNzaW9uSWQYASABKAkSEwoLc2VxdWVuY2VOdW0YAiABKA0SMAoOZ3BzQ29vcmRpbmF0ZXMYAyABKAsyGC5ibXByb3RvLmxhbmRzZWVyLkxhdExvbhInCgZmcmFtZXMYBCADKAsyFy5ibXByb3RvLmxhbmRzZWVyLkZyYW1lEi0KCXNsYW1Qb3NlcxgFIAMoCzIWLmJtcHJvdG8ubGFuZHNlZXIuUG9zZUICGAESFwoPcGFyZW50U2Vzc2lvbklkGAYgASgJEisKB3NsYW1Mb2cYByADKAsyGi5ibXByb3RvLmxhbmRzZWVyLlNsYW1Qb3NlEjAKCmRldmljZVR5cGUYCCABKA4yHC5ibXByb3RvLmxhbmRzZWVyLkRldmljZVR5cGUSMgoLZW52aXJvbm1lbnQYCSABKA4yHS5ibXByb3RvLmxhbmRzZWVyLkVudmlyb25tZW50Eh0KFXNsYW1Db29yZGluYXRlU3BhY2VJZBgKIAEoDRI+ChFkZXZpY2VQb3NpdGlvbkxvZxgLIAMoCzIjLmJtcHJvdG8ubGFuZHNlZXIuRGV2aWNlUG9zaXRpb25Mb2cSNgoNdHJhbnNmb3JtVHlwZRgMIAEoDjIfLmJtcHJvdG8ubGFuZHNlZXIuVHJhbnNmb3JtVHlwZRItCgZncHNMb2cYDSADKAsyHS5ibXByb3RvLmxhbmRzZWVyLkdwc0xvZ0VudHJ5EjIKBmFjY0xvZxgOIAEoCzIiLmJtcHJvdG8ubGFuZHNlZXIuQWNjZWxlcm9tZXRlckxvZxIqCgdneXJvTG9nGA8gASgLMhkuYm1wcm90by5sYW5kc2Vlci5HeXJvTG9nEi0KBnJvdExvZxgQIAEoCzIdLmJtcHJvdG8ubGFuZHNlZXIuUm90YXRpb25Mb2cSFQoNaXNIZWFsdGhjaGVjaxgRIAEoCBIOCgZpc1NjYW4YEiABKAgiNgoGTWF0cml4EgwKBHJvd3MYASABKA0SDAoEY29scxgCIAEoDRIQCghlbGVtZW50cxgDIAMoASLiAgoQTG9jYWxpc2VSZXNwb25zZRIRCglzZXNzaW9uSWQYASABKAkSEwoLc2VxdWVuY2VOdW0YAiABKA0SKwoLc2xhbUludmVyc2UYAyABKAsyFi5ibXByb3RvLmxhbmRzZWVyLlBvc2USLgoOb3B0aW1pc2VkUG9zZXMYBCADKAsyFi5ibXByb3RvLmxhbmRzZWVyLlBvc2USEgoKbG9jYXRpb25JZBgFIAEoCRIPCgdmbG9vcklkGAYgASgJEi0KC3NsYW1Ub01vZGVsGAcgASgLMhguYm1wcm90by5sYW5kc2Vlci5NYXRyaXgSHgoWY29uZmlkZW5jZVJhZGl1c01ldGVycxgIIAEoAhIdChVzbGFtQ29vcmRpbmF0ZVNwYWNlSWQYCSABKA0SNgoNdHJhbnNmb3JtVHlwZRgKIAEoDjIfLmJtcHJvdG8ubGFuZHNlZXIuVHJhbnNmb3JtVHlwZSKOAQoRU3VibWl0U2NhblJlcXVlc3QSDgoGc2NhbklkGAEgASgJEgoKAnRzGAIgASgDEjAKCmRldmljZVR5cGUYAyABKA4yHC5ibXByb3RvLmxhbmRzZWVyLkRldmljZVR5cGUSKwoKc2NhbkZyYW1lcxgEIAMoCzIXLmJtcHJvdG8ubGFuZHNlZXIuRnJhbWUiJAoSU3VibWl0U2NhblJlc3BvbnNlEg4KBnNjYW5JZBgBIAEoCSJbChlHZXRGbG9vclRyYW5zZm9ybXNSZXF1ZXN0EhIKCmxvY2F0aW9uSWQYASABKAkSKgoDZW52GAIgASgOMh0uYm1wcm90by5sYW5kc2Vlci5FbnZpcm9ubWVudCLnAQoORmxvb3JUcmFuc2Zvcm0SEgoKbG9jYXRpb25JZBgBIAEoCRIPCgdmbG9vcklkGAIgASgJEjIKDG1vZGVsVG9GbG9vchgDIAEoCzIYLmJtcHJvdG8ubGFuZHNlZXIuTWF0cml4QgIYARIXCg9tb2RlbFRvRmxvb3JSb3QYBCADKAISGQoRY29vcmRpbmF0ZVNwYWNlSWQYBSABKAkSDQoFZmxpcFkYBiABKAgSOQoXbW9kZWxUb0Zsb29yVW5jb3JyZWN0ZWQYByABKAsyGC5ibXByb3RvLmxhbmRzZWVyLk1hdHJpeCKXAQoaR2V0Rmxvb3JUcmFuc2Zvcm1zUmVzcG9uc2USEgoKbG9jYXRpb25JZBgBIAEoCRI5Cg9mbG9vclRyYW5zZm9ybXMYAiADKAsyIC5ibXByb3RvLmxhbmRzZWVyLkZsb29yVHJhbnNmb3JtEioKA2VudhgDIAEoDjIdLmJtcHJvdG8ubGFuZHNlZXIuRW52aXJvbm1lbnQiOQoQTG9jYWxpc2VBc3luY0FjaxIRCglzZXNzaW9uSWQYASABKAkSEgoKc2VxdWVuY2VJZBgCIAEoDSKMAQoVTG9jYWxpc2VBc3luY1Jlc3BvbnNlEjEKA2FjaxgBIAEoCzIiLmJtcHJvdG8ubGFuZHNlZXIuTG9jYWxpc2VBc3luY0Fja0gAEjYKCHJlc3BvbnNlGAIgASgLMiIuYm1wcm90by5sYW5kc2Vlci5Mb2NhbGlzZVJlc3BvbnNlSABCCAoGcmVzdWx0IkYKFExvY2FsaXNlQXN5bmNSZXF1ZXN0Ei4KA3JlcRgBIAEoCzIhLmJtcHJvdG8ubGFuZHNlZXIuTG9jYWxpc2VSZXF1ZXN0IkYKFFN0YXJ0TG9jYWxpc2VSZXF1ZXN0Ei4KA3JlcRgBIAEoCzIhLmJtcHJvdG8ubGFuZHNlZXIuTG9jYWxpc2VSZXF1ZXN0Ij4KFVN0YXJ0TG9jYWxpc2VSZXNwb25zZRIRCglzZXNzaW9uSWQYASABKAkSEgoKc2VxdWVuY2VJZBgCIAEoDSK4AQoRTW9kZWxQb3NlRXN0aW1hdGUSDwoHZmxvb3JJZBgBIAEoCRItCg1lc3RpbWF0ZWRQb3NlGAIgASgLMhYuYm1wcm90by5sYW5kc2Vlci5Qb3NlEhYKDmFjY3VyYWN5TWV0ZXJzGAMgASgCEhcKD2FjY3VyYWN5RGVncmVlcxgEIAEoAhISCgpsb2NhdGlvbklkGAUgASgJEh4KFndlaWdodGVkQXZlcmFnZUltYWdlVHMYBiABKAQikgEKD0xvY2FsaXNlUGVuZGluZxIRCglzZXNzaW9uSWQYASABKAkSEgoKc2VxdWVuY2VJZBgCIAEoDRIdChVyZXF1ZXN0UmVjZWl2ZWRVbml4TXMYAyABKAMSOQoMcG9zZUVzdGltYXRlGAQgASgLMiMuYm1wcm90by5sYW5kc2Vlci5Nb2RlbFBvc2VFc3RpbWF0ZSKIAQoNTG9jYWxpc2VFcnJvchIRCglzZXNzaW9uSWQYASABKAkSEgoKc2VxdWVuY2VJZBgCIAEoDRIdChVyZXF1ZXN0UmVjZWl2ZWRVbml4TXMYAyABKAMSGwoTcmVxdWVzdEZhaWxlZFVuaXhNcxgEIAEoAxIUCgxlcnJvck1lc3NhZ2UYBSABKAkiQQoYR2V0TG9jYWxpc2VSZXN1bHRSZXF1ZXN0EhEKCXNlc3Npb25JZBgBIAEoCRISCgpzZXF1ZW5jZUlkGAIgASgNIsUBChlHZXRMb2NhbGlzZVJlc3VsdFJlc3BvbnNlEjQKB3BlbmRpbmcYASABKAsyIS5ibXByb3RvLmxhbmRzZWVyLkxvY2FsaXNlUGVuZGluZ0gAEjAKBWVycm9yGAIgASgLMh8uYm1wcm90by5sYW5kc2Vlci5Mb2NhbGlzZUVycm9ySAASNgoIcmVzcG9uc2UYAyABKAsyIi5ibXByb3RvLmxhbmRzZWVyLkxvY2FsaXNlUmVzcG9uc2VIAEIICgZyZXN1bHQicQoVTG9jYXRpb25Fc3RpbWF0ZUltYWdlEgoKAnRzGAEgASgEEhEKCWltYWdlRGF0YRgCIAEoDBI5ChRub3J0aERlZ3JlZXNFc3RpbWF0ZRgDIAEoCzIbLmdvb2dsZS5wcm90b2J1Zi5GbG9hdFZhbHVlIrYBChdMb2NhdGlvbkVzdGltYXRlUmVxdWVzdBIKCgJ0cxgBIAEoBBIqCgNncHMYAiABKAsyHS5ibXByb3RvLmxhbmRzZWVyLkdwc0xvZ0VudHJ5EjcKBmltYWdlcxgDIAMoCzInLmJtcHJvdG8ubGFuZHNlZXIuTG9jYXRpb25Fc3RpbWF0ZUltYWdlEioKA2VudhgEIAEoDjIdLmJtcHJvdG8ubGFuZHNlZXIuRW52aXJvbm1lbnQimAEKEExvY2F0aW9uRXN0aW1hdGUSCgoCdHMYASABKAQSEgoKbG9jYXRpb25JZBgCIAEoCRIPCgdmbG9vcklkGAMgASgJEhAKCGxhdGl0dWRlGAQgASgBEhEKCWxvbmdpdHVkZRgFIAEoARIWCg5oZWFkaW5nRGVncmVlcxgGIAEoAhIWCg5hY2N1cmFjeU1ldGVycxgHIAEoAiKeAQoYTG9jYXRpb25Fc3RpbWF0ZVJlc3BvbnNlEjwKEGxvY2F0aW9uRXN0aW1hdGUYASABKAsyIi5ibXByb3RvLmxhbmRzZWVyLkxvY2F0aW9uRXN0aW1hdGUSNAoMbG9jYWxpc2VUeXBlGAIgASgOMh4uYm1wcm90by5sYW5kc2Vlci5Mb2NhbGlzZVR5cGUSDgoGZXJyb3JzGAMgAygJKlcKCkRldmljZVR5cGUSGwoXREVWSUNFX1RZUEVfVU5TUEVDSUZJRUQQABITCg9ERVZJQ0VfVFlQRV9JT1MQARIXChNERVZJQ0VfVFlQRV9BTkRST0lEEAIqngEKC0Vudmlyb25tZW50EhsKF0VOVklST05NRU5UX1VOU1BFQ0lGSUVEEAASGwoXRU5WSVJPTk1FTlRfREVWX1BSRVZJRVcQARIaChZFTlZJUk9OTUVOVF9ERVZfUFVCTElDEAISHAoYRU5WSVJPTk1FTlRfUFJPRF9QUkVWSUVXEAMSGwoXRU5WSVJPTk1FTlRfUFJPRF9QVUJMSUMQBCpWCg1UcmFuc2Zvcm1UeXBlEh4KGlRSQU5TRk9STV9UWVBFX1VOU1BFQ0lGSUVEEAASJQohVFJBTlNGT1JNX1RZUEVfTk9fUVVBVEVSTklPTl9GTElQEAEqgQEKDExvY2FsaXNlVHlwZRIdChlMT0NBTElTRV9UWVBFX1VOU1BFQ0lGSUVEEAASGAoUTE9DQUxJU0VfVFlQRV9GQUlMRUQQARIVChFMT0NBTElTRV9UWVBFX0dQUxACEiEKHUxPQ0FMSVNFX1RZUEVfVklTVUFMX0VTVElNQVRFEAMy0wUKEkxhbmRzZWVyQVBJU2VydmljZRJQCgdoZWFsdGh6EiAuYm1wcm90by5sYW5kc2Vlci5IZWFsdGh6UmVxdWVzdBohLmJtcHJvdG8ubGFuZHNlZXIuSGVhbHRoelJlc3BvbnNlIgASUwoIbG9jYWxpc2USIS5ibXByb3RvLmxhbmRzZWVyLkxvY2FsaXNlUmVxdWVzdBoiLmJtcHJvdG8ubGFuZHNlZXIuTG9jYWxpc2VSZXNwb25zZSIAEmIKDWxvY2FsaXNlQXN5bmMSJi5ibXByb3RvLmxhbmRzZWVyLkxvY2FsaXNlQXN5bmNSZXF1ZXN0GicuYm1wcm90by5sYW5kc2Vlci5Mb2NhbGlzZUFzeW5jUmVzcG9uc2UiABJiCg1zdGFydExvY2FsaXNlEiYuYm1wcm90by5sYW5kc2Vlci5TdGFydExvY2FsaXNlUmVxdWVzdBonLmJtcHJvdG8ubGFuZHNlZXIuU3RhcnRMb2NhbGlzZVJlc3BvbnNlIgASbgoRZ2V0TG9jYWxpc2VSZXN1bHQSKi5ibXByb3RvLmxhbmRzZWVyLkdldExvY2FsaXNlUmVzdWx0UmVxdWVzdBorLmJtcHJvdG8ubGFuZHNlZXIuR2V0TG9jYWxpc2VSZXN1bHRSZXNwb25zZSIAEmsKEGxvY2F0aW9uRXN0aW1hdGUSKS5ibXByb3RvLmxhbmRzZWVyLkxvY2F0aW9uRXN0aW1hdGVSZXF1ZXN0GiouYm1wcm90by5sYW5kc2Vlci5Mb2NhdGlvbkVzdGltYXRlUmVzcG9uc2UiABJxChJnZXRGbG9vclRyYW5zZm9ybXMSKy5ibXByb3RvLmxhbmRzZWVyLkdldEZsb29yVHJhbnNmb3Jtc1JlcXVlc3QaLC5ibXByb3RvLmxhbmRzZWVyLkdldEZsb29yVHJhbnNmb3Jtc1Jlc3BvbnNlIgBCJVojZ2l0aHViLmNvbS9iaW5kaW1hcHMvbGFuZHNlZXItZ29idWZiBnByb3RvMw", [file_google_protobuf_wrappers]);
26
+ var EnvironmentSchema = /* @__PURE__ */ enumDesc(file_bmproto_landseer_landseer, 1);
27
+ var Environment = /* @__PURE__ */ tsEnum(EnvironmentSchema);
28
+ var LocaliseTypeSchema = /* @__PURE__ */ enumDesc(file_bmproto_landseer_landseer, 3);
29
+ var LocaliseType = /* @__PURE__ */ tsEnum(LocaliseTypeSchema);
30
+ var LandseerAPIService = /* @__PURE__ */ serviceDesc(file_bmproto_landseer_landseer, 0);
31
+
32
+ // src/index.ts
33
+ import { createClient } from "@connectrpc/connect";
34
+ import { createGrpcWebTransport } from "@connectrpc/connect-web";
35
+
36
+ // src/image-selection.ts
37
+ var getImageThumbnail = (blob, size = 16) => __async(null, null, function* () {
38
+ const bitmap = yield createImageBitmap(blob, { resizeWidth: size, resizeHeight: size, resizeQuality: "low" });
39
+ const canvas = document.createElement("canvas");
40
+ canvas.width = size;
41
+ canvas.height = size;
42
+ const ctx = canvas.getContext("2d");
43
+ if (!ctx) throw new Error("Could not get canvas context");
44
+ ctx.drawImage(bitmap, 0, 0);
45
+ bitmap.close();
46
+ const imageData = ctx.getImageData(0, 0, size, size).data;
47
+ const grayscale = new Uint8Array(size * size);
48
+ for (let i = 0; i < imageData.length; i += 4) {
49
+ grayscale[i / 4] = (imageData[i] + imageData[i + 1] + imageData[i + 2]) / 3;
50
+ }
51
+ return grayscale;
52
+ });
53
+ var calculateDistance = (a, b) => {
54
+ let sum = 0;
55
+ for (let i = 0; i < a.length; i++) {
56
+ const diff = a[i] - b[i];
57
+ sum += diff * diff;
58
+ }
59
+ return Math.sqrt(sum);
60
+ };
61
+ var selectDiverseFrames = (frames, count) => __async(null, null, function* () {
62
+ if (frames.length <= count) return frames;
63
+ const thumbnails = yield Promise.all(frames.map((f) => getImageThumbnail(f.imageData)));
64
+ const selectedIndices = /* @__PURE__ */ new Set([0]);
65
+ while (selectedIndices.size < count) {
66
+ let maxMinDist = -1;
67
+ let bestCandidateIdx = -1;
68
+ for (let i = 0; i < frames.length; i++) {
69
+ if (selectedIndices.has(i)) continue;
70
+ let minDist = Infinity;
71
+ for (const selectedIdx of selectedIndices) {
72
+ const dist = calculateDistance(thumbnails[i], thumbnails[selectedIdx]);
73
+ if (dist < minDist) minDist = dist;
74
+ }
75
+ if (minDist > maxMinDist) {
76
+ maxMinDist = minDist;
77
+ bestCandidateIdx = i;
78
+ }
79
+ }
80
+ if (bestCandidateIdx === -1) break;
81
+ selectedIndices.add(bestCandidateIdx);
82
+ }
83
+ return [...selectedIndices].sort((a, b) => a - b).map((idx) => frames[idx]);
84
+ });
85
+
86
+ // src/index.ts
87
+ var MIN_LOCALISATION_IMAGE_COUNT = 6;
88
+ var USE_DIVERSE_FRAME_SELECTION = false;
89
+ var HyperlocalEnvironment = /* @__PURE__ */ ((HyperlocalEnvironment2) => {
90
+ HyperlocalEnvironment2[HyperlocalEnvironment2["UNSPECIFIED"] = 0] = "UNSPECIFIED";
91
+ HyperlocalEnvironment2[HyperlocalEnvironment2["DEV_PREVIEW"] = 1] = "DEV_PREVIEW";
92
+ HyperlocalEnvironment2[HyperlocalEnvironment2["DEV_PUBLIC"] = 2] = "DEV_PUBLIC";
93
+ HyperlocalEnvironment2[HyperlocalEnvironment2["PROD_PREVIEW"] = 3] = "PROD_PREVIEW";
94
+ HyperlocalEnvironment2[HyperlocalEnvironment2["PROD_PUBLIC"] = 4] = "PROD_PUBLIC";
95
+ return HyperlocalEnvironment2;
96
+ })(HyperlocalEnvironment || {});
97
+ var toLandseerEnv = (env) => {
98
+ const map = {
99
+ [0 /* UNSPECIFIED */]: Environment.UNSPECIFIED,
100
+ [1 /* DEV_PREVIEW */]: Environment.DEV_PREVIEW,
101
+ [2 /* DEV_PUBLIC */]: Environment.DEV_PUBLIC,
102
+ [3 /* PROD_PREVIEW */]: Environment.PROD_PREVIEW,
103
+ [4 /* PROD_PUBLIC */]: Environment.PROD_PUBLIC
104
+ };
105
+ return map[env];
106
+ };
107
+ var blobToUint8Array = (blob) => __async(null, null, function* () {
108
+ const arrayBuffer = yield blob.arrayBuffer();
109
+ return new Uint8Array(arrayBuffer);
110
+ });
111
+ var getBaseUrl = (environment) => environment === 4 /* PROD_PUBLIC */ || environment === 3 /* PROD_PREVIEW */ ? "https://landseer.bindimaps.app" : "https://landseer.bindi.dev";
112
+ var isValidGpsPosition = (gpsPosition) => {
113
+ if (!gpsPosition || typeof gpsPosition !== "object") return false;
114
+ const maybeGps = gpsPosition;
115
+ return Number.isFinite(maybeGps.latitude) && Number.isFinite(maybeGps.longitude) && Number(maybeGps.latitude) >= -90 && Number(maybeGps.latitude) <= 90 && Number(maybeGps.longitude) >= -180 && Number(maybeGps.longitude) <= 180;
116
+ };
117
+ var normalizeHeadingDegrees = (headingDegrees) => Number.isFinite(headingDegrees) ? Number(headingDegrees) : 0;
118
+ var validateInputs = (frames, locationId, gpsPosition) => {
119
+ if (typeof locationId !== "string" || locationId.trim().length === 0) {
120
+ return {
121
+ type: "error",
122
+ code: "invalid_input",
123
+ message: "locationId must be a non-empty string."
124
+ };
125
+ }
126
+ if (!isValidGpsPosition(gpsPosition)) {
127
+ return {
128
+ type: "error",
129
+ code: "invalid_input",
130
+ message: "gpsPosition coordinates are out of range (lat: -90..90, lng: -180..180)."
131
+ };
132
+ }
133
+ if (!Array.isArray(frames)) {
134
+ return {
135
+ type: "error",
136
+ code: "invalid_input",
137
+ message: "frames must be an array."
138
+ };
139
+ }
140
+ if (frames.length < MIN_LOCALISATION_IMAGE_COUNT) {
141
+ return {
142
+ type: "error",
143
+ code: "insufficient_frames",
144
+ message: `At least ${MIN_LOCALISATION_IMAGE_COUNT} frames are required for localisation.`,
145
+ details: `Received ${frames.length} frames, required ${MIN_LOCALISATION_IMAGE_COUNT}.`
146
+ };
147
+ }
148
+ return null;
149
+ };
150
+ var buildRequest = (frames, gpsPosition, environment) => __async(null, null, function* () {
151
+ const selectedFrames = USE_DIVERSE_FRAME_SELECTION ? yield selectDiverseFrames(frames, MIN_LOCALISATION_IMAGE_COUNT) : frames;
152
+ const now = /* @__PURE__ */ new Date();
153
+ const ts = BigInt(now.getTime());
154
+ const gps = {
155
+ gpsLocation: {
156
+ lat: gpsPosition.latitude,
157
+ lon: gpsPosition.longitude,
158
+ horizontalAccuracyMeters: 0,
159
+ mslAltitudeMeters: 0,
160
+ verticalAccuracyMeters: 0,
161
+ $typeName: "bmproto.landseer.LatLon"
162
+ },
163
+ ts,
164
+ $typeName: "bmproto.landseer.GpsLogEntry"
165
+ };
166
+ const images = yield Promise.all(
167
+ selectedFrames.map((img) => __async(null, null, function* () {
168
+ return {
169
+ ts: BigInt(img.timestamp.getTime()),
170
+ imageData: yield blobToUint8Array(img.imageData),
171
+ $typeName: "bmproto.landseer.LocationEstimateImage"
172
+ };
173
+ }))
174
+ );
175
+ return {
176
+ ts,
177
+ gps,
178
+ images,
179
+ env: toLandseerEnv(environment),
180
+ $typeName: "bmproto.landseer.LocationEstimateRequest"
181
+ };
182
+ });
183
+ var interpretResponse = (response, locationId) => {
184
+ var _a;
185
+ if (response.errors.length > 0 || response.localiseType === LocaliseType.FAILED || !response.locationEstimate || !response.locationEstimate.floorId || !Number.isFinite(response.locationEstimate.latitude) || !Number.isFinite(response.locationEstimate.longitude)) {
186
+ return {
187
+ type: "error",
188
+ code: "no_match",
189
+ message: response.errors.length > 0 ? response.errors.join(", ") : "Localisation failed \u2014 no match found.",
190
+ details: response.errors.length > 0 ? response.errors.join("; ") : `localiseType=${response.localiseType}`
191
+ };
192
+ }
193
+ if (!response.locationEstimate.locationId || response.locationEstimate.locationId !== locationId) {
194
+ return {
195
+ type: "error",
196
+ code: "location_mismatch",
197
+ message: "Matched a different location than requested.",
198
+ details: `expected=${locationId}, got=${(_a = response.locationEstimate.locationId) != null ? _a : "undefined"}`
199
+ };
200
+ }
201
+ return {
202
+ type: "success",
203
+ position: {
204
+ latitude: response.locationEstimate.latitude,
205
+ longitude: response.locationEstimate.longitude
206
+ },
207
+ floorId: response.locationEstimate.floorId,
208
+ headingDegrees: normalizeHeadingDegrees(response.locationEstimate.headingDegrees)
209
+ };
210
+ };
211
+ var estimatePosition = (frames, locationId, gpsPosition, options) => __async(null, null, function* () {
212
+ var _a, _b, _c, _d;
213
+ if (options == null ? void 0 : options.mock) {
214
+ yield new Promise((resolve) => setTimeout(resolve, 500));
215
+ const config = options.mock === true ? {} : options.mock;
216
+ return {
217
+ type: "success",
218
+ position: (_a = config.position) != null ? _a : gpsPosition,
219
+ floorId: (_b = config.floorId) != null ? _b : "mock-floor-1",
220
+ headingDegrees: (_c = config.headingDegrees) != null ? _c : 45
221
+ };
222
+ }
223
+ const validationError = validateInputs(frames, locationId, gpsPosition);
224
+ if (validationError) return validationError;
225
+ const environment = (_d = options == null ? void 0 : options.environment) != null ? _d : 4 /* PROD_PUBLIC */;
226
+ try {
227
+ const req = yield buildRequest(frames, gpsPosition, environment);
228
+ const transport = createGrpcWebTransport({ baseUrl: getBaseUrl(environment) });
229
+ const client = createClient(LandseerAPIService, transport);
230
+ const response = yield client.locationEstimate(req, {
231
+ signal: AbortSignal.timeout(3e4)
232
+ });
233
+ return interpretResponse(response, locationId);
234
+ } catch (err) {
235
+ return {
236
+ type: "error",
237
+ code: "api_error",
238
+ message: "Network or transport error during localisation.",
239
+ details: err instanceof Error ? err.message : String(err)
240
+ };
241
+ }
242
+ });
243
+ export {
244
+ HyperlocalEnvironment,
245
+ MIN_LOCALISATION_IMAGE_COUNT,
246
+ estimatePosition
247
+ };
248
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../node_modules/@bindimaps/landseer/bmproto/landseer/landseer_pb.js","../src/index.ts","../src/image-selection.ts"],"sourcesContent":["// @generated by protoc-gen-es v2.9.0\n// @generated from file bmproto/landseer/landseer.proto (package bmproto.landseer, syntax proto3)\n/* eslint-disable */\n\nimport { enumDesc, fileDesc, messageDesc, serviceDesc, tsEnum } from \"@bufbuild/protobuf/codegenv2\";\nimport { file_google_protobuf_wrappers } from \"@bufbuild/protobuf/wkt\";\n\n/**\n * Describes the file bmproto/landseer/landseer.proto.\n */\nexport const file_bmproto_landseer_landseer = /*@__PURE__*/\n fileDesc(\"Ch9ibXByb3RvL2xhbmRzZWVyL2xhbmRzZWVyLnByb3RvEhBibXByb3RvLmxhbmRzZWVyIhAKDkhlYWx0aHpSZXF1ZXN0IiQKD0hlYWx0aHpSZXNwb25zZRIRCgl0aW1lc3RhbXAYASABKAMiIgoEUG9zZRIMCgR0dmVjGAEgAygCEgwKBHF2ZWMYAiADKAIiSAoRUGluaG9sZUludHJpbnNpY3MSCQoBZhgBIAMoAhIJCgFjGAIgAygCEg4KBmhlaWdodBgDIAEoDRINCgV3aWR0aBgEIAEoDSJDCg1EZWx0YUVuY29kaW5nEg8KB2luaXRpYWwYASABKAESDgoGZGVsdGFzGAIgAygREhEKCXByZWNpc2lvbhgDIAEoDSLUAQoQQWNjZWxlcm9tZXRlckxvZxIzCgp1bml4TWlsbGlzGAEgASgLMh8uYm1wcm90by5sYW5kc2Vlci5EZWx0YUVuY29kaW5nEi0KBHhNczIYAiABKAsyHy5ibXByb3RvLmxhbmRzZWVyLkRlbHRhRW5jb2RpbmcSLQoEeU1zMhgDIAEoCzIfLmJtcHJvdG8ubGFuZHNlZXIuRGVsdGFFbmNvZGluZxItCgR6TXMyGAQgASgLMh8uYm1wcm90by5sYW5kc2Vlci5EZWx0YUVuY29kaW5nIt0BCgdHeXJvTG9nEjMKCnVuaXhNaWxsaXMYASABKAsyHy5ibXByb3RvLmxhbmRzZWVyLkRlbHRhRW5jb2RpbmcSMwoKeFJhZFBlclNlYxgCIAEoCzIfLmJtcHJvdG8ubGFuZHNlZXIuRGVsdGFFbmNvZGluZxIzCgp5UmFkUGVyU2VjGAMgASgLMh8uYm1wcm90by5sYW5kc2Vlci5EZWx0YUVuY29kaW5nEjMKCnpSYWRQZXJTZWMYBCABKAsyHy5ibXByb3RvLmxhbmRzZWVyLkRlbHRhRW5jb2Rpbmci8gEKC1JvdGF0aW9uTG9nEjMKCnVuaXhNaWxsaXMYASABKAsyHy5ibXByb3RvLmxhbmRzZWVyLkRlbHRhRW5jb2RpbmcSKgoBeBgCIAEoCzIfLmJtcHJvdG8ubGFuZHNlZXIuRGVsdGFFbmNvZGluZxIqCgF5GAMgASgLMh8uYm1wcm90by5sYW5kc2Vlci5EZWx0YUVuY29kaW5nEioKAXoYBCABKAsyHy5ibXByb3RvLmxhbmRzZWVyLkRlbHRhRW5jb2RpbmcSKgoBdxgFIAEoCzIfLmJtcHJvdG8ubGFuZHNlZXIuRGVsdGFFbmNvZGluZyLKAwoFRnJhbWUSCgoCdHMYASABKAMSEQoJaW1hZ2VEYXRhGAIgASgMEigKCHNsYW1Qb3NlGAMgASgLMhYuYm1wcm90by5sYW5kc2Vlci5Qb3NlEjcKCmludHJpbnNpY3MYBCABKAsyIy5ibXByb3RvLmxhbmRzZWVyLlBpbmhvbGVJbnRyaW5zaWNzEhgKDG5vcnRoRGVncmVlcxgFIAEoAkICGAESOQoUbm9ydGhEZWdyZWVzRXN0aW1hdGUYBiABKAsyGy5nb29nbGUucHJvdG9idWYuRmxvYXRWYWx1ZRIwCgtwcmVzc3VyZUhQYRgHIAEoCzIbLmdvb2dsZS5wcm90b2J1Zi5GbG9hdFZhbHVlEjYKEHByZWRpY3RlZEZsb29ySWQYCCABKAsyHC5nb29nbGUucHJvdG9idWYuU3RyaW5nVmFsdWUSPQoYcHJlZGljdGVkRmxvb3JDb25maWRlbmNlGAkgASgLMhsuZ29vZ2xlLnByb3RvYnVmLkZsb2F0VmFsdWUSEgoKZnJhbWVJbmRleBgKIAEoDRItCgtncHNMb2NhdGlvbhgLIAEoCzIYLmJtcHJvdG8ubGFuZHNlZXIuTGF0TG9uIrkBCgZMYXRMb24SCwoDbGF0GAEgASgCEgsKA2xvbhgCIAEoAhIgChhob3Jpem9udGFsQWNjdXJhY3lNZXRlcnMYAyABKAISNgoRbXNsQWx0aXR1ZGVNZXRlcnMYBCABKAsyGy5nb29nbGUucHJvdG9idWYuRmxvYXRWYWx1ZRI7ChZ2ZXJ0aWNhbEFjY3VyYWN5TWV0ZXJzGAUgASgLMhsuZ29vZ2xlLnByb3RvYnVmLkZsb2F0VmFsdWUiwAEKCFNsYW1Qb3NlEgoKAnRzGAEgASgEEiQKBHBvc2UYAiABKAsyFi5ibXByb3RvLmxhbmRzZWVyLlBvc2USMQoMbm9ydGhEZWdyZWVzGAMgASgLMhsuZ29vZ2xlLnByb3RvYnVmLkZsb2F0VmFsdWUSMAoLcHJlc3N1cmVIUGEYBCABKAsyGy5nb29nbGUucHJvdG9idWYuRmxvYXRWYWx1ZRIdChVzbGFtQ29vcmRpbmF0ZVNwYWNlSWQYBSABKA0ifgoORGV2aWNlUG9zaXRpb24SCQoBeBgBIAEoAhIJCgF5GAIgASgCEhQKDG5vcnRoRGVncmVlcxgDIAEoAhIgChhob3Jpem9udGFsQWNjdXJhY3lNZXRlcnMYBCABKAISDwoHZmxvb3JJZBgFIAEoCRINCgVsb2NJZBgGIAEoCSJOChFEZXZpY2VQb3NpdGlvbkxvZxIKCgJ0cxgBIAEoBBItCgNwb3MYAiABKAsyIC5ibXByb3RvLmxhbmRzZWVyLkRldmljZVBvc2l0aW9uIkgKC0dwc0xvZ0VudHJ5EgoKAnRzGAEgASgEEi0KC2dwc0xvY2F0aW9uGAIgASgLMhguYm1wcm90by5sYW5kc2Vlci5MYXRMb24i6wUKD0xvY2FsaXNlUmVxdWVzdBIRCglzZXNzaW9uSWQYASABKAkSEwoLc2VxdWVuY2VOdW0YAiABKA0SMAoOZ3BzQ29vcmRpbmF0ZXMYAyABKAsyGC5ibXByb3RvLmxhbmRzZWVyLkxhdExvbhInCgZmcmFtZXMYBCADKAsyFy5ibXByb3RvLmxhbmRzZWVyLkZyYW1lEi0KCXNsYW1Qb3NlcxgFIAMoCzIWLmJtcHJvdG8ubGFuZHNlZXIuUG9zZUICGAESFwoPcGFyZW50U2Vzc2lvbklkGAYgASgJEisKB3NsYW1Mb2cYByADKAsyGi5ibXByb3RvLmxhbmRzZWVyLlNsYW1Qb3NlEjAKCmRldmljZVR5cGUYCCABKA4yHC5ibXByb3RvLmxhbmRzZWVyLkRldmljZVR5cGUSMgoLZW52aXJvbm1lbnQYCSABKA4yHS5ibXByb3RvLmxhbmRzZWVyLkVudmlyb25tZW50Eh0KFXNsYW1Db29yZGluYXRlU3BhY2VJZBgKIAEoDRI+ChFkZXZpY2VQb3NpdGlvbkxvZxgLIAMoCzIjLmJtcHJvdG8ubGFuZHNlZXIuRGV2aWNlUG9zaXRpb25Mb2cSNgoNdHJhbnNmb3JtVHlwZRgMIAEoDjIfLmJtcHJvdG8ubGFuZHNlZXIuVHJhbnNmb3JtVHlwZRItCgZncHNMb2cYDSADKAsyHS5ibXByb3RvLmxhbmRzZWVyLkdwc0xvZ0VudHJ5EjIKBmFjY0xvZxgOIAEoCzIiLmJtcHJvdG8ubGFuZHNlZXIuQWNjZWxlcm9tZXRlckxvZxIqCgdneXJvTG9nGA8gASgLMhkuYm1wcm90by5sYW5kc2Vlci5HeXJvTG9nEi0KBnJvdExvZxgQIAEoCzIdLmJtcHJvdG8ubGFuZHNlZXIuUm90YXRpb25Mb2cSFQoNaXNIZWFsdGhjaGVjaxgRIAEoCBIOCgZpc1NjYW4YEiABKAgiNgoGTWF0cml4EgwKBHJvd3MYASABKA0SDAoEY29scxgCIAEoDRIQCghlbGVtZW50cxgDIAMoASLiAgoQTG9jYWxpc2VSZXNwb25zZRIRCglzZXNzaW9uSWQYASABKAkSEwoLc2VxdWVuY2VOdW0YAiABKA0SKwoLc2xhbUludmVyc2UYAyABKAsyFi5ibXByb3RvLmxhbmRzZWVyLlBvc2USLgoOb3B0aW1pc2VkUG9zZXMYBCADKAsyFi5ibXByb3RvLmxhbmRzZWVyLlBvc2USEgoKbG9jYXRpb25JZBgFIAEoCRIPCgdmbG9vcklkGAYgASgJEi0KC3NsYW1Ub01vZGVsGAcgASgLMhguYm1wcm90by5sYW5kc2Vlci5NYXRyaXgSHgoWY29uZmlkZW5jZVJhZGl1c01ldGVycxgIIAEoAhIdChVzbGFtQ29vcmRpbmF0ZVNwYWNlSWQYCSABKA0SNgoNdHJhbnNmb3JtVHlwZRgKIAEoDjIfLmJtcHJvdG8ubGFuZHNlZXIuVHJhbnNmb3JtVHlwZSKOAQoRU3VibWl0U2NhblJlcXVlc3QSDgoGc2NhbklkGAEgASgJEgoKAnRzGAIgASgDEjAKCmRldmljZVR5cGUYAyABKA4yHC5ibXByb3RvLmxhbmRzZWVyLkRldmljZVR5cGUSKwoKc2NhbkZyYW1lcxgEIAMoCzIXLmJtcHJvdG8ubGFuZHNlZXIuRnJhbWUiJAoSU3VibWl0U2NhblJlc3BvbnNlEg4KBnNjYW5JZBgBIAEoCSJbChlHZXRGbG9vclRyYW5zZm9ybXNSZXF1ZXN0EhIKCmxvY2F0aW9uSWQYASABKAkSKgoDZW52GAIgASgOMh0uYm1wcm90by5sYW5kc2Vlci5FbnZpcm9ubWVudCLnAQoORmxvb3JUcmFuc2Zvcm0SEgoKbG9jYXRpb25JZBgBIAEoCRIPCgdmbG9vcklkGAIgASgJEjIKDG1vZGVsVG9GbG9vchgDIAEoCzIYLmJtcHJvdG8ubGFuZHNlZXIuTWF0cml4QgIYARIXCg9tb2RlbFRvRmxvb3JSb3QYBCADKAISGQoRY29vcmRpbmF0ZVNwYWNlSWQYBSABKAkSDQoFZmxpcFkYBiABKAgSOQoXbW9kZWxUb0Zsb29yVW5jb3JyZWN0ZWQYByABKAsyGC5ibXByb3RvLmxhbmRzZWVyLk1hdHJpeCKXAQoaR2V0Rmxvb3JUcmFuc2Zvcm1zUmVzcG9uc2USEgoKbG9jYXRpb25JZBgBIAEoCRI5Cg9mbG9vclRyYW5zZm9ybXMYAiADKAsyIC5ibXByb3RvLmxhbmRzZWVyLkZsb29yVHJhbnNmb3JtEioKA2VudhgDIAEoDjIdLmJtcHJvdG8ubGFuZHNlZXIuRW52aXJvbm1lbnQiOQoQTG9jYWxpc2VBc3luY0FjaxIRCglzZXNzaW9uSWQYASABKAkSEgoKc2VxdWVuY2VJZBgCIAEoDSKMAQoVTG9jYWxpc2VBc3luY1Jlc3BvbnNlEjEKA2FjaxgBIAEoCzIiLmJtcHJvdG8ubGFuZHNlZXIuTG9jYWxpc2VBc3luY0Fja0gAEjYKCHJlc3BvbnNlGAIgASgLMiIuYm1wcm90by5sYW5kc2Vlci5Mb2NhbGlzZVJlc3BvbnNlSABCCAoGcmVzdWx0IkYKFExvY2FsaXNlQXN5bmNSZXF1ZXN0Ei4KA3JlcRgBIAEoCzIhLmJtcHJvdG8ubGFuZHNlZXIuTG9jYWxpc2VSZXF1ZXN0IkYKFFN0YXJ0TG9jYWxpc2VSZXF1ZXN0Ei4KA3JlcRgBIAEoCzIhLmJtcHJvdG8ubGFuZHNlZXIuTG9jYWxpc2VSZXF1ZXN0Ij4KFVN0YXJ0TG9jYWxpc2VSZXNwb25zZRIRCglzZXNzaW9uSWQYASABKAkSEgoKc2VxdWVuY2VJZBgCIAEoDSK4AQoRTW9kZWxQb3NlRXN0aW1hdGUSDwoHZmxvb3JJZBgBIAEoCRItCg1lc3RpbWF0ZWRQb3NlGAIgASgLMhYuYm1wcm90by5sYW5kc2Vlci5Qb3NlEhYKDmFjY3VyYWN5TWV0ZXJzGAMgASgCEhcKD2FjY3VyYWN5RGVncmVlcxgEIAEoAhISCgpsb2NhdGlvbklkGAUgASgJEh4KFndlaWdodGVkQXZlcmFnZUltYWdlVHMYBiABKAQikgEKD0xvY2FsaXNlUGVuZGluZxIRCglzZXNzaW9uSWQYASABKAkSEgoKc2VxdWVuY2VJZBgCIAEoDRIdChVyZXF1ZXN0UmVjZWl2ZWRVbml4TXMYAyABKAMSOQoMcG9zZUVzdGltYXRlGAQgASgLMiMuYm1wcm90by5sYW5kc2Vlci5Nb2RlbFBvc2VFc3RpbWF0ZSKIAQoNTG9jYWxpc2VFcnJvchIRCglzZXNzaW9uSWQYASABKAkSEgoKc2VxdWVuY2VJZBgCIAEoDRIdChVyZXF1ZXN0UmVjZWl2ZWRVbml4TXMYAyABKAMSGwoTcmVxdWVzdEZhaWxlZFVuaXhNcxgEIAEoAxIUCgxlcnJvck1lc3NhZ2UYBSABKAkiQQoYR2V0TG9jYWxpc2VSZXN1bHRSZXF1ZXN0EhEKCXNlc3Npb25JZBgBIAEoCRISCgpzZXF1ZW5jZUlkGAIgASgNIsUBChlHZXRMb2NhbGlzZVJlc3VsdFJlc3BvbnNlEjQKB3BlbmRpbmcYASABKAsyIS5ibXByb3RvLmxhbmRzZWVyLkxvY2FsaXNlUGVuZGluZ0gAEjAKBWVycm9yGAIgASgLMh8uYm1wcm90by5sYW5kc2Vlci5Mb2NhbGlzZUVycm9ySAASNgoIcmVzcG9uc2UYAyABKAsyIi5ibXByb3RvLmxhbmRzZWVyLkxvY2FsaXNlUmVzcG9uc2VIAEIICgZyZXN1bHQicQoVTG9jYXRpb25Fc3RpbWF0ZUltYWdlEgoKAnRzGAEgASgEEhEKCWltYWdlRGF0YRgCIAEoDBI5ChRub3J0aERlZ3JlZXNFc3RpbWF0ZRgDIAEoCzIbLmdvb2dsZS5wcm90b2J1Zi5GbG9hdFZhbHVlIrYBChdMb2NhdGlvbkVzdGltYXRlUmVxdWVzdBIKCgJ0cxgBIAEoBBIqCgNncHMYAiABKAsyHS5ibXByb3RvLmxhbmRzZWVyLkdwc0xvZ0VudHJ5EjcKBmltYWdlcxgDIAMoCzInLmJtcHJvdG8ubGFuZHNlZXIuTG9jYXRpb25Fc3RpbWF0ZUltYWdlEioKA2VudhgEIAEoDjIdLmJtcHJvdG8ubGFuZHNlZXIuRW52aXJvbm1lbnQimAEKEExvY2F0aW9uRXN0aW1hdGUSCgoCdHMYASABKAQSEgoKbG9jYXRpb25JZBgCIAEoCRIPCgdmbG9vcklkGAMgASgJEhAKCGxhdGl0dWRlGAQgASgBEhEKCWxvbmdpdHVkZRgFIAEoARIWCg5oZWFkaW5nRGVncmVlcxgGIAEoAhIWCg5hY2N1cmFjeU1ldGVycxgHIAEoAiKeAQoYTG9jYXRpb25Fc3RpbWF0ZVJlc3BvbnNlEjwKEGxvY2F0aW9uRXN0aW1hdGUYASABKAsyIi5ibXByb3RvLmxhbmRzZWVyLkxvY2F0aW9uRXN0aW1hdGUSNAoMbG9jYWxpc2VUeXBlGAIgASgOMh4uYm1wcm90by5sYW5kc2Vlci5Mb2NhbGlzZVR5cGUSDgoGZXJyb3JzGAMgAygJKlcKCkRldmljZVR5cGUSGwoXREVWSUNFX1RZUEVfVU5TUEVDSUZJRUQQABITCg9ERVZJQ0VfVFlQRV9JT1MQARIXChNERVZJQ0VfVFlQRV9BTkRST0lEEAIqngEKC0Vudmlyb25tZW50EhsKF0VOVklST05NRU5UX1VOU1BFQ0lGSUVEEAASGwoXRU5WSVJPTk1FTlRfREVWX1BSRVZJRVcQARIaChZFTlZJUk9OTUVOVF9ERVZfUFVCTElDEAISHAoYRU5WSVJPTk1FTlRfUFJPRF9QUkVWSUVXEAMSGwoXRU5WSVJPTk1FTlRfUFJPRF9QVUJMSUMQBCpWCg1UcmFuc2Zvcm1UeXBlEh4KGlRSQU5TRk9STV9UWVBFX1VOU1BFQ0lGSUVEEAASJQohVFJBTlNGT1JNX1RZUEVfTk9fUVVBVEVSTklPTl9GTElQEAEqgQEKDExvY2FsaXNlVHlwZRIdChlMT0NBTElTRV9UWVBFX1VOU1BFQ0lGSUVEEAASGAoUTE9DQUxJU0VfVFlQRV9GQUlMRUQQARIVChFMT0NBTElTRV9UWVBFX0dQUxACEiEKHUxPQ0FMSVNFX1RZUEVfVklTVUFMX0VTVElNQVRFEAMy0wUKEkxhbmRzZWVyQVBJU2VydmljZRJQCgdoZWFsdGh6EiAuYm1wcm90by5sYW5kc2Vlci5IZWFsdGh6UmVxdWVzdBohLmJtcHJvdG8ubGFuZHNlZXIuSGVhbHRoelJlc3BvbnNlIgASUwoIbG9jYWxpc2USIS5ibXByb3RvLmxhbmRzZWVyLkxvY2FsaXNlUmVxdWVzdBoiLmJtcHJvdG8ubGFuZHNlZXIuTG9jYWxpc2VSZXNwb25zZSIAEmIKDWxvY2FsaXNlQXN5bmMSJi5ibXByb3RvLmxhbmRzZWVyLkxvY2FsaXNlQXN5bmNSZXF1ZXN0GicuYm1wcm90by5sYW5kc2Vlci5Mb2NhbGlzZUFzeW5jUmVzcG9uc2UiABJiCg1zdGFydExvY2FsaXNlEiYuYm1wcm90by5sYW5kc2Vlci5TdGFydExvY2FsaXNlUmVxdWVzdBonLmJtcHJvdG8ubGFuZHNlZXIuU3RhcnRMb2NhbGlzZVJlc3BvbnNlIgASbgoRZ2V0TG9jYWxpc2VSZXN1bHQSKi5ibXByb3RvLmxhbmRzZWVyLkdldExvY2FsaXNlUmVzdWx0UmVxdWVzdBorLmJtcHJvdG8ubGFuZHNlZXIuR2V0TG9jYWxpc2VSZXN1bHRSZXNwb25zZSIAEmsKEGxvY2F0aW9uRXN0aW1hdGUSKS5ibXByb3RvLmxhbmRzZWVyLkxvY2F0aW9uRXN0aW1hdGVSZXF1ZXN0GiouYm1wcm90by5sYW5kc2Vlci5Mb2NhdGlvbkVzdGltYXRlUmVzcG9uc2UiABJxChJnZXRGbG9vclRyYW5zZm9ybXMSKy5ibXByb3RvLmxhbmRzZWVyLkdldEZsb29yVHJhbnNmb3Jtc1JlcXVlc3QaLC5ibXByb3RvLmxhbmRzZWVyLkdldEZsb29yVHJhbnNmb3Jtc1Jlc3BvbnNlIgBCJVojZ2l0aHViLmNvbS9iaW5kaW1hcHMvbGFuZHNlZXItZ29idWZiBnByb3RvMw\", [file_google_protobuf_wrappers]);\n\n/**\n * Describes the message bmproto.landseer.HealthzRequest.\n * Use `create(HealthzRequestSchema)` to create a new message.\n */\nexport const HealthzRequestSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 0);\n\n/**\n * Describes the message bmproto.landseer.HealthzResponse.\n * Use `create(HealthzResponseSchema)` to create a new message.\n */\nexport const HealthzResponseSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 1);\n\n/**\n * Describes the message bmproto.landseer.Pose.\n * Use `create(PoseSchema)` to create a new message.\n */\nexport const PoseSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 2);\n\n/**\n * Describes the message bmproto.landseer.PinholeIntrinsics.\n * Use `create(PinholeIntrinsicsSchema)` to create a new message.\n */\nexport const PinholeIntrinsicsSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 3);\n\n/**\n * Describes the message bmproto.landseer.DeltaEncoding.\n * Use `create(DeltaEncodingSchema)` to create a new message.\n */\nexport const DeltaEncodingSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 4);\n\n/**\n * Describes the message bmproto.landseer.AccelerometerLog.\n * Use `create(AccelerometerLogSchema)` to create a new message.\n */\nexport const AccelerometerLogSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 5);\n\n/**\n * Describes the message bmproto.landseer.GyroLog.\n * Use `create(GyroLogSchema)` to create a new message.\n */\nexport const GyroLogSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 6);\n\n/**\n * Describes the message bmproto.landseer.RotationLog.\n * Use `create(RotationLogSchema)` to create a new message.\n */\nexport const RotationLogSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 7);\n\n/**\n * Describes the message bmproto.landseer.Frame.\n * Use `create(FrameSchema)` to create a new message.\n */\nexport const FrameSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 8);\n\n/**\n * Describes the message bmproto.landseer.LatLon.\n * Use `create(LatLonSchema)` to create a new message.\n */\nexport const LatLonSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 9);\n\n/**\n * Describes the message bmproto.landseer.SlamPose.\n * Use `create(SlamPoseSchema)` to create a new message.\n */\nexport const SlamPoseSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 10);\n\n/**\n * Describes the message bmproto.landseer.DevicePosition.\n * Use `create(DevicePositionSchema)` to create a new message.\n */\nexport const DevicePositionSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 11);\n\n/**\n * Describes the message bmproto.landseer.DevicePositionLog.\n * Use `create(DevicePositionLogSchema)` to create a new message.\n */\nexport const DevicePositionLogSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 12);\n\n/**\n * Describes the message bmproto.landseer.GpsLogEntry.\n * Use `create(GpsLogEntrySchema)` to create a new message.\n */\nexport const GpsLogEntrySchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 13);\n\n/**\n * Describes the message bmproto.landseer.LocaliseRequest.\n * Use `create(LocaliseRequestSchema)` to create a new message.\n */\nexport const LocaliseRequestSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 14);\n\n/**\n * Describes the message bmproto.landseer.Matrix.\n * Use `create(MatrixSchema)` to create a new message.\n */\nexport const MatrixSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 15);\n\n/**\n * Describes the message bmproto.landseer.LocaliseResponse.\n * Use `create(LocaliseResponseSchema)` to create a new message.\n */\nexport const LocaliseResponseSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 16);\n\n/**\n * Describes the message bmproto.landseer.SubmitScanRequest.\n * Use `create(SubmitScanRequestSchema)` to create a new message.\n */\nexport const SubmitScanRequestSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 17);\n\n/**\n * Describes the message bmproto.landseer.SubmitScanResponse.\n * Use `create(SubmitScanResponseSchema)` to create a new message.\n */\nexport const SubmitScanResponseSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 18);\n\n/**\n * Describes the message bmproto.landseer.GetFloorTransformsRequest.\n * Use `create(GetFloorTransformsRequestSchema)` to create a new message.\n */\nexport const GetFloorTransformsRequestSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 19);\n\n/**\n * Describes the message bmproto.landseer.FloorTransform.\n * Use `create(FloorTransformSchema)` to create a new message.\n */\nexport const FloorTransformSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 20);\n\n/**\n * Describes the message bmproto.landseer.GetFloorTransformsResponse.\n * Use `create(GetFloorTransformsResponseSchema)` to create a new message.\n */\nexport const GetFloorTransformsResponseSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 21);\n\n/**\n * Describes the message bmproto.landseer.LocaliseAsyncAck.\n * Use `create(LocaliseAsyncAckSchema)` to create a new message.\n */\nexport const LocaliseAsyncAckSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 22);\n\n/**\n * Describes the message bmproto.landseer.LocaliseAsyncResponse.\n * Use `create(LocaliseAsyncResponseSchema)` to create a new message.\n */\nexport const LocaliseAsyncResponseSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 23);\n\n/**\n * Describes the message bmproto.landseer.LocaliseAsyncRequest.\n * Use `create(LocaliseAsyncRequestSchema)` to create a new message.\n */\nexport const LocaliseAsyncRequestSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 24);\n\n/**\n * Describes the message bmproto.landseer.StartLocaliseRequest.\n * Use `create(StartLocaliseRequestSchema)` to create a new message.\n */\nexport const StartLocaliseRequestSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 25);\n\n/**\n * Describes the message bmproto.landseer.StartLocaliseResponse.\n * Use `create(StartLocaliseResponseSchema)` to create a new message.\n */\nexport const StartLocaliseResponseSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 26);\n\n/**\n * Describes the message bmproto.landseer.ModelPoseEstimate.\n * Use `create(ModelPoseEstimateSchema)` to create a new message.\n */\nexport const ModelPoseEstimateSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 27);\n\n/**\n * Describes the message bmproto.landseer.LocalisePending.\n * Use `create(LocalisePendingSchema)` to create a new message.\n */\nexport const LocalisePendingSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 28);\n\n/**\n * Describes the message bmproto.landseer.LocaliseError.\n * Use `create(LocaliseErrorSchema)` to create a new message.\n */\nexport const LocaliseErrorSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 29);\n\n/**\n * Describes the message bmproto.landseer.GetLocaliseResultRequest.\n * Use `create(GetLocaliseResultRequestSchema)` to create a new message.\n */\nexport const GetLocaliseResultRequestSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 30);\n\n/**\n * Describes the message bmproto.landseer.GetLocaliseResultResponse.\n * Use `create(GetLocaliseResultResponseSchema)` to create a new message.\n */\nexport const GetLocaliseResultResponseSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 31);\n\n/**\n * Describes the message bmproto.landseer.LocationEstimateImage.\n * Use `create(LocationEstimateImageSchema)` to create a new message.\n */\nexport const LocationEstimateImageSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 32);\n\n/**\n * Describes the message bmproto.landseer.LocationEstimateRequest.\n * Use `create(LocationEstimateRequestSchema)` to create a new message.\n */\nexport const LocationEstimateRequestSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 33);\n\n/**\n * Describes the message bmproto.landseer.LocationEstimate.\n * Use `create(LocationEstimateSchema)` to create a new message.\n */\nexport const LocationEstimateSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 34);\n\n/**\n * Describes the message bmproto.landseer.LocationEstimateResponse.\n * Use `create(LocationEstimateResponseSchema)` to create a new message.\n */\nexport const LocationEstimateResponseSchema = /*@__PURE__*/\n messageDesc(file_bmproto_landseer_landseer, 35);\n\n/**\n * Describes the enum bmproto.landseer.DeviceType.\n */\nexport const DeviceTypeSchema = /*@__PURE__*/\n enumDesc(file_bmproto_landseer_landseer, 0);\n\n/**\n * @generated from enum bmproto.landseer.DeviceType\n */\nexport const DeviceType = /*@__PURE__*/\n tsEnum(DeviceTypeSchema);\n\n/**\n * Describes the enum bmproto.landseer.Environment.\n */\nexport const EnvironmentSchema = /*@__PURE__*/\n enumDesc(file_bmproto_landseer_landseer, 1);\n\n/**\n * specifies the CMS environment we will use for localisation\n *\n * @generated from enum bmproto.landseer.Environment\n */\nexport const Environment = /*@__PURE__*/\n tsEnum(EnvironmentSchema);\n\n/**\n * Describes the enum bmproto.landseer.TransformType.\n */\nexport const TransformTypeSchema = /*@__PURE__*/\n enumDesc(file_bmproto_landseer_landseer, 2);\n\n/**\n * @generated from enum bmproto.landseer.TransformType\n */\nexport const TransformType = /*@__PURE__*/\n tsEnum(TransformTypeSchema);\n\n/**\n * Describes the enum bmproto.landseer.LocaliseType.\n */\nexport const LocaliseTypeSchema = /*@__PURE__*/\n enumDesc(file_bmproto_landseer_landseer, 3);\n\n/**\n * @generated from enum bmproto.landseer.LocaliseType\n */\nexport const LocaliseType = /*@__PURE__*/\n tsEnum(LocaliseTypeSchema);\n\n/**\n * @generated from service bmproto.landseer.LandseerAPIService\n */\nexport const LandseerAPIService = /*@__PURE__*/\n serviceDesc(file_bmproto_landseer_landseer, 0);\n\n","/**\n * @bindimaps/hyperlocal-web-sdk\n *\n * SDK for image-based indoor localisation using the Landseer API.\n */\n\nimport type {\n GpsLogEntry,\n LocationEstimateImage,\n LocationEstimateRequest,\n LocationEstimateResponse,\n} from \"@bindimaps/landseer/bmproto/landseer/landseer_pb\"\nimport {\n LandseerAPIService,\n Environment as LandseerEnvironment,\n LocaliseType,\n} from \"@bindimaps/landseer/bmproto/landseer/landseer_pb\"\nimport { createClient } from \"@connectrpc/connect\"\nimport { createGrpcWebTransport } from \"@connectrpc/connect-web\"\nimport { selectDiverseFrames } from \"./image-selection\"\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type GeoCoordinate = {\n latitude: number\n longitude: number\n}\n\nexport type CapturedFrame = {\n timestamp: Date\n imageData: Blob\n}\n\nexport type PositionEstimate = {\n type: \"success\"\n position: GeoCoordinate\n floorId: string\n headingDegrees: number\n}\n\nexport type PositionErrorCode =\n | \"invalid_input\"\n | \"insufficient_frames\"\n | \"no_match\"\n | \"location_mismatch\"\n | \"api_error\"\n\nexport type PositionError = {\n type: \"error\"\n code: PositionErrorCode\n message: string\n details?: string\n}\n\nexport type PositionResult = PositionEstimate | PositionError\n\nexport type MockPositionConfig =\n | true\n | {\n position?: GeoCoordinate\n floorId?: string\n headingDegrees?: number\n }\n\nexport type EstimatePositionOptions = {\n environment?: HyperlocalEnvironment\n mock?: MockPositionConfig\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nexport const MIN_LOCALISATION_IMAGE_COUNT = 6\nconst USE_DIVERSE_FRAME_SELECTION = false\n\n// Prefixed to avoid clashing with other `Environment` enums of the same name\nexport enum HyperlocalEnvironment {\n UNSPECIFIED = 0,\n DEV_PREVIEW = 1,\n DEV_PUBLIC = 2,\n PROD_PREVIEW = 3,\n PROD_PUBLIC = 4,\n}\n\n// ============================================================================\n// Internal Helpers\n// ============================================================================\n\nconst toLandseerEnv = (env: HyperlocalEnvironment): LandseerEnvironment => {\n const map: Record<HyperlocalEnvironment, LandseerEnvironment> = {\n [HyperlocalEnvironment.UNSPECIFIED]: LandseerEnvironment.UNSPECIFIED,\n [HyperlocalEnvironment.DEV_PREVIEW]: LandseerEnvironment.DEV_PREVIEW,\n [HyperlocalEnvironment.DEV_PUBLIC]: LandseerEnvironment.DEV_PUBLIC,\n [HyperlocalEnvironment.PROD_PREVIEW]: LandseerEnvironment.PROD_PREVIEW,\n [HyperlocalEnvironment.PROD_PUBLIC]: LandseerEnvironment.PROD_PUBLIC,\n }\n return map[env]\n}\n\nconst blobToUint8Array = async (blob: Blob): Promise<Uint8Array> => {\n const arrayBuffer = await blob.arrayBuffer()\n return new Uint8Array(arrayBuffer)\n}\n\nconst getBaseUrl = (environment: HyperlocalEnvironment): string =>\n (environment === HyperlocalEnvironment.PROD_PUBLIC || environment === HyperlocalEnvironment.PROD_PREVIEW)\n ? \"https://landseer.bindimaps.app\"\n : \"https://landseer.bindi.dev\"\n\nconst isValidGpsPosition = (gpsPosition: unknown): gpsPosition is GeoCoordinate => {\n if (!gpsPosition || typeof gpsPosition !== \"object\") return false\n const maybeGps = gpsPosition as { latitude?: unknown, longitude?: unknown }\n\n return (\n Number.isFinite(maybeGps.latitude) &&\n Number.isFinite(maybeGps.longitude) &&\n Number(maybeGps.latitude) >= -90 && Number(maybeGps.latitude) <= 90 &&\n Number(maybeGps.longitude) >= -180 && Number(maybeGps.longitude) <= 180\n )\n}\n\nconst normalizeHeadingDegrees = (headingDegrees: unknown): number =>\n Number.isFinite(headingDegrees) ? Number(headingDegrees) : 0\n\nconst validateInputs = (\n frames: CapturedFrame[],\n locationId: string,\n gpsPosition: GeoCoordinate,\n): PositionError | null => {\n if (typeof locationId !== \"string\" || locationId.trim().length === 0) {\n return {\n type: \"error\",\n code: \"invalid_input\",\n message: \"locationId must be a non-empty string.\",\n }\n }\n\n if (!isValidGpsPosition(gpsPosition)) {\n return {\n type: \"error\",\n code: \"invalid_input\",\n message: \"gpsPosition coordinates are out of range (lat: -90..90, lng: -180..180).\",\n }\n }\n\n if (!Array.isArray(frames)) {\n return {\n type: \"error\",\n code: \"invalid_input\",\n message: \"frames must be an array.\",\n }\n }\n\n if (frames.length < MIN_LOCALISATION_IMAGE_COUNT) {\n return {\n type: \"error\",\n code: \"insufficient_frames\",\n message: `At least ${MIN_LOCALISATION_IMAGE_COUNT} frames are required for localisation.`,\n details: `Received ${frames.length} frames, required ${MIN_LOCALISATION_IMAGE_COUNT}.`,\n }\n }\n\n return null\n}\n\nconst buildRequest = async (\n frames: CapturedFrame[],\n gpsPosition: GeoCoordinate,\n environment: HyperlocalEnvironment,\n): Promise<LocationEstimateRequest> => {\n const selectedFrames = USE_DIVERSE_FRAME_SELECTION\n ? await selectDiverseFrames(frames, MIN_LOCALISATION_IMAGE_COUNT)\n : frames\n\n const now = new Date()\n const ts = BigInt(now.getTime())\n\n const gps: GpsLogEntry = {\n gpsLocation: {\n lat: gpsPosition.latitude,\n lon: gpsPosition.longitude,\n horizontalAccuracyMeters: 0,\n mslAltitudeMeters: 0,\n verticalAccuracyMeters: 0,\n $typeName: \"bmproto.landseer.LatLon\",\n },\n ts,\n $typeName: \"bmproto.landseer.GpsLogEntry\",\n }\n\n const images: LocationEstimateImage[] = await Promise.all(\n selectedFrames.map(async (img) => ({\n ts: BigInt(img.timestamp.getTime()),\n imageData: await blobToUint8Array(img.imageData),\n $typeName: \"bmproto.landseer.LocationEstimateImage\",\n }))\n )\n\n return {\n ts,\n gps,\n images,\n env: toLandseerEnv(environment),\n $typeName: \"bmproto.landseer.LocationEstimateRequest\",\n }\n}\n\nconst interpretResponse = (response: LocationEstimateResponse, locationId: string): PositionResult => {\n if (\n response.errors.length > 0 ||\n response.localiseType === LocaliseType.FAILED ||\n !response.locationEstimate ||\n !response.locationEstimate.floorId ||\n !Number.isFinite(response.locationEstimate.latitude) ||\n !Number.isFinite(response.locationEstimate.longitude)\n ) {\n return {\n type: \"error\",\n code: \"no_match\",\n message: response.errors.length > 0 ? response.errors.join(\", \") : \"Localisation failed — no match found.\",\n details: response.errors.length > 0 ? response.errors.join(\"; \") : `localiseType=${response.localiseType}`,\n }\n }\n\n if (!response.locationEstimate.locationId || response.locationEstimate.locationId !== locationId) {\n return {\n type: \"error\",\n code: \"location_mismatch\",\n message: \"Matched a different location than requested.\",\n details: `expected=${locationId}, got=${response.locationEstimate.locationId ?? \"undefined\"}`,\n }\n }\n\n return {\n type: \"success\",\n position: {\n latitude: response.locationEstimate.latitude,\n longitude: response.locationEstimate.longitude,\n },\n floorId: response.locationEstimate.floorId,\n headingDegrees: normalizeHeadingDegrees(response.locationEstimate.headingDegrees),\n }\n}\n\n// ============================================================================\n// API\n// ============================================================================\n\n/**\n * Estimate a user's indoor position from captured camera frames and GPS.\n *\n * @param frames - Captured camera frames (minimum 6)\n * @param locationId - The Bindi location ID to localise within\n * @param gpsPosition - Approximate GPS coordinates\n * @param options - Optional configuration (environment, mock mode)\n */\nexport const estimatePosition = async (\n frames: CapturedFrame[],\n locationId: string,\n gpsPosition: GeoCoordinate,\n options?: EstimatePositionOptions,\n): Promise<PositionResult> => {\n // Mock mode — early return before any validation/protobuf/gRPC\n if (options?.mock) {\n await new Promise(resolve => setTimeout(resolve, 500))\n const config = options.mock === true ? {} : options.mock\n return {\n type: \"success\",\n position: config.position ?? gpsPosition,\n floorId: config.floorId ?? \"mock-floor-1\",\n headingDegrees: config.headingDegrees ?? 45,\n }\n }\n\n const validationError = validateInputs(frames, locationId, gpsPosition)\n if (validationError) return validationError\n\n const environment = options?.environment ?? HyperlocalEnvironment.PROD_PUBLIC\n\n try {\n const req = await buildRequest(frames, gpsPosition, environment)\n const transport = createGrpcWebTransport({ baseUrl: getBaseUrl(environment) })\n const client = createClient(LandseerAPIService, transport)\n const response = await client.locationEstimate(req, {\n signal: AbortSignal.timeout(30_000),\n })\n return interpretResponse(response, locationId)\n } catch (err) {\n return {\n type: \"error\",\n code: \"api_error\",\n message: \"Network or transport error during localisation.\",\n details: err instanceof Error ? err.message : String(err),\n }\n }\n}\n","import type { CapturedFrame } from \"./index\"\n\n/**\n * Downsamples an image to a small grayscale thumbnail for diversity comparison.\n */\nconst getImageThumbnail = async (blob: Blob, size = 16): Promise<Uint8Array> => {\n const bitmap = await createImageBitmap(blob, { resizeWidth: size, resizeHeight: size, resizeQuality: \"low\" })\n const canvas = document.createElement(\"canvas\")\n canvas.width = size\n canvas.height = size\n const ctx = canvas.getContext(\"2d\")\n if (!ctx) throw new Error(\"Could not get canvas context\")\n ctx.drawImage(bitmap, 0, 0)\n bitmap.close()\n const imageData = ctx.getImageData(0, 0, size, size).data\n const grayscale = new Uint8Array(size * size)\n for (let i = 0; i < imageData.length; i += 4) {\n grayscale[i / 4] = (imageData[i] + imageData[i + 1] + imageData[i + 2]) / 3\n }\n return grayscale\n}\n\n/**\n * Calculates the Euclidean distance between two thumbnails.\n */\nconst calculateDistance = (a: Uint8Array, b: Uint8Array): number => {\n let sum = 0\n for (let i = 0; i < a.length; i++) {\n const diff = a[i] - b[i]\n sum += diff * diff\n }\n return Math.sqrt(sum)\n}\n\n/**\n * Selects the most diverse frames from a set of candidates.\n * Uses a greedy \"max-min\" distance algorithm.\n * O(n·k) greedy, suitable for <100 candidates. For larger inputs consider a spatial index.\n */\nexport const selectDiverseFrames = async (\n frames: CapturedFrame[],\n count: number\n): Promise<CapturedFrame[]> => {\n if (frames.length <= count) return frames\n\n const thumbnails = await Promise.all(frames.map(f => getImageThumbnail(f.imageData)))\n const selectedIndices = new Set<number>([0])\n\n while (selectedIndices.size < count) {\n let maxMinDist = -1\n let bestCandidateIdx = -1\n\n for (let i = 0; i < frames.length; i++) {\n if (selectedIndices.has(i)) continue\n\n let minDist = Infinity\n for (const selectedIdx of selectedIndices) {\n const dist = calculateDistance(thumbnails[i], thumbnails[selectedIdx])\n if (dist < minDist) minDist = dist\n }\n\n if (minDist > maxMinDist) {\n maxMinDist = minDist\n bestCandidateIdx = i\n }\n }\n\n if (bestCandidateIdx === -1) break\n selectedIndices.add(bestCandidateIdx)\n }\n\n return [...selectedIndices]\n .sort((a, b) => a - b)\n .map(idx => frames[idx])\n}\n"],"mappings":"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","names":["HyperlocalEnvironment"]}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "//": "@bindimaps/landseer is a devDependency because tsup bundles it into dist — it must NOT be a runtime dependency in the published package",
3
+ "name": "@bindimaps/hyperlocal-web-sdk",
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "description": "BindiMaps Hyperlocal Web SDK for indoor localisation",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "development": "./src/index.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "start": "tsc -w",
23
+ "build": "tsup",
24
+ "type-check": "tsc --noEmit",
25
+ "test": "vitest run",
26
+ "prepublishOnly": "yarn build"
27
+ },
28
+ "keywords": [
29
+ "bindimaps",
30
+ "hyperlocal",
31
+ "indoor-positioning",
32
+ "navigation",
33
+ "sdk"
34
+ ],
35
+ "author": "BindiMaps",
36
+ "license": "MIT",
37
+ "publishConfig": {
38
+ "access": "public",
39
+ "registry": "https://registry.npmjs.org"
40
+ },
41
+ "dependencies": {
42
+ "@bufbuild/protobuf": "^2.9.0",
43
+ "@connectrpc/connect": "^2.1.0",
44
+ "@connectrpc/connect-web": "^2.1.0"
45
+ },
46
+ "devDependencies": {
47
+ "@bindimaps/eslint-config": "workspace:*",
48
+ "@bindimaps/landseer": "^0.0.65",
49
+ "@bindimaps/tsconfig": "workspace:*",
50
+ "tsup": "^8.4.0",
51
+ "typescript": "^5.9.3",
52
+ "vitest": "^2.1.8"
53
+ }
54
+ }
@@ -0,0 +1,75 @@
1
+ import type { CapturedFrame } from "./index"
2
+
3
+ /**
4
+ * Downsamples an image to a small grayscale thumbnail for diversity comparison.
5
+ */
6
+ const getImageThumbnail = async (blob: Blob, size = 16): Promise<Uint8Array> => {
7
+ const bitmap = await createImageBitmap(blob, { resizeWidth: size, resizeHeight: size, resizeQuality: "low" })
8
+ const canvas = document.createElement("canvas")
9
+ canvas.width = size
10
+ canvas.height = size
11
+ const ctx = canvas.getContext("2d")
12
+ if (!ctx) throw new Error("Could not get canvas context")
13
+ ctx.drawImage(bitmap, 0, 0)
14
+ bitmap.close()
15
+ const imageData = ctx.getImageData(0, 0, size, size).data
16
+ const grayscale = new Uint8Array(size * size)
17
+ for (let i = 0; i < imageData.length; i += 4) {
18
+ grayscale[i / 4] = (imageData[i] + imageData[i + 1] + imageData[i + 2]) / 3
19
+ }
20
+ return grayscale
21
+ }
22
+
23
+ /**
24
+ * Calculates the Euclidean distance between two thumbnails.
25
+ */
26
+ const calculateDistance = (a: Uint8Array, b: Uint8Array): number => {
27
+ let sum = 0
28
+ for (let i = 0; i < a.length; i++) {
29
+ const diff = a[i] - b[i]
30
+ sum += diff * diff
31
+ }
32
+ return Math.sqrt(sum)
33
+ }
34
+
35
+ /**
36
+ * Selects the most diverse frames from a set of candidates.
37
+ * Uses a greedy "max-min" distance algorithm.
38
+ * O(n·k) greedy, suitable for <100 candidates. For larger inputs consider a spatial index.
39
+ */
40
+ export const selectDiverseFrames = async (
41
+ frames: CapturedFrame[],
42
+ count: number
43
+ ): Promise<CapturedFrame[]> => {
44
+ if (frames.length <= count) return frames
45
+
46
+ const thumbnails = await Promise.all(frames.map(f => getImageThumbnail(f.imageData)))
47
+ const selectedIndices = new Set<number>([0])
48
+
49
+ while (selectedIndices.size < count) {
50
+ let maxMinDist = -1
51
+ let bestCandidateIdx = -1
52
+
53
+ for (let i = 0; i < frames.length; i++) {
54
+ if (selectedIndices.has(i)) continue
55
+
56
+ let minDist = Infinity
57
+ for (const selectedIdx of selectedIndices) {
58
+ const dist = calculateDistance(thumbnails[i], thumbnails[selectedIdx])
59
+ if (dist < minDist) minDist = dist
60
+ }
61
+
62
+ if (minDist > maxMinDist) {
63
+ maxMinDist = minDist
64
+ bestCandidateIdx = i
65
+ }
66
+ }
67
+
68
+ if (bestCandidateIdx === -1) break
69
+ selectedIndices.add(bestCandidateIdx)
70
+ }
71
+
72
+ return [...selectedIndices]
73
+ .sort((a, b) => a - b)
74
+ .map(idx => frames[idx])
75
+ }
package/src/index.ts ADDED
@@ -0,0 +1,299 @@
1
+ /**
2
+ * @bindimaps/hyperlocal-web-sdk
3
+ *
4
+ * SDK for image-based indoor localisation using the Landseer API.
5
+ */
6
+
7
+ import type {
8
+ GpsLogEntry,
9
+ LocationEstimateImage,
10
+ LocationEstimateRequest,
11
+ LocationEstimateResponse,
12
+ } from "@bindimaps/landseer/bmproto/landseer/landseer_pb"
13
+ import {
14
+ LandseerAPIService,
15
+ Environment as LandseerEnvironment,
16
+ LocaliseType,
17
+ } from "@bindimaps/landseer/bmproto/landseer/landseer_pb"
18
+ import { createClient } from "@connectrpc/connect"
19
+ import { createGrpcWebTransport } from "@connectrpc/connect-web"
20
+ import { selectDiverseFrames } from "./image-selection"
21
+
22
+ // ============================================================================
23
+ // Types
24
+ // ============================================================================
25
+
26
+ export type GeoCoordinate = {
27
+ latitude: number
28
+ longitude: number
29
+ }
30
+
31
+ export type CapturedFrame = {
32
+ timestamp: Date
33
+ imageData: Blob
34
+ }
35
+
36
+ export type PositionEstimate = {
37
+ type: "success"
38
+ position: GeoCoordinate
39
+ floorId: string
40
+ headingDegrees: number
41
+ }
42
+
43
+ export type PositionErrorCode =
44
+ | "invalid_input"
45
+ | "insufficient_frames"
46
+ | "no_match"
47
+ | "location_mismatch"
48
+ | "api_error"
49
+
50
+ export type PositionError = {
51
+ type: "error"
52
+ code: PositionErrorCode
53
+ message: string
54
+ details?: string
55
+ }
56
+
57
+ export type PositionResult = PositionEstimate | PositionError
58
+
59
+ export type MockPositionConfig =
60
+ | true
61
+ | {
62
+ position?: GeoCoordinate
63
+ floorId?: string
64
+ headingDegrees?: number
65
+ }
66
+
67
+ export type EstimatePositionOptions = {
68
+ environment?: HyperlocalEnvironment
69
+ mock?: MockPositionConfig
70
+ }
71
+
72
+ // ============================================================================
73
+ // Constants
74
+ // ============================================================================
75
+
76
+ export const MIN_LOCALISATION_IMAGE_COUNT = 6
77
+ const USE_DIVERSE_FRAME_SELECTION = false
78
+
79
+ // Prefixed to avoid clashing with other `Environment` enums of the same name
80
+ export enum HyperlocalEnvironment {
81
+ UNSPECIFIED = 0,
82
+ DEV_PREVIEW = 1,
83
+ DEV_PUBLIC = 2,
84
+ PROD_PREVIEW = 3,
85
+ PROD_PUBLIC = 4,
86
+ }
87
+
88
+ // ============================================================================
89
+ // Internal Helpers
90
+ // ============================================================================
91
+
92
+ const toLandseerEnv = (env: HyperlocalEnvironment): LandseerEnvironment => {
93
+ const map: Record<HyperlocalEnvironment, LandseerEnvironment> = {
94
+ [HyperlocalEnvironment.UNSPECIFIED]: LandseerEnvironment.UNSPECIFIED,
95
+ [HyperlocalEnvironment.DEV_PREVIEW]: LandseerEnvironment.DEV_PREVIEW,
96
+ [HyperlocalEnvironment.DEV_PUBLIC]: LandseerEnvironment.DEV_PUBLIC,
97
+ [HyperlocalEnvironment.PROD_PREVIEW]: LandseerEnvironment.PROD_PREVIEW,
98
+ [HyperlocalEnvironment.PROD_PUBLIC]: LandseerEnvironment.PROD_PUBLIC,
99
+ }
100
+ return map[env]
101
+ }
102
+
103
+ const blobToUint8Array = async (blob: Blob): Promise<Uint8Array> => {
104
+ const arrayBuffer = await blob.arrayBuffer()
105
+ return new Uint8Array(arrayBuffer)
106
+ }
107
+
108
+ const getBaseUrl = (environment: HyperlocalEnvironment): string =>
109
+ (environment === HyperlocalEnvironment.PROD_PUBLIC || environment === HyperlocalEnvironment.PROD_PREVIEW)
110
+ ? "https://landseer.bindimaps.app"
111
+ : "https://landseer.bindi.dev"
112
+
113
+ const isValidGpsPosition = (gpsPosition: unknown): gpsPosition is GeoCoordinate => {
114
+ if (!gpsPosition || typeof gpsPosition !== "object") return false
115
+ const maybeGps = gpsPosition as { latitude?: unknown, longitude?: unknown }
116
+
117
+ return (
118
+ Number.isFinite(maybeGps.latitude) &&
119
+ Number.isFinite(maybeGps.longitude) &&
120
+ Number(maybeGps.latitude) >= -90 && Number(maybeGps.latitude) <= 90 &&
121
+ Number(maybeGps.longitude) >= -180 && Number(maybeGps.longitude) <= 180
122
+ )
123
+ }
124
+
125
+ const normalizeHeadingDegrees = (headingDegrees: unknown): number =>
126
+ Number.isFinite(headingDegrees) ? Number(headingDegrees) : 0
127
+
128
+ const validateInputs = (
129
+ frames: CapturedFrame[],
130
+ locationId: string,
131
+ gpsPosition: GeoCoordinate,
132
+ ): PositionError | null => {
133
+ if (typeof locationId !== "string" || locationId.trim().length === 0) {
134
+ return {
135
+ type: "error",
136
+ code: "invalid_input",
137
+ message: "locationId must be a non-empty string.",
138
+ }
139
+ }
140
+
141
+ if (!isValidGpsPosition(gpsPosition)) {
142
+ return {
143
+ type: "error",
144
+ code: "invalid_input",
145
+ message: "gpsPosition coordinates are out of range (lat: -90..90, lng: -180..180).",
146
+ }
147
+ }
148
+
149
+ if (!Array.isArray(frames)) {
150
+ return {
151
+ type: "error",
152
+ code: "invalid_input",
153
+ message: "frames must be an array.",
154
+ }
155
+ }
156
+
157
+ if (frames.length < MIN_LOCALISATION_IMAGE_COUNT) {
158
+ return {
159
+ type: "error",
160
+ code: "insufficient_frames",
161
+ message: `At least ${MIN_LOCALISATION_IMAGE_COUNT} frames are required for localisation.`,
162
+ details: `Received ${frames.length} frames, required ${MIN_LOCALISATION_IMAGE_COUNT}.`,
163
+ }
164
+ }
165
+
166
+ return null
167
+ }
168
+
169
+ const buildRequest = async (
170
+ frames: CapturedFrame[],
171
+ gpsPosition: GeoCoordinate,
172
+ environment: HyperlocalEnvironment,
173
+ ): Promise<LocationEstimateRequest> => {
174
+ const selectedFrames = USE_DIVERSE_FRAME_SELECTION
175
+ ? await selectDiverseFrames(frames, MIN_LOCALISATION_IMAGE_COUNT)
176
+ : frames
177
+
178
+ const now = new Date()
179
+ const ts = BigInt(now.getTime())
180
+
181
+ const gps: GpsLogEntry = {
182
+ gpsLocation: {
183
+ lat: gpsPosition.latitude,
184
+ lon: gpsPosition.longitude,
185
+ horizontalAccuracyMeters: 0,
186
+ mslAltitudeMeters: 0,
187
+ verticalAccuracyMeters: 0,
188
+ $typeName: "bmproto.landseer.LatLon",
189
+ },
190
+ ts,
191
+ $typeName: "bmproto.landseer.GpsLogEntry",
192
+ }
193
+
194
+ const images: LocationEstimateImage[] = await Promise.all(
195
+ selectedFrames.map(async (img) => ({
196
+ ts: BigInt(img.timestamp.getTime()),
197
+ imageData: await blobToUint8Array(img.imageData),
198
+ $typeName: "bmproto.landseer.LocationEstimateImage",
199
+ }))
200
+ )
201
+
202
+ return {
203
+ ts,
204
+ gps,
205
+ images,
206
+ env: toLandseerEnv(environment),
207
+ $typeName: "bmproto.landseer.LocationEstimateRequest",
208
+ }
209
+ }
210
+
211
+ const interpretResponse = (response: LocationEstimateResponse, locationId: string): PositionResult => {
212
+ if (
213
+ response.errors.length > 0 ||
214
+ response.localiseType === LocaliseType.FAILED ||
215
+ !response.locationEstimate ||
216
+ !response.locationEstimate.floorId ||
217
+ !Number.isFinite(response.locationEstimate.latitude) ||
218
+ !Number.isFinite(response.locationEstimate.longitude)
219
+ ) {
220
+ return {
221
+ type: "error",
222
+ code: "no_match",
223
+ message: response.errors.length > 0 ? response.errors.join(", ") : "Localisation failed — no match found.",
224
+ details: response.errors.length > 0 ? response.errors.join("; ") : `localiseType=${response.localiseType}`,
225
+ }
226
+ }
227
+
228
+ if (!response.locationEstimate.locationId || response.locationEstimate.locationId !== locationId) {
229
+ return {
230
+ type: "error",
231
+ code: "location_mismatch",
232
+ message: "Matched a different location than requested.",
233
+ details: `expected=${locationId}, got=${response.locationEstimate.locationId ?? "undefined"}`,
234
+ }
235
+ }
236
+
237
+ return {
238
+ type: "success",
239
+ position: {
240
+ latitude: response.locationEstimate.latitude,
241
+ longitude: response.locationEstimate.longitude,
242
+ },
243
+ floorId: response.locationEstimate.floorId,
244
+ headingDegrees: normalizeHeadingDegrees(response.locationEstimate.headingDegrees),
245
+ }
246
+ }
247
+
248
+ // ============================================================================
249
+ // API
250
+ // ============================================================================
251
+
252
+ /**
253
+ * Estimate a user's indoor position from captured camera frames and GPS.
254
+ *
255
+ * @param frames - Captured camera frames (minimum 6)
256
+ * @param locationId - The Bindi location ID to localise within
257
+ * @param gpsPosition - Approximate GPS coordinates
258
+ * @param options - Optional configuration (environment, mock mode)
259
+ */
260
+ export const estimatePosition = async (
261
+ frames: CapturedFrame[],
262
+ locationId: string,
263
+ gpsPosition: GeoCoordinate,
264
+ options?: EstimatePositionOptions,
265
+ ): Promise<PositionResult> => {
266
+ // Mock mode — early return before any validation/protobuf/gRPC
267
+ if (options?.mock) {
268
+ await new Promise(resolve => setTimeout(resolve, 500))
269
+ const config = options.mock === true ? {} : options.mock
270
+ return {
271
+ type: "success",
272
+ position: config.position ?? gpsPosition,
273
+ floorId: config.floorId ?? "mock-floor-1",
274
+ headingDegrees: config.headingDegrees ?? 45,
275
+ }
276
+ }
277
+
278
+ const validationError = validateInputs(frames, locationId, gpsPosition)
279
+ if (validationError) return validationError
280
+
281
+ const environment = options?.environment ?? HyperlocalEnvironment.PROD_PUBLIC
282
+
283
+ try {
284
+ const req = await buildRequest(frames, gpsPosition, environment)
285
+ const transport = createGrpcWebTransport({ baseUrl: getBaseUrl(environment) })
286
+ const client = createClient(LandseerAPIService, transport)
287
+ const response = await client.locationEstimate(req, {
288
+ signal: AbortSignal.timeout(30_000),
289
+ })
290
+ return interpretResponse(response, locationId)
291
+ } catch (err) {
292
+ return {
293
+ type: "error",
294
+ code: "api_error",
295
+ message: "Network or transport error during localisation.",
296
+ details: err instanceof Error ? err.message : String(err),
297
+ }
298
+ }
299
+ }