@holoscript/plugin-geolocation-gis 2.0.1 → 2.0.2

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/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@holoscript/plugin-geolocation-gis",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "main": "src/index.ts",
5
5
  "peerDependencies": {
6
- "@holoscript/core": "8.0.6"
6
+ "@holoscript/core": ">=8.0.0"
7
7
  },
8
8
  "license": "MIT",
9
9
  "scripts": {
10
10
  "test": "vitest run --passWithNoTests",
11
11
  "test:coverage": "vitest run --coverage --passWithNoTests"
12
12
  }
13
- }
13
+ }
@@ -39,8 +39,8 @@ describe('vincentyInverse', () => {
39
39
  it('London → New York distance is in the 5550–5620 km range', () => {
40
40
  // Exact value depends on precise coordinates; WGS-84 ellipsoidal geodesic ≈ 5585 km.
41
41
  // Published spherical estimates vary 5570–5590 km depending on source.
42
- const london = { latDeg: 51.5074, lonDeg: -0.1278 };
43
- const newYork = { latDeg: 40.7128, lonDeg: -74.0060 };
42
+ const london = { latDeg: 51.5074, lonDeg: -0.1278 };
43
+ const newYork = { latDeg: 40.7128, lonDeg: -74.006 };
44
44
  const r = vincentyInverse(london, newYork);
45
45
  expect(r.distanceM / 1000).toBeGreaterThan(5550);
46
46
  expect(r.distanceM / 1000).toBeLessThan(5620);
@@ -49,7 +49,7 @@ describe('vincentyInverse', () => {
49
49
  it('London → New York forward azimuth is roughly NW (~288°)', () => {
50
50
  const r = vincentyInverse(
51
51
  { latDeg: 51.5074, lonDeg: -0.1278 },
52
- { latDeg: 40.7128, lonDeg: -74.0060 },
52
+ { latDeg: 40.7128, lonDeg: -74.006 }
53
53
  );
54
54
  // Initial bearing London → NY is roughly 288° (NW)
55
55
  expect(r.forwardAzimuthDeg).toBeGreaterThan(270);
@@ -111,18 +111,18 @@ describe('llaToECEF and ecefToLLA', () => {
111
111
 
112
112
  it('ECEF roundtrip preserves LLA within 1 mm altitude and 1e-9 deg lat/lon', () => {
113
113
  const original: { latDeg: number; lonDeg: number; altM: number }[] = [
114
- { latDeg: 51.5074, lonDeg: -0.1278, altM: 10 },
115
- { latDeg: 40.7128, lonDeg: -74.0060, altM: 50 },
116
- { latDeg: -33.8688, lonDeg: 151.2093, altM: 40 },
117
- { latDeg: 35.6762, lonDeg: 139.6503, altM: 40 },
118
- { latDeg: -0.1000, lonDeg: -78.4678, altM: 2_850 }, // Quito
114
+ { latDeg: 51.5074, lonDeg: -0.1278, altM: 10 },
115
+ { latDeg: 40.7128, lonDeg: -74.006, altM: 50 },
116
+ { latDeg: -33.8688, lonDeg: 151.2093, altM: 40 },
117
+ { latDeg: 35.6762, lonDeg: 139.6503, altM: 40 },
118
+ { latDeg: -0.1, lonDeg: -78.4678, altM: 2_850 }, // Quito
119
119
  ];
120
120
  for (const lla of original) {
121
- const ecef = llaToECEF(lla);
122
- const back = ecefToLLA(ecef);
121
+ const ecef = llaToECEF(lla);
122
+ const back = ecefToLLA(ecef);
123
123
  expect(Math.abs(back.latDeg - lla.latDeg)).toBeLessThan(1e-9);
124
124
  expect(Math.abs(back.lonDeg - lla.lonDeg)).toBeLessThan(1e-9);
125
- expect(Math.abs(back.altM - lla.altM )).toBeLessThan(0.001); // 1 mm
125
+ expect(Math.abs(back.altM - lla.altM)).toBeLessThan(0.001); // 1 mm
126
126
  }
127
127
  });
128
128
 
@@ -138,8 +138,8 @@ describe('llaToECEF and ecefToLLA', () => {
138
138
  // ─── Great-circle waypoints ───────────────────────────────────────────────────
139
139
 
140
140
  describe('greatCircleWaypoints', () => {
141
- const london = { latDeg: 51.5074, lonDeg: -0.1278 };
142
- const newYork = { latDeg: 40.7128, lonDeg: -74.0060 };
141
+ const london = { latDeg: 51.5074, lonDeg: -0.1278 };
142
+ const newYork = { latDeg: 40.7128, lonDeg: -74.006 };
143
143
 
144
144
  it('returns numIntermediate+2 waypoints (endpoints included)', () => {
145
145
  const route = greatCircleWaypoints(london, newYork, 8);
@@ -149,7 +149,7 @@ describe('greatCircleWaypoints', () => {
149
149
  it('first waypoint matches origin, last matches destination', () => {
150
150
  const route = greatCircleWaypoints(london, newYork, 4);
151
151
  const first = route.waypoints[0];
152
- const last = route.waypoints[route.waypoints.length - 1];
152
+ const last = route.waypoints[route.waypoints.length - 1];
153
153
  expect(first.latDeg).toBeCloseTo(london.latDeg, 4);
154
154
  expect(first.lonDeg).toBeCloseTo(london.lonDeg, 4);
155
155
  expect(last.latDeg).toBeCloseTo(newYork.latDeg, 4);
@@ -165,7 +165,7 @@ describe('greatCircleWaypoints', () => {
165
165
  });
166
166
 
167
167
  it('totalDistanceM matches vincentyInverse result', () => {
168
- const route = greatCircleWaypoints(london, newYork, 4);
168
+ const route = greatCircleWaypoints(london, newYork, 4);
169
169
  const vincenty = vincentyInverse(london, newYork);
170
170
  expect(route.totalDistanceM).toBeCloseTo(vincenty.distanceM, 0);
171
171
  });
@@ -187,10 +187,10 @@ describe('pointInGeofence', () => {
187
187
  const parisFence = {
188
188
  id: 'paris-box',
189
189
  vertices: [
190
- { latDeg: 48.80, lonDeg: 2.25 },
190
+ { latDeg: 48.8, lonDeg: 2.25 },
191
191
  { latDeg: 48.92, lonDeg: 2.25 },
192
192
  { latDeg: 48.92, lonDeg: 2.42 },
193
- { latDeg: 48.80, lonDeg: 2.42 },
193
+ { latDeg: 48.8, lonDeg: 2.42 },
194
194
  ],
195
195
  };
196
196
 
@@ -231,7 +231,9 @@ describe('pointInGeofence', () => {
231
231
  });
232
232
 
233
233
  it('degenerate polygon (< 3 vertices) returns false', () => {
234
- expect(pointInGeofence({ latDeg: 0, lonDeg: 0 }, { id: 'bad', vertices: [{ latDeg: 0, lonDeg: 0 }] })).toBe(false);
234
+ expect(
235
+ pointInGeofence({ latDeg: 0, lonDeg: 0 }, { id: 'bad', vertices: [{ latDeg: 0, lonDeg: 0 }] })
236
+ ).toBe(false);
235
237
  });
236
238
  });
237
239
 
@@ -239,7 +241,10 @@ describe('pointInGeofence', () => {
239
241
 
240
242
  describe('analyzeGeodesy', () => {
241
243
  it('returns converged=true for non-antipodal points', () => {
242
- const r = analyzeGeodesy({ latDeg: 48.8566, lonDeg: 2.3522 }, { latDeg: 35.6762, lonDeg: 139.6503 });
244
+ const r = analyzeGeodesy(
245
+ { latDeg: 48.8566, lonDeg: 2.3522 },
246
+ { latDeg: 35.6762, lonDeg: 139.6503 }
247
+ );
243
248
  expect(r.converged).toBe(true);
244
249
  });
245
250
 
@@ -252,7 +257,10 @@ describe('analyzeGeodesy', () => {
252
257
 
253
258
  describe('buildGeodesyReceipt', () => {
254
259
  it('produces receipt with plugin=geolocation-gis and CAEL event', () => {
255
- const result = analyzeGeodesy({ latDeg: 51.5074, lonDeg: -0.1278 }, { latDeg: 40.7128, lonDeg: -74.0060 });
260
+ const result = analyzeGeodesy(
261
+ { latDeg: 51.5074, lonDeg: -0.1278 },
262
+ { latDeg: 40.7128, lonDeg: -74.006 }
263
+ );
256
264
  const receipt = buildGeodesyReceipt(result);
257
265
  expect(receipt.plugin).toBe('geolocation-gis');
258
266
  expect(receipt.cael.event).toBe('geolocation_gis.geodesy');
@@ -260,21 +268,24 @@ describe('buildGeodesyReceipt', () => {
260
268
  });
261
269
 
262
270
  it('accepted=true for convergent analysis', () => {
263
- const result = analyzeGeodesy({ latDeg: 0, lonDeg: 0 }, { latDeg: 1, lonDeg: 1 });
271
+ const result = analyzeGeodesy({ latDeg: 0, lonDeg: 0 }, { latDeg: 1, lonDeg: 1 });
264
272
  const receipt = buildGeodesyReceipt(result);
265
273
  expect(receipt.acceptance.accepted).toBe(true);
266
274
  expect(receipt.acceptance.violations).toHaveLength(0);
267
275
  });
268
276
 
269
277
  it('resultSummary.distanceKm matches vincenty distance', () => {
270
- const result = analyzeGeodesy({ latDeg: 51.5074, lonDeg: -0.1278 }, { latDeg: 40.7128, lonDeg: -74.0060 });
278
+ const result = analyzeGeodesy(
279
+ { latDeg: 51.5074, lonDeg: -0.1278 },
280
+ { latDeg: 40.7128, lonDeg: -74.006 }
281
+ );
271
282
  const receipt = buildGeodesyReceipt(result);
272
283
  // Receipt distance should match the Vincenty result (within floating-point rounding to 4 dp)
273
284
  expect(receipt.resultSummary.distanceKm).toBeCloseTo(result.vincenty.distanceM / 1000, 1);
274
285
  });
275
286
 
276
287
  it('uses provided runId', () => {
277
- const result = analyzeGeodesy({ latDeg: 0, lonDeg: 0 }, { latDeg: 10, lonDeg: 10 });
288
+ const result = analyzeGeodesy({ latDeg: 0, lonDeg: 0 }, { latDeg: 10, lonDeg: 10 });
278
289
  const receipt = buildGeodesyReceipt(result, { runId: 'geo-run-1' });
279
290
  expect(receipt.runId).toBe('geo-run-1');
280
291
  });
package/src/geodesy.ts CHANGED
@@ -14,15 +14,12 @@
14
14
  * WGS-84: NIMA TR8350.2 (3rd ed., 2000)
15
15
  */
16
16
 
17
- import {
18
- DOMAIN_SIMULATION_RECEIPT_SCHEMA,
19
- buildDomainSimulationReceipt,
20
- } from '@holoscript/core';
17
+ import { DOMAIN_SIMULATION_RECEIPT_SCHEMA, buildDomainSimulationReceipt } from '@holoscript/core';
21
18
 
22
19
  // ─── WGS-84 constants ─────────────────────────────────────────────────────────
23
- const WGS84_A = 6_378_137.0; // semi-major axis (m)
24
- const WGS84_F = 1 / 298.257_223_563; // flattening
25
- const WGS84_B = WGS84_A * (1 - WGS84_F); // semi-minor axis (m)
20
+ const WGS84_A = 6_378_137.0; // semi-major axis (m)
21
+ const WGS84_F = 1 / 298.257_223_563; // flattening
22
+ const WGS84_B = WGS84_A * (1 - WGS84_F); // semi-minor axis (m)
26
23
  const WGS84_E2 = 2 * WGS84_F - WGS84_F ** 2; // first eccentricity squared
27
24
  // second eccentricity squared
28
25
  const WGS84_EP2 = WGS84_E2 / (1 - WGS84_E2);
@@ -31,9 +28,9 @@ const WGS84_EP2 = WGS84_E2 / (1 - WGS84_E2);
31
28
 
32
29
  /** Geodetic coordinates (WGS-84). Lat/lon in decimal degrees, altitude in metres. */
33
30
  export interface LatLonAlt {
34
- latDeg: number; // −90 … +90
35
- lonDeg: number; // −180 … +180
36
- altM: number; // metres above WGS-84 ellipsoid
31
+ latDeg: number; // −90 … +90
32
+ lonDeg: number; // −180 … +180
33
+ altM: number; // metres above WGS-84 ellipsoid
37
34
  }
38
35
 
39
36
  /** Earth-Centered Earth-Fixed Cartesian coordinates (metres). */
@@ -45,47 +42,47 @@ export interface ECEF {
45
42
 
46
43
  /** Geofence defined by an ordered list of vertices forming a closed polygon. */
47
44
  export interface GeofencePolygon {
48
- id: string;
45
+ id: string;
49
46
  vertices: { latDeg: number; lonDeg: number }[];
50
47
  }
51
48
 
52
49
  /** Result of Vincenty inverse computation. */
53
50
  export interface VincentyResult {
54
- distanceM: number; // geodesic distance (m)
51
+ distanceM: number; // geodesic distance (m)
55
52
  forwardAzimuthDeg: number; // bearing from p1 → p2 (deg, 0=N, 90=E)
56
- backAzimuthDeg: number; // bearing from p2 → p1 (deg)
53
+ backAzimuthDeg: number; // bearing from p2 → p1 (deg)
57
54
  /** true if points are antipodal (distance undefined within machine epsilon) */
58
- antipodal: boolean;
55
+ antipodal: boolean;
59
56
  }
60
57
 
61
58
  /** A sequence of waypoints along a great-circle path. */
62
59
  export interface GreatCircleRoute {
63
- origin: { latDeg: number; lonDeg: number };
60
+ origin: { latDeg: number; lonDeg: number };
64
61
  destination: { latDeg: number; lonDeg: number };
65
- waypoints: { latDeg: number; lonDeg: number; fractionOfTotal: number }[];
62
+ waypoints: { latDeg: number; lonDeg: number; fractionOfTotal: number }[];
66
63
  totalDistanceM: number;
67
64
  }
68
65
 
69
66
  /** Combined result from a full geodesy analysis session. */
70
67
  export interface GeodesyAnalysisResult {
71
- vincenty: VincentyResult;
72
- route: GreatCircleRoute;
68
+ vincenty: VincentyResult;
69
+ route: GreatCircleRoute;
73
70
  ecefOrigin: ECEF;
74
- ecefDest: ECEF;
75
- converged: boolean;
71
+ ecefDest: ECEF;
72
+ converged: boolean;
76
73
  }
77
74
 
78
75
  export interface GeodesyReceipt {
79
- plugin: string;
80
- runId: string;
81
- payloadHash: string;
76
+ plugin: string;
77
+ runId: string;
78
+ payloadHash: string;
82
79
  hashAlgorithm: string;
83
- cael: { event: string; schemaVersion: string; ts: string };
84
- acceptance: { accepted: boolean; violations: string[] };
80
+ cael: { event: string; schemaVersion: string; ts: string };
81
+ acceptance: { accepted: boolean; violations: string[] };
85
82
  resultSummary: {
86
- distanceKm: number;
87
- forwardAzimuthDeg: number;
88
- waypointCount: number;
83
+ distanceKm: number;
84
+ forwardAzimuthDeg: number;
85
+ waypointCount: number;
89
86
  };
90
87
  }
91
88
 
@@ -94,9 +91,15 @@ export interface GeodesyReceipt {
94
91
  const DEG = Math.PI / 180;
95
92
  const RAD = 180 / Math.PI;
96
93
 
97
- function toRad(deg: number): number { return deg * DEG; }
98
- function toDeg(rad: number): number { return rad * RAD; }
99
- function normalizeDeg(d: number): number { return ((d % 360) + 360) % 360; }
94
+ function toRad(deg: number): number {
95
+ return deg * DEG;
96
+ }
97
+ function toDeg(rad: number): number {
98
+ return rad * RAD;
99
+ }
100
+ function normalizeDeg(d: number): number {
101
+ return ((d % 360) + 360) % 360;
102
+ }
100
103
 
101
104
  /** Prime vertical radius of curvature N(φ) */
102
105
  function primeVerticalRadius(sinPhi: number): number {
@@ -112,17 +115,19 @@ function primeVerticalRadius(sinPhi: number): number {
112
115
  */
113
116
  export function vincentyInverse(
114
117
  p1: { latDeg: number; lonDeg: number },
115
- p2: { latDeg: number; lonDeg: number },
118
+ p2: { latDeg: number; lonDeg: number }
116
119
  ): VincentyResult {
117
120
  const phi1 = toRad(p1.latDeg);
118
121
  const phi2 = toRad(p2.latDeg);
119
- const L = toRad(p2.lonDeg - p1.lonDeg);
122
+ const L = toRad(p2.lonDeg - p1.lonDeg);
120
123
 
121
124
  const U1 = Math.atan((1 - WGS84_F) * Math.tan(phi1));
122
125
  const U2 = Math.atan((1 - WGS84_F) * Math.tan(phi2));
123
126
 
124
- const sinU1 = Math.sin(U1), cosU1 = Math.cos(U1);
125
- const sinU2 = Math.sin(U2), cosU2 = Math.cos(U2);
127
+ const sinU1 = Math.sin(U1),
128
+ cosU1 = Math.cos(U1);
129
+ const sinU2 = Math.sin(U2),
130
+ cosU2 = Math.cos(U2);
126
131
 
127
132
  let lambda = L;
128
133
  let lambdaP: number;
@@ -137,8 +142,7 @@ export function vincentyInverse(
137
142
  cosLambda = Math.cos(lambda);
138
143
 
139
144
  sinSigma = Math.sqrt(
140
- (cosU2 * sinLambda) ** 2 +
141
- (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) ** 2,
145
+ (cosU2 * sinLambda) ** 2 + (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) ** 2
142
146
  );
143
147
 
144
148
  if (sinSigma === 0) {
@@ -146,35 +150,37 @@ export function vincentyInverse(
146
150
  return { distanceM: 0, forwardAzimuthDeg: 0, backAzimuthDeg: 0, antipodal: false };
147
151
  }
148
152
 
149
- cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda;
150
- sigma = Math.atan2(sinSigma, cosSigma);
151
- sinAlpha = (cosU1 * cosU2 * sinLambda) / sinSigma;
153
+ cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda;
154
+ sigma = Math.atan2(sinSigma, cosSigma);
155
+ sinAlpha = (cosU1 * cosU2 * sinLambda) / sinSigma;
152
156
  cos2Alpha = 1 - sinAlpha * sinAlpha;
153
157
 
154
- cos2SigmaM = cos2Alpha !== 0
155
- ? cosSigma - (2 * sinU1 * sinU2) / cos2Alpha
156
- : 0; // equatorial line
158
+ cos2SigmaM = cos2Alpha !== 0 ? cosSigma - (2 * sinU1 * sinU2) / cos2Alpha : 0; // equatorial line
157
159
 
158
160
  C = (WGS84_F / 16) * cos2Alpha * (4 + WGS84_F * (4 - 3 * cos2Alpha));
159
161
 
160
162
  lambdaP = lambda;
161
- lambda = L + (1 - C) * WGS84_F * sinAlpha *
162
- (sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM ** 2)));
163
-
163
+ lambda =
164
+ L +
165
+ (1 - C) *
166
+ WGS84_F *
167
+ sinAlpha *
168
+ (sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM ** 2)));
164
169
  } while (Math.abs(lambda - lambdaP) > 1e-12 && ++iterations < 200);
165
170
 
166
171
  const antipodal = iterations >= 200;
167
172
 
168
- const uSq = cos2Alpha * WGS84_EP2;
173
+ const uSq = cos2Alpha * WGS84_EP2;
169
174
  const A_vc = 1 + (uSq / 16384) * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
170
175
  const B_vc = (uSq / 1024) * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));
171
176
 
172
- const deltaSigma = B_vc * sinSigma * (
173
- cos2SigmaM + (B_vc / 4) * (
174
- cosSigma * (-1 + 2 * cos2SigmaM ** 2) -
175
- (B_vc / 6) * cos2SigmaM * (-3 + 4 * sinSigma ** 2) * (-3 + 4 * cos2SigmaM ** 2)
176
- )
177
- );
177
+ const deltaSigma =
178
+ B_vc *
179
+ sinSigma *
180
+ (cos2SigmaM +
181
+ (B_vc / 4) *
182
+ (cosSigma * (-1 + 2 * cos2SigmaM ** 2) -
183
+ (B_vc / 6) * cos2SigmaM * (-3 + 4 * sinSigma ** 2) * (-3 + 4 * cos2SigmaM ** 2)));
178
184
 
179
185
  const distanceM = WGS84_B * A_vc * (sigma - deltaSigma);
180
186
 
@@ -184,7 +190,7 @@ export function vincentyInverse(
184
190
  return {
185
191
  distanceM,
186
192
  forwardAzimuthDeg: normalizeDeg(toDeg(fwdAz)),
187
- backAzimuthDeg: normalizeDeg(toDeg(bakAz)),
193
+ backAzimuthDeg: normalizeDeg(toDeg(bakAz)),
188
194
  antipodal,
189
195
  };
190
196
  }
@@ -193,13 +199,13 @@ export function vincentyInverse(
193
199
 
194
200
  /** Convert WGS-84 geodetic coordinates to ECEF Cartesian. */
195
201
  export function llaToECEF(lla: LatLonAlt): ECEF {
196
- const phi = toRad(lla.latDeg);
202
+ const phi = toRad(lla.latDeg);
197
203
  const lambda = toRad(lla.lonDeg);
198
- const h = lla.altM;
204
+ const h = lla.altM;
199
205
 
200
206
  const sinPhi = Math.sin(phi);
201
207
  const cosPhi = Math.cos(phi);
202
- const N = primeVerticalRadius(sinPhi);
208
+ const N = primeVerticalRadius(sinPhi);
203
209
 
204
210
  return {
205
211
  xM: (N + h) * cosPhi * Math.cos(lambda),
@@ -214,7 +220,7 @@ export function llaToECEF(lla: LatLonAlt): ECEF {
214
220
  */
215
221
  export function ecefToLLA(ecef: ECEF): LatLonAlt {
216
222
  const { xM, yM, zM } = ecef;
217
- const p = Math.sqrt(xM * xM + yM * yM);
223
+ const p = Math.sqrt(xM * xM + yM * yM);
218
224
  const lambda = Math.atan2(yM, xM);
219
225
 
220
226
  // Initial estimate (Bowring)
@@ -222,20 +228,23 @@ export function ecefToLLA(ecef: ECEF): LatLonAlt {
222
228
 
223
229
  for (let i = 0; i < 10; i++) {
224
230
  const sinPhi = Math.sin(phi);
225
- const N = primeVerticalRadius(sinPhi);
231
+ const N = primeVerticalRadius(sinPhi);
226
232
  const phiNew = Math.atan2(zM + WGS84_E2 * N * sinPhi, p);
227
- if (Math.abs(phiNew - phi) < 1e-12) { phi = phiNew; break; }
233
+ if (Math.abs(phiNew - phi) < 1e-12) {
234
+ phi = phiNew;
235
+ break;
236
+ }
228
237
  phi = phiNew;
229
238
  }
230
239
 
231
240
  const sinPhi = Math.sin(phi);
232
- const N = primeVerticalRadius(sinPhi);
233
- const h = p / Math.cos(phi) - N;
241
+ const N = primeVerticalRadius(sinPhi);
242
+ const h = p / Math.cos(phi) - N;
234
243
 
235
244
  return {
236
245
  latDeg: toDeg(phi),
237
246
  lonDeg: toDeg(lambda),
238
- altM: h,
247
+ altM: h,
239
248
  };
240
249
  }
241
250
 
@@ -249,21 +258,22 @@ export function ecefToLLA(ecef: ECEF): LatLonAlt {
249
258
  export function greatCircleWaypoints(
250
259
  p1: { latDeg: number; lonDeg: number },
251
260
  p2: { latDeg: number; lonDeg: number },
252
- numIntermediate = 8,
261
+ numIntermediate = 8
253
262
  ): GreatCircleRoute {
254
263
  const vincenty = vincentyInverse(p1, p2);
255
264
 
256
265
  // Convert to ECEF unit vectors (ignoring altitude — surface only)
257
266
  const toUnitVec = (p: { latDeg: number; lonDeg: number }) => {
258
- const phi = toRad(p.latDeg), lam = toRad(p.lonDeg);
259
- const cp = Math.cos(phi);
267
+ const phi = toRad(p.latDeg),
268
+ lam = toRad(p.lonDeg);
269
+ const cp = Math.cos(phi);
260
270
  return { x: cp * Math.cos(lam), y: cp * Math.sin(lam), z: Math.sin(phi) };
261
271
  };
262
272
 
263
273
  const v1 = toUnitVec(p1);
264
274
  const v2 = toUnitVec(p2);
265
275
 
266
- const dot = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
276
+ const dot = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
267
277
  const omega = Math.acos(Math.max(-1, Math.min(1, dot))); // angular separation
268
278
 
269
279
  const total = numIntermediate + 2;
@@ -271,7 +281,9 @@ export function greatCircleWaypoints(
271
281
  const t = k / (total - 1);
272
282
  let vx: number, vy: number, vz: number;
273
283
  if (omega < 1e-10) {
274
- vx = v1.x; vy = v1.y; vz = v1.z;
284
+ vx = v1.x;
285
+ vy = v1.y;
286
+ vz = v1.z;
275
287
  } else {
276
288
  const s = Math.sin(omega);
277
289
  const a = Math.sin((1 - t) * omega) / s;
@@ -287,8 +299,8 @@ export function greatCircleWaypoints(
287
299
  });
288
300
 
289
301
  return {
290
- origin: { latDeg: p1.latDeg, lonDeg: p1.lonDeg },
291
- destination: { latDeg: p2.latDeg, lonDeg: p2.lonDeg },
302
+ origin: { latDeg: p1.latDeg, lonDeg: p1.lonDeg },
303
+ destination: { latDeg: p2.latDeg, lonDeg: p2.lonDeg },
292
304
  waypoints,
293
305
  totalDistanceM: vincenty.distanceM,
294
306
  };
@@ -303,36 +315,36 @@ export function greatCircleWaypoints(
303
315
  * Returns true if the point is inside or on the boundary.
304
316
  */
305
317
  export function pointInGeofence(
306
- point: { latDeg: number; lonDeg: number },
307
- polygon: GeofencePolygon,
318
+ point: { latDeg: number; lonDeg: number },
319
+ polygon: GeofencePolygon
308
320
  ): boolean {
309
321
  const verts = polygon.vertices;
310
322
  if (verts.length < 3) return false;
311
323
 
312
324
  // Project all vertices and test point onto flat plane using gnomonic projection
313
325
  // centred on the first vertex (good for polygons up to ~500 km wide).
314
- const phi0 = toRad(verts[0].latDeg);
315
- const lam0 = toRad(verts[0].lonDeg);
326
+ const phi0 = toRad(verts[0].latDeg);
327
+ const lam0 = toRad(verts[0].lonDeg);
316
328
  const cosPhi0 = Math.cos(phi0);
317
329
  const sinPhi0 = Math.sin(phi0);
318
330
 
319
331
  const project = (lat: number, lon: number): [number, number] => {
320
- const phi = toRad(lat), lam = toRad(lon);
321
- const c = sinPhi0 * Math.sin(phi) + cosPhi0 * Math.cos(phi) * Math.cos(lam - lam0);
322
- const x = Math.cos(phi) * Math.sin(lam - lam0) / c;
323
- const y = (cosPhi0 * Math.sin(phi) - sinPhi0 * Math.cos(phi) * Math.cos(lam - lam0)) / c;
332
+ const phi = toRad(lat),
333
+ lam = toRad(lon);
334
+ const c = sinPhi0 * Math.sin(phi) + cosPhi0 * Math.cos(phi) * Math.cos(lam - lam0);
335
+ const x = (Math.cos(phi) * Math.sin(lam - lam0)) / c;
336
+ const y = (cosPhi0 * Math.sin(phi) - sinPhi0 * Math.cos(phi) * Math.cos(lam - lam0)) / c;
324
337
  return [x, y];
325
338
  };
326
339
 
327
340
  const [px, py] = project(point.latDeg, point.lonDeg);
328
- const n = verts.length;
329
- let inside = false;
341
+ const n = verts.length;
342
+ let inside = false;
330
343
 
331
344
  for (let i = 0, j = n - 1; i < n; j = i++) {
332
345
  const [xi, yi] = project(verts[i].latDeg, verts[i].lonDeg);
333
346
  const [xj, yj] = project(verts[j].latDeg, verts[j].lonDeg);
334
- const intersect = yi > py !== yj > py &&
335
- px < ((xj - xi) * (py - yi)) / (yj - yi) + xi;
347
+ const intersect = yi > py !== yj > py && px < ((xj - xi) * (py - yi)) / (yj - yi) + xi;
336
348
  if (intersect) inside = !inside;
337
349
  }
338
350
 
@@ -347,17 +359,17 @@ export function pointInGeofence(
347
359
  export function analyzeGeodesy(
348
360
  origin: { latDeg: number; lonDeg: number; altM?: number },
349
361
  destination: { latDeg: number; lonDeg: number; altM?: number },
350
- numWaypoints = 8,
362
+ numWaypoints = 8
351
363
  ): GeodesyAnalysisResult {
352
- const p1 = { latDeg: origin.latDeg, lonDeg: origin.lonDeg };
353
- const p2 = { latDeg: destination.latDeg, lonDeg: destination.lonDeg };
364
+ const p1 = { latDeg: origin.latDeg, lonDeg: origin.lonDeg };
365
+ const p2 = { latDeg: destination.latDeg, lonDeg: destination.lonDeg };
354
366
  const lla1: LatLonAlt = { ...p1, altM: origin.altM ?? 0 };
355
367
  const lla2: LatLonAlt = { ...p2, altM: destination.altM ?? 0 };
356
368
 
357
- const vincenty = vincentyInverse(p1, p2);
358
- const route = greatCircleWaypoints(p1, p2, numWaypoints);
369
+ const vincenty = vincentyInverse(p1, p2);
370
+ const route = greatCircleWaypoints(p1, p2, numWaypoints);
359
371
  const ecefOrigin = llaToECEF(lla1);
360
- const ecefDest = llaToECEF(lla2);
372
+ const ecefDest = llaToECEF(lla2);
361
373
 
362
374
  return { vincenty, route, ecefOrigin, ecefDest, converged: !vincenty.antipodal };
363
375
  }
@@ -366,31 +378,34 @@ export function analyzeGeodesy(
366
378
 
367
379
  export function buildGeodesyReceipt(
368
380
  result: GeodesyAnalysisResult,
369
- options?: { runId?: string },
381
+ options?: { runId?: string }
370
382
  ): GeodesyReceipt {
371
383
  const violations: Array<{ criterion: string; message: string }> = [];
372
384
  if (result.vincenty.antipodal)
373
- violations.push({ criterion: 'convergence', message: 'points are antipodal — geodesic undefined' });
385
+ violations.push({
386
+ criterion: 'convergence',
387
+ message: 'points are antipodal — geodesic undefined',
388
+ });
374
389
  if (!result.converged)
375
390
  violations.push({ criterion: 'convergence', message: 'Vincenty iteration did not converge' });
376
391
 
377
392
  const raw = buildDomainSimulationReceipt({
378
- plugin: 'geolocation-gis',
393
+ plugin: 'geolocation-gis',
379
394
  pluginVersion: '1.0.0',
380
- runId: options?.runId ?? `geo-${Date.now().toString(36)}`,
381
- modelId: 'geodesy-analysis',
395
+ runId: options?.runId ?? `geo-${Date.now().toString(36)}`,
396
+ modelId: 'geodesy-analysis',
382
397
  solverConfig: {
383
398
  solverType: 'vincenty-wgs84',
384
- scale: 'global',
399
+ scale: 'global',
385
400
  },
386
401
  resultSummary: {
387
- distanceKm: +(result.vincenty.distanceM / 1000).toFixed(4),
402
+ distanceKm: +(result.vincenty.distanceM / 1000).toFixed(4),
388
403
  forwardAzimuthDeg: +result.vincenty.forwardAzimuthDeg.toFixed(4),
389
- waypointCount: result.route.waypoints.length,
404
+ waypointCount: result.route.waypoints.length,
390
405
  },
391
406
  cael: {
392
- version: 'cael.v1',
393
- event: 'geolocation_gis.geodesy',
407
+ version: 'cael.v1',
408
+ event: 'geolocation_gis.geodesy',
394
409
  solverType: 'geolocation-gis.vincenty-wgs84',
395
410
  },
396
411
  acceptance: { accepted: violations.length === 0, violations },
package/src/index.ts CHANGED
@@ -13,8 +13,18 @@ import { createGeofenceHandler } from './traits/GeofenceTrait';
13
13
 
14
14
  export * from './geodesy';
15
15
 
16
- export const pluginMeta = { name: '@holoscript/plugin-geolocation-gis', version: '1.0.0', traits: ['map_view', 'route', 'poi', 'geocode', 'geofence', 'vincenty_geodesy'] };
17
- export const traitHandlers = [createMapViewHandler(), createRouteHandler(), createPOIHandler(), createGeocodeHandler(), createGeofenceHandler()];
16
+ export const pluginMeta = {
17
+ name: '@holoscript/plugin-geolocation-gis',
18
+ version: '1.0.0',
19
+ traits: ['map_view', 'route', 'poi', 'geocode', 'geofence', 'vincenty_geodesy'],
20
+ };
21
+ export const traitHandlers = [
22
+ createMapViewHandler(),
23
+ createRouteHandler(),
24
+ createPOIHandler(),
25
+ createGeocodeHandler(),
26
+ createGeofenceHandler(),
27
+ ];
18
28
 
19
29
  // Runtime integration — behavioral `vincenty_geodesy` handler + opt-in registrar
20
30
  // that wire the real Vincenty solver into HoloScriptRuntime via the shared P1
package/src/runtime.ts CHANGED
@@ -48,13 +48,13 @@ export interface RuntimeTraitHandler {
48
48
  onAttach?: (
49
49
  node: unknown,
50
50
  config: VincentyGeodesyTraitConfig,
51
- context: TraitDispatchContext,
51
+ context: TraitDispatchContext
52
52
  ) => void;
53
53
  onUpdate?: (
54
54
  node: unknown,
55
55
  config: VincentyGeodesyTraitConfig,
56
56
  context: TraitDispatchContext,
57
- delta: number,
57
+ delta: number
58
58
  ) => void;
59
59
  }
60
60
 
@@ -69,7 +69,7 @@ interface VincentyNode {
69
69
  function solveOntoNode(
70
70
  node: unknown,
71
71
  config: VincentyGeodesyTraitConfig | undefined,
72
- context: TraitDispatchContext,
72
+ context: TraitDispatchContext
73
73
  ): void {
74
74
  const carrier = node as VincentyNode;
75
75
  const nodeId = carrier.id ?? carrier.name ?? 'unknown';
@@ -1,18 +1,38 @@
1
1
  /** @geocode Trait — Address to coordinate conversion. @trait geocode */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export interface GeocodeConfig { address: string; provider: 'nominatim' | 'google' | 'mapbox'; language: string; }
4
+ export interface GeocodeConfig {
5
+ address: string;
6
+ provider: 'nominatim' | 'google' | 'mapbox';
7
+ language: string;
8
+ }
5
9
  const defaultConfig: GeocodeConfig = { address: '', provider: 'nominatim', language: 'en' };
6
10
 
7
11
  export function createGeocodeHandler(): TraitHandler<GeocodeConfig> {
8
- return { name: 'geocode', defaultConfig,
9
- onAttach(n: HSPlusNode, _c: GeocodeConfig, ctx: TraitContext) { n.__geocodeState = { result: null, isGeocoding: false }; ctx.emit?.('geocode:ready'); },
10
- onDetach(n: HSPlusNode, _c: GeocodeConfig, ctx: TraitContext) { delete n.__geocodeState; ctx.emit?.('geocode:detached'); },
12
+ return {
13
+ name: 'geocode',
14
+ defaultConfig,
15
+ onAttach(n: HSPlusNode, _c: GeocodeConfig, ctx: TraitContext) {
16
+ n.__geocodeState = { result: null, isGeocoding: false };
17
+ ctx.emit?.('geocode:ready');
18
+ },
19
+ onDetach(n: HSPlusNode, _c: GeocodeConfig, ctx: TraitContext) {
20
+ delete n.__geocodeState;
21
+ ctx.emit?.('geocode:detached');
22
+ },
11
23
  onUpdate() {},
12
24
  onEvent(n: HSPlusNode, _c: GeocodeConfig, ctx: TraitContext, e: TraitEvent) {
13
- const s = n.__geocodeState as Record<string, unknown> | undefined; if (!s) return;
14
- if (e.type === 'geocode:lookup') { s.isGeocoding = true; ctx.emit?.('geocode:searching', { address: e.payload?.address }); }
15
- if (e.type === 'geocode:result') { s.result = e.payload; s.isGeocoding = false; ctx.emit?.('geocode:found', e.payload); }
25
+ const s = n.__geocodeState as Record<string, unknown> | undefined;
26
+ if (!s) return;
27
+ if (e.type === 'geocode:lookup') {
28
+ s.isGeocoding = true;
29
+ ctx.emit?.('geocode:searching', { address: e.payload?.address });
30
+ }
31
+ if (e.type === 'geocode:result') {
32
+ s.result = e.payload;
33
+ s.isGeocoding = false;
34
+ ctx.emit?.('geocode:found', e.payload);
35
+ }
16
36
  },
17
37
  };
18
38
  }
@@ -1,22 +1,47 @@
1
1
  /** @geofence Trait — Geographic boundary monitoring. @trait geofence */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export interface GeofenceConfig { center: { lat: number; lng: number }; radiusM: number; shape: 'circle' | 'polygon'; polygonPoints?: Array<{ lat: number; lng: number }>; triggerOnEnter: boolean; triggerOnExit: boolean; }
5
- const defaultConfig: GeofenceConfig = { center: { lat: 0, lng: 0 }, radiusM: 100, shape: 'circle', triggerOnEnter: true, triggerOnExit: true };
4
+ export interface GeofenceConfig {
5
+ center: { lat: number; lng: number };
6
+ radiusM: number;
7
+ shape: 'circle' | 'polygon';
8
+ polygonPoints?: Array<{ lat: number; lng: number }>;
9
+ triggerOnEnter: boolean;
10
+ triggerOnExit: boolean;
11
+ }
12
+ const defaultConfig: GeofenceConfig = {
13
+ center: { lat: 0, lng: 0 },
14
+ radiusM: 100,
15
+ shape: 'circle',
16
+ triggerOnEnter: true,
17
+ triggerOnExit: true,
18
+ };
6
19
 
7
20
  export function createGeofenceHandler(): TraitHandler<GeofenceConfig> {
8
- return { name: 'geofence', defaultConfig,
9
- onAttach(n: HSPlusNode, c: GeofenceConfig, ctx: TraitContext) { n.__geofenceState = { isInside: false, lastCheck: null }; ctx.emit?.('geofence:created', { radius: c.radiusM, shape: c.shape }); },
10
- onDetach(n: HSPlusNode, _c: GeofenceConfig, ctx: TraitContext) { delete n.__geofenceState; ctx.emit?.('geofence:removed'); },
21
+ return {
22
+ name: 'geofence',
23
+ defaultConfig,
24
+ onAttach(n: HSPlusNode, c: GeofenceConfig, ctx: TraitContext) {
25
+ n.__geofenceState = { isInside: false, lastCheck: null };
26
+ ctx.emit?.('geofence:created', { radius: c.radiusM, shape: c.shape });
27
+ },
28
+ onDetach(n: HSPlusNode, _c: GeofenceConfig, ctx: TraitContext) {
29
+ delete n.__geofenceState;
30
+ ctx.emit?.('geofence:removed');
31
+ },
11
32
  onUpdate() {},
12
33
  onEvent(n: HSPlusNode, c: GeofenceConfig, ctx: TraitContext, e: TraitEvent) {
13
- const s = n.__geofenceState as Record<string, unknown> | undefined; if (!s) return;
34
+ const s = n.__geofenceState as Record<string, unknown> | undefined;
35
+ if (!s) return;
14
36
  if (e.type === 'geofence:check') {
15
37
  const pos = e.payload as unknown as { lat: number; lng: number };
16
- const dist = Math.sqrt(Math.pow(pos.lat - c.center.lat, 2) + Math.pow(pos.lng - c.center.lng, 2)) * 111320;
38
+ const dist =
39
+ Math.sqrt(Math.pow(pos.lat - c.center.lat, 2) + Math.pow(pos.lng - c.center.lng, 2)) *
40
+ 111320;
17
41
  const inside = dist <= c.radiusM;
18
42
  const wasInside = s.isInside as boolean;
19
- s.isInside = inside; s.lastCheck = Date.now();
43
+ s.isInside = inside;
44
+ s.lastCheck = Date.now();
20
45
  if (inside && !wasInside && c.triggerOnEnter) ctx.emit?.('geofence:entered');
21
46
  if (!inside && wasInside && c.triggerOnExit) ctx.emit?.('geofence:exited');
22
47
  }
@@ -1,18 +1,47 @@
1
1
  /** @map_view Trait — Interactive map display. @trait map_view */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export interface MapViewConfig { center: { lat: number; lng: number }; zoom: number; tileProvider: 'osm' | 'mapbox' | 'google' | 'custom'; style: 'streets' | 'satellite' | 'terrain' | 'dark'; maxZoom: number; minZoom: number; }
5
- const defaultConfig: MapViewConfig = { center: { lat: 0, lng: 0 }, zoom: 10, tileProvider: 'osm', style: 'streets', maxZoom: 18, minZoom: 1 };
4
+ export interface MapViewConfig {
5
+ center: { lat: number; lng: number };
6
+ zoom: number;
7
+ tileProvider: 'osm' | 'mapbox' | 'google' | 'custom';
8
+ style: 'streets' | 'satellite' | 'terrain' | 'dark';
9
+ maxZoom: number;
10
+ minZoom: number;
11
+ }
12
+ const defaultConfig: MapViewConfig = {
13
+ center: { lat: 0, lng: 0 },
14
+ zoom: 10,
15
+ tileProvider: 'osm',
16
+ style: 'streets',
17
+ maxZoom: 18,
18
+ minZoom: 1,
19
+ };
6
20
 
7
21
  export function createMapViewHandler(): TraitHandler<MapViewConfig> {
8
- return { name: 'map_view', defaultConfig,
9
- onAttach(n: HSPlusNode, c: MapViewConfig, ctx: TraitContext) { n.__mapState = { center: c.center, zoom: c.zoom, bounds: null }; ctx.emit?.('map:ready', { center: c.center }); },
10
- onDetach(n: HSPlusNode, _c: MapViewConfig, ctx: TraitContext) { delete n.__mapState; ctx.emit?.('map:destroyed'); },
22
+ return {
23
+ name: 'map_view',
24
+ defaultConfig,
25
+ onAttach(n: HSPlusNode, c: MapViewConfig, ctx: TraitContext) {
26
+ n.__mapState = { center: c.center, zoom: c.zoom, bounds: null };
27
+ ctx.emit?.('map:ready', { center: c.center });
28
+ },
29
+ onDetach(n: HSPlusNode, _c: MapViewConfig, ctx: TraitContext) {
30
+ delete n.__mapState;
31
+ ctx.emit?.('map:destroyed');
32
+ },
11
33
  onUpdate() {},
12
34
  onEvent(n: HSPlusNode, _c: MapViewConfig, ctx: TraitContext, e: TraitEvent) {
13
- const s = n.__mapState as Record<string, unknown> | undefined; if (!s) return;
14
- if (e.type === 'map:pan') { s.center = e.payload?.center; ctx.emit?.('map:moved', { center: s.center }); }
15
- if (e.type === 'map:zoom') { s.zoom = e.payload?.zoom; ctx.emit?.('map:zoomed', { zoom: s.zoom }); }
35
+ const s = n.__mapState as Record<string, unknown> | undefined;
36
+ if (!s) return;
37
+ if (e.type === 'map:pan') {
38
+ s.center = e.payload?.center;
39
+ ctx.emit?.('map:moved', { center: s.center });
40
+ }
41
+ if (e.type === 'map:zoom') {
42
+ s.zoom = e.payload?.zoom;
43
+ ctx.emit?.('map:zoomed', { zoom: s.zoom });
44
+ }
16
45
  },
17
46
  };
18
47
  }
@@ -1,18 +1,41 @@
1
1
  /** @poi Trait — Point of Interest. @trait poi */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export interface POIConfig { name: string; lat: number; lng: number; category: string; icon?: string; description?: string; rating?: number; }
4
+ export interface POIConfig {
5
+ name: string;
6
+ lat: number;
7
+ lng: number;
8
+ category: string;
9
+ icon?: string;
10
+ description?: string;
11
+ rating?: number;
12
+ }
5
13
  const defaultConfig: POIConfig = { name: '', lat: 0, lng: 0, category: 'general' };
6
14
 
7
15
  export function createPOIHandler(): TraitHandler<POIConfig> {
8
- return { name: 'poi', defaultConfig,
9
- onAttach(n: HSPlusNode, c: POIConfig, ctx: TraitContext) { n.__poiState = { visible: true, selected: false }; ctx.emit?.('poi:added', { name: c.name, category: c.category }); },
10
- onDetach(n: HSPlusNode, _c: POIConfig, ctx: TraitContext) { delete n.__poiState; ctx.emit?.('poi:removed'); },
16
+ return {
17
+ name: 'poi',
18
+ defaultConfig,
19
+ onAttach(n: HSPlusNode, c: POIConfig, ctx: TraitContext) {
20
+ n.__poiState = { visible: true, selected: false };
21
+ ctx.emit?.('poi:added', { name: c.name, category: c.category });
22
+ },
23
+ onDetach(n: HSPlusNode, _c: POIConfig, ctx: TraitContext) {
24
+ delete n.__poiState;
25
+ ctx.emit?.('poi:removed');
26
+ },
11
27
  onUpdate() {},
12
28
  onEvent(n: HSPlusNode, c: POIConfig, ctx: TraitContext, e: TraitEvent) {
13
- const s = n.__poiState as Record<string, unknown> | undefined; if (!s) return;
14
- if (e.type === 'poi:select') { s.selected = true; ctx.emit?.('poi:selected', { name: c.name }); }
15
- if (e.type === 'poi:deselect') { s.selected = false; ctx.emit?.('poi:deselected'); }
29
+ const s = n.__poiState as Record<string, unknown> | undefined;
30
+ if (!s) return;
31
+ if (e.type === 'poi:select') {
32
+ s.selected = true;
33
+ ctx.emit?.('poi:selected', { name: c.name });
34
+ }
35
+ if (e.type === 'poi:deselect') {
36
+ s.selected = false;
37
+ ctx.emit?.('poi:deselected');
38
+ }
16
39
  },
17
40
  };
18
41
  }
@@ -1,18 +1,58 @@
1
1
  /** @route Trait — Route planning and navigation. @trait route */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export interface Waypoint { lat: number; lng: number; label?: string; }
5
- export interface RouteConfig { origin: Waypoint; destination: Waypoint; waypoints: Waypoint[]; mode: 'driving' | 'walking' | 'cycling' | 'transit'; avoidTolls: boolean; avoidHighways: boolean; }
6
- const defaultConfig: RouteConfig = { origin: { lat: 0, lng: 0 }, destination: { lat: 0, lng: 0 }, waypoints: [], mode: 'driving', avoidTolls: false, avoidHighways: false };
4
+ export interface Waypoint {
5
+ lat: number;
6
+ lng: number;
7
+ label?: string;
8
+ }
9
+ export interface RouteConfig {
10
+ origin: Waypoint;
11
+ destination: Waypoint;
12
+ waypoints: Waypoint[];
13
+ mode: 'driving' | 'walking' | 'cycling' | 'transit';
14
+ avoidTolls: boolean;
15
+ avoidHighways: boolean;
16
+ }
17
+ const defaultConfig: RouteConfig = {
18
+ origin: { lat: 0, lng: 0 },
19
+ destination: { lat: 0, lng: 0 },
20
+ waypoints: [],
21
+ mode: 'driving',
22
+ avoidTolls: false,
23
+ avoidHighways: false,
24
+ };
7
25
 
8
26
  export function createRouteHandler(): TraitHandler<RouteConfig> {
9
- return { name: 'route', defaultConfig,
10
- onAttach(n: HSPlusNode, c: RouteConfig, ctx: TraitContext) { n.__routeState = { distanceKm: 0, durationMin: 0, isCalculating: false }; ctx.emit?.('route:created', { mode: c.mode }); },
11
- onDetach(n: HSPlusNode, _c: RouteConfig, ctx: TraitContext) { delete n.__routeState; ctx.emit?.('route:cleared'); },
27
+ return {
28
+ name: 'route',
29
+ defaultConfig,
30
+ onAttach(n: HSPlusNode, c: RouteConfig, ctx: TraitContext) {
31
+ n.__routeState = { distanceKm: 0, durationMin: 0, isCalculating: false };
32
+ ctx.emit?.('route:created', { mode: c.mode });
33
+ },
34
+ onDetach(n: HSPlusNode, _c: RouteConfig, ctx: TraitContext) {
35
+ delete n.__routeState;
36
+ ctx.emit?.('route:cleared');
37
+ },
12
38
  onUpdate() {},
13
39
  onEvent(n: HSPlusNode, _c: RouteConfig, ctx: TraitContext, e: TraitEvent) {
14
- if (e.type === 'route:calculate') { const s = n.__routeState as Record<string, unknown>; if (s) { s.isCalculating = true; ctx.emit?.('route:calculating'); } }
15
- if (e.type === 'route:result') { const s = n.__routeState as Record<string, unknown>; if (s) { s.distanceKm = e.payload?.distanceKm; s.durationMin = e.payload?.durationMin; s.isCalculating = false; ctx.emit?.('route:calculated', e.payload); } }
40
+ if (e.type === 'route:calculate') {
41
+ const s = n.__routeState as Record<string, unknown>;
42
+ if (s) {
43
+ s.isCalculating = true;
44
+ ctx.emit?.('route:calculating');
45
+ }
46
+ }
47
+ if (e.type === 'route:result') {
48
+ const s = n.__routeState as Record<string, unknown>;
49
+ if (s) {
50
+ s.distanceKm = e.payload?.distanceKm;
51
+ s.durationMin = e.payload?.durationMin;
52
+ s.isCalculating = false;
53
+ ctx.emit?.('route:calculated', e.payload);
54
+ }
55
+ }
16
56
  },
17
57
  };
18
58
  }
@@ -1,4 +1,22 @@
1
- export interface HSPlusNode { id?: string; properties?: Record<string, unknown>; [key: string]: unknown; }
2
- export interface TraitContext { emit?: (event: string, payload?: unknown) => void; [key: string]: unknown; }
3
- export interface TraitEvent { type: string; payload?: Record<string, unknown>; [key: string]: unknown; }
4
- export interface TraitHandler<T = unknown> { name: string; defaultConfig: T; onAttach(n: HSPlusNode, c: T, ctx: TraitContext): void; onDetach(n: HSPlusNode, c: T, ctx: TraitContext): void; onUpdate(n: HSPlusNode, c: T, ctx: TraitContext, d: number): void; onEvent(n: HSPlusNode, c: T, ctx: TraitContext, e: TraitEvent): void; }
1
+ export interface HSPlusNode {
2
+ id?: string;
3
+ properties?: Record<string, unknown>;
4
+ [key: string]: unknown;
5
+ }
6
+ export interface TraitContext {
7
+ emit?: (event: string, payload?: unknown) => void;
8
+ [key: string]: unknown;
9
+ }
10
+ export interface TraitEvent {
11
+ type: string;
12
+ payload?: Record<string, unknown>;
13
+ [key: string]: unknown;
14
+ }
15
+ export interface TraitHandler<T = unknown> {
16
+ name: string;
17
+ defaultConfig: T;
18
+ onAttach(n: HSPlusNode, c: T, ctx: TraitContext): void;
19
+ onDetach(n: HSPlusNode, c: T, ctx: TraitContext): void;
20
+ onUpdate(n: HSPlusNode, c: T, ctx: TraitContext, d: number): void;
21
+ onEvent(n: HSPlusNode, c: T, ctx: TraitContext, e: TraitEvent): void;
22
+ }
package/tsconfig.json CHANGED
@@ -1 +1,5 @@
1
- { "extends": "../../../tsconfig.json", "compilerOptions": { "outDir": "dist", "rootDir": "src", "declaration": true }, "include": ["src"] }
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": { "outDir": "dist", "rootDir": "src", "declaration": true },
4
+ "include": ["src"]
5
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025-2026 HoloScript Contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.