Skip to content

Commit 678c15d

Browse files
committed
quick GPS smoothing algorithm to handle wackiness on BASE terrace
1 parent bf2354c commit 678c15d

File tree

5 files changed

+160
-8
lines changed

5 files changed

+160
-8
lines changed

src/geo-position.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { GeoEmaSmoother } from "./geo_smoothing";
12
import { GeoListenMode } from "./mixer";
23
import { logger } from "./shims";
34
import { Coordinates, GeoPositionOptions } from "./types";
45

56
const initialGeoTimeoutSeconds = 6;
7+
// default; can be overridden by options on Roundware instantiation
68
const geoUpdateThrottleMs = 0;
79

810
const frameworkDefaultCoords: Coordinates = {
@@ -44,6 +46,7 @@ export class GeoPosition {
4446
updateCallback: CallableFunction;
4547
private _geoWatchID?: number | null;
4648
private _geoPositionStatus: true | number = 3;
49+
private _smoother?: GeoEmaSmoother;
4750

4851
constructor(navigator: Window[`navigator`], options: GeoPositionOptions) {
4952
this._navigator = navigator;
@@ -63,6 +66,24 @@ export class GeoPosition {
6366
this.updateCallback = () => {};
6467
this._geoWatchID = null;
6568

69+
// Setup optional smoother
70+
const {
71+
geoSmoothingEnabled = false,
72+
geoSmoothingAlpha = 0.15,
73+
geoSmoothingMinAccuracyMeters = 20,
74+
geoSmoothingMinEmitDeltaMeters = 0,
75+
geoSmoothingResetJumpMeters = 50,
76+
} = options;
77+
if (geoSmoothingEnabled) {
78+
this._smoother = new GeoEmaSmoother({
79+
enabled: geoSmoothingEnabled,
80+
alpha: geoSmoothingAlpha,
81+
minAccuracyMeters: geoSmoothingMinAccuracyMeters,
82+
minEmitDeltaMeters: geoSmoothingMinEmitDeltaMeters,
83+
resetJumpMeters: geoSmoothingResetJumpMeters,
84+
});
85+
}
86+
6687
//console.info({ defaultCoords: this.defaultCoords });
6788
}
6889

@@ -174,11 +195,29 @@ export class GeoPosition {
174195
)}`
175196
);
176197

198+
// Optionally smooth before forwarding; always emit as simple Coordinates
199+
let coordsToSend: Coordinates = {
200+
latitude: coords.latitude,
201+
longitude: coords.longitude,
202+
};
203+
if (this._smoother) {
204+
const smoothed = this._smoother.update(updatedPosition);
205+
if (smoothed) {
206+
coordsToSend = {
207+
latitude: smoothed.latitude,
208+
longitude: smoothed.longitude,
209+
};
210+
} else {
211+
// rejected by smoothing (poor accuracy or within deadband)
212+
return;
213+
}
214+
}
215+
177216
this._lastUpdateTime = now;
178-
this._lastCoords = coords;
217+
this._lastCoords = coordsToSend;
179218
this._geoPositionStatus = true;
180-
logger.info("Received updated geolocation:", coords);
181-
this.updateCallback(coords);
219+
logger.info("Received updated geolocation:", coordsToSend);
220+
this.updateCallback(coordsToSend);
182221
},
183222
(error) => {
184223
logger.warn(

src/geo_smoothing.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { Coordinates } from "./types";
2+
3+
export type GeoSmoothingConfig = {
4+
enabled: boolean;
5+
alpha: number; // 0..1
6+
minAccuracyMeters: number; // discard poor readings
7+
minEmitDeltaMeters: number; // deadband; 0 disables
8+
resetJumpMeters: number; // reset EMA if jump exceeds
9+
};
10+
11+
function haversineMeters(a: Coordinates, b: Coordinates): number {
12+
const R = 6371000; // meters
13+
const lat1 = ((a.latitude || 0) * Math.PI) / 180;
14+
const lat2 = ((b.latitude || 0) * Math.PI) / 180;
15+
const dLat = (((b.latitude || 0) - (a.latitude || 0)) * Math.PI) / 180;
16+
const dLon = (((b.longitude || 0) - (a.longitude || 0)) * Math.PI) / 180;
17+
const s =
18+
Math.sin(dLat / 2) ** 2 +
19+
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
20+
const c = 2 * Math.atan2(Math.sqrt(s), Math.sqrt(1 - s));
21+
return R * c;
22+
}
23+
24+
export class GeoEmaSmoother {
25+
private config: GeoSmoothingConfig;
26+
private smoothed: Coordinates | null = null;
27+
28+
constructor(config: GeoSmoothingConfig) {
29+
this.config = config;
30+
}
31+
32+
reset(): void {
33+
this.smoothed = null;
34+
}
35+
36+
update(position: GeolocationPosition): Coordinates | null {
37+
if (!this.config.enabled) return position.coords;
38+
39+
const { accuracy, latitude, longitude } = position.coords;
40+
if (
41+
typeof accuracy === "number" &&
42+
accuracy > this.config.minAccuracyMeters
43+
) {
44+
return null; // reject poor reading
45+
}
46+
47+
const incoming: Coordinates = { latitude, longitude };
48+
49+
if (!this.smoothed) {
50+
this.smoothed = incoming;
51+
return this.smoothed;
52+
}
53+
54+
// reset on teleport
55+
if (
56+
haversineMeters(this.smoothed, incoming) > this.config.resetJumpMeters
57+
) {
58+
this.smoothed = incoming;
59+
return this.smoothed;
60+
}
61+
62+
const a = this.config.alpha;
63+
this.smoothed = {
64+
latitude:
65+
a * (incoming.latitude || 0) + (1 - a) * (this.smoothed.latitude || 0),
66+
longitude:
67+
a * (incoming.longitude || 0) +
68+
(1 - a) * (this.smoothed.longitude || 0),
69+
};
70+
71+
if (this.config.minEmitDeltaMeters > 0) {
72+
const delta = haversineMeters(this.smoothed, incoming);
73+
if (delta < this.config.minEmitDeltaMeters) {
74+
// hold last output; pretend no update
75+
return null;
76+
}
77+
}
78+
79+
return this.smoothed;
80+
}
81+
}
82+

src/roundware.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,23 @@ import {
2424
Coordinates,
2525
GeoListenModeType,
2626
IAudioData,
27-
IInitialParams,
2827
IMixParams,
2928
ITimedAssetData,
3029
IUiConfig,
3130
} from "./types";
32-
import { IAssetFilters } from "./types/asset";
33-
import { IAssetData } from "./types/asset";
31+
import { IAssetData, IAssetFilters } from "./types/asset";
3432
import { IAudioTrackData } from "./types/audioTrack";
3533
import { IEnvelopeData } from "./types/envelope";
3634
import { IOptions, IRoundwareConstructorOptions } from "./types/roundware";
3735
import { ISpeakerData, ISpeakerFilters } from "./types/speaker";
3836
import { User } from "./user";
3937

4038
export * from "./assetFilters";
39+
export { GeoListenMode, Roundware };
4140

42-
import { multiPolygon, featureCollection } from "@turf/helpers";
4341
import bbox from "@turf/bbox";
4442
import buffer from "@turf/buffer";
43+
import { featureCollection, multiPolygon } from "@turf/helpers";
4544
import { ListenHistory } from "./listenHistory";
4645

4746
/** This class is the primary integration point between Roundware's server and your application
@@ -197,6 +196,13 @@ class Roundware {
197196
new GeoPosition(navigator, {
198197
geoListenMode: newOptions.geoListenMode,
199198
defaultCoords: listenerLocation,
199+
geoSmoothingEnabled: newOptions.geoSmoothingEnabled,
200+
geoSmoothingAlpha: newOptions.geoSmoothingAlpha,
201+
geoSmoothingMinAccuracyMeters: newOptions.geoSmoothingMinAccuracyMeters,
202+
geoSmoothingMinEmitDeltaMeters:
203+
newOptions.geoSmoothingMinEmitDeltaMeters,
204+
geoSmoothingResetJumpMeters: newOptions.geoSmoothingResetJumpMeters,
205+
geoUpdateThrottleMs: newOptions.geoUpdateThrottleMs,
200206
});
201207
this._session =
202208
session ||
@@ -636,4 +642,3 @@ class Roundware {
636642
};
637643
}
638644
}
639-
export { GeoListenMode, Roundware };

src/types/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ export interface IAudioData extends Blob {
8181
export interface GeoPositionOptions {
8282
defaultCoords: Coordinates;
8383
geoListenMode: GeoListenModeType;
84+
geoSmoothingEnabled?: boolean;
85+
geoSmoothingAlpha?: number;
86+
geoSmoothingMinAccuracyMeters?: number;
87+
geoSmoothingMinEmitDeltaMeters?: number;
88+
geoSmoothingResetJumpMeters?: number;
89+
geoUpdateThrottleMs?: number;
8490
}
8591

8692
export interface IGeoPosition {

src/types/roundware.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ export interface IOptions {
1717
deviceId: string;
1818
clientType?: string;
1919
geoListenMode: GeoListenModeType;
20+
/** Enable EMA-based geo smoothing (default: false) */
21+
geoSmoothingEnabled?: boolean;
22+
/** EMA alpha in [0,1], lower = more smoothing (default: 0.15) */
23+
geoSmoothingAlpha?: number;
24+
/** Discard updates with accuracy worse than this (meters). Default: 20 */
25+
geoSmoothingMinAccuracyMeters?: number;
26+
/** Minimum movement before emitting new point (meters). Default: 0 (disabled) */
27+
geoSmoothingMinEmitDeltaMeters?: number;
28+
/** Reset EMA if jump exceeds this (meters). Default: 50 */
29+
geoSmoothingResetJumpMeters?: number;
30+
/** Throttle geolocation update handling (ms). Default: 0 (no throttle) */
31+
geoUpdateThrottleMs?: number;
2032
}
2133
export interface IRoundwareConstructorOptions extends IOptions {
2234
serverUrl: string;
@@ -91,4 +103,12 @@ export type SpeakerConfig = {
91103
// newly submitted speaker priority configuration
92104
prioritizeNewlySubmitted?: boolean; // default: true - whether to prioritize newly submitted speakers
93105
newlySubmittedPriorityDurationMs?: number; // default: 30000 (30 seconds) - how long to prioritize newly submitted speakers
106+
107+
// GPS smoothing configuration
108+
geoSmoothingEnabled?: boolean; // Enable EMA-based geo smoothing (default: false)
109+
geoSmoothingAlpha?: number; // EMA alpha in [0,1], lower = more smoothing (default: 0.15)
110+
geoSmoothingMinAccuracyMeters?: number; // Discard updates with accuracy worse than this (meters). Default: 20
111+
geoSmoothingMinEmitDeltaMeters?: number; // Minimum movement before emitting new point (meters). Default: 0 (disabled)
112+
geoSmoothingResetJumpMeters?: number; // Reset EMA if jump exceeds this (meters). Default: 50
113+
geoUpdateThrottleMs?: number; // Throttle geolocation update handling (ms). Default: 0 (no throttle)
94114
};

0 commit comments

Comments
 (0)