feat: zoom on click, google maps click fix, and activity hysteresis
This commit is contained in:
parent
66906f2eed
commit
3d7c74b325
3 changed files with 57 additions and 11 deletions
|
|
@ -46,8 +46,7 @@ export const MARKER_SIZES = {
|
||||||
export const ACTIVITY_THRESHOLDS = {
|
export const ACTIVITY_THRESHOLDS = {
|
||||||
still: 1,
|
still: 1,
|
||||||
walking: 7,
|
walking: 7,
|
||||||
cycling: 25,
|
vehicle: 7
|
||||||
vehicle: 25
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ export class EntityDataFetcher {
|
||||||
this._entities = [];
|
this._entities = [];
|
||||||
this._positionHistory = new Map(); // entityId → PositionEntry[]
|
this._positionHistory = new Map(); // entityId → PositionEntry[]
|
||||||
this._lastPredictedActivity = new Map(); // entityId → string (last known predicted activity)
|
this._lastPredictedActivity = new Map(); // entityId → string (last known predicted activity)
|
||||||
|
this._candidateActivity = new Map(); // entityId → { activity: string, timestamp: number }
|
||||||
|
this._activityStabilityMs = 3000; // 3 second hysteresis
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -138,12 +140,23 @@ export class EntityDataFetcher {
|
||||||
// We use optional chaining and strict equality to ensure we only enter this block
|
// We use optional chaining and strict equality to ensure we only enter this block
|
||||||
// if the user explicitly selected 'speed_predicted'
|
// if the user explicitly selected 'speed_predicted'
|
||||||
if (config?.activity_source === 'speed_predicted') {
|
if (config?.activity_source === 'speed_predicted') {
|
||||||
if (data.speed) {
|
if (data.speed && data.speed.speed_kmh !== null && data.speed.speed_kmh !== undefined) {
|
||||||
// Use predicted activity and cache it for sticky behavior
|
// Use stable predicted activity with hysteresis
|
||||||
const predictedActivity = data.predicted_activity || 'unknown';
|
const activity = this._getStablePredictedActivity(entityId, data.speed.speed_kmh);
|
||||||
this._lastPredictedActivity.set(entityId, predictedActivity);
|
if (activity) {
|
||||||
this._log(`Activity for ${entityId}: predicted(${predictedActivity}) - speed available`);
|
this._log(`Activity for ${entityId}: predicted(${activity}) - speed available`);
|
||||||
return predictedActivity;
|
return activity;
|
||||||
|
} else {
|
||||||
|
// Activity is stabilizing, use last predicted activity if available
|
||||||
|
const lastActivity = this._lastPredictedActivity.get(entityId);
|
||||||
|
if (lastActivity) {
|
||||||
|
this._log(`Activity for ${entityId}: stable(${lastActivity}) - stabilizing`);
|
||||||
|
return lastActivity;
|
||||||
|
}
|
||||||
|
// No last activity, use unknown
|
||||||
|
this._log(`Activity for ${entityId}: unknown - stabilizing, no last activity`);
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Speed is null, use sticky last predicted activity if available
|
// Speed is null, use sticky last predicted activity if available
|
||||||
const lastActivity = this._lastPredictedActivity.get(entityId);
|
const lastActivity = this._lastPredictedActivity.get(entityId);
|
||||||
|
|
@ -323,13 +336,43 @@ export class EntityDataFetcher {
|
||||||
return 'still';
|
return 'still';
|
||||||
} else if (speedKmh < ACTIVITY_THRESHOLDS.walking) {
|
} else if (speedKmh < ACTIVITY_THRESHOLDS.walking) {
|
||||||
return 'walking';
|
return 'walking';
|
||||||
} else if (speedKmh < ACTIVITY_THRESHOLDS.cycling) {
|
|
||||||
return 'on_bicycle';
|
|
||||||
} else {
|
} else {
|
||||||
return 'in_vehicle';
|
return 'in_vehicle';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets stable predicted activity with 3-second hysteresis
|
||||||
|
* @param {string} entityId - Entity identifier
|
||||||
|
* @param {number} speedKmh - Speed in km/h
|
||||||
|
* @returns {string|null} Activity if stable, null if stabilizing
|
||||||
|
*/
|
||||||
|
_getStablePredictedActivity(entityId, speedKmh) {
|
||||||
|
const currentActivity = this.predictActivity(speedKmh);
|
||||||
|
const candidate = this._candidateActivity.get(entityId);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (!candidate || candidate.activity !== currentActivity) {
|
||||||
|
// New candidate activity, start tracking
|
||||||
|
this._candidateActivity.set(entityId, { activity: currentActivity, timestamp: now });
|
||||||
|
this._log(`Activity candidate for ${entityId}: ${currentActivity} (new)`);
|
||||||
|
return this._lastPredictedActivity.get(entityId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same candidate, check if we've passed stability period
|
||||||
|
const elapsed = now - candidate.timestamp;
|
||||||
|
if (elapsed > this._activityStabilityMs) {
|
||||||
|
// Activity is stable, update last predicted activity
|
||||||
|
this._lastPredictedActivity.set(entityId, currentActivity);
|
||||||
|
this._log(`Activity stable for ${entityId}: ${currentActivity} (elapsed: ${elapsed}ms)`);
|
||||||
|
return currentActivity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Still stabilizing, return last stable activity
|
||||||
|
this._log(`Activity stabilizing for ${entityId}: ${currentActivity} (elapsed: ${elapsed}ms)`);
|
||||||
|
return this._lastPredictedActivity.get(entityId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates position history for an entity (ring buffer of max 5 entries)
|
* Updates position history for an entity (ring buffer of max 5 entries)
|
||||||
* @param {string} entityId - Entity identifier
|
* @param {string} entityId - Entity identifier
|
||||||
|
|
|
||||||
|
|
@ -582,6 +582,7 @@ function updateMarkerOSM(entityId, data, lat, lon, personState, activityState, p
|
||||||
currentPopup.closePopup();
|
currentPopup.closePopup();
|
||||||
}
|
}
|
||||||
currentPopup = marker;
|
currentPopup = marker;
|
||||||
|
map.setView(marker.getLatLng(), 17, { animate: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
markers[entityId] = marker;
|
markers[entityId] = marker;
|
||||||
|
|
@ -634,13 +635,16 @@ function updateMarkerGoogle(entityId, data, lat, lon, personState, activityState
|
||||||
div.title = this.title;
|
div.title = this.title;
|
||||||
|
|
||||||
// Add click event for info window
|
// Add click event for info window
|
||||||
div.addEventListener('click', () => {
|
div.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
// Close currently open popup
|
// Close currently open popup
|
||||||
if (currentPopup && currentPopup !== this.infoWindow) {
|
if (currentPopup && currentPopup !== this.infoWindow) {
|
||||||
currentPopup.close();
|
currentPopup.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.infoWindow) {
|
if (this.infoWindow) {
|
||||||
|
map.setCenter(this.position);
|
||||||
|
map.setZoom(17);
|
||||||
this.infoWindow.open(this.getMap());
|
this.infoWindow.open(this.getMap());
|
||||||
currentPopup = this.infoWindow;
|
currentPopup = this.infoWindow;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue