@sailboat-computer/data-storage
Version:
Shared data storage library for sailboat computer v3
325 lines • 12.6 kB
JavaScript
;
/**
* Geospatial downsampling strategy implementation
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.createGeospatialDownsampler = exports.GeospatialDownsampler = void 0;
/**
* Geospatial downsampler implementation
*/
class GeospatialDownsampler {
constructor() {
this.type = 'geospatial';
}
/**
* Downsample data using geospatial strategy
*
* @param data - Data to downsample
* @param strategy - Geospatial strategy
* @returns Downsampled data
*/
async downsample(data, strategy) {
if (data.length <= 2) {
// No downsampling needed for 0, 1, or 2 points
return data;
}
try {
// Extract points from data
const points = this.extractPoints(data);
if (points.length <= 2) {
// Not enough valid points for downsampling
return data;
}
// Sort points by timestamp
points.sort((a, b) => {
const aTime = new Date(a.data.metadata.timestamp).getTime();
const bTime = new Date(b.data.metadata.timestamp).getTime();
return aTime - bTime;
});
// Apply Douglas-Peucker algorithm for path simplification
const simplifiedPoints = this.douglasPeucker(points, strategy.simplificationTolerance);
// If preserving direction changes is enabled, add back important direction changes
if (strategy.preserveDirectionChanges) {
this.preserveDirectionChanges(points, simplifiedPoints, strategy.minAngleChange || 30 // Default to 30 degrees if not specified
);
}
// If preserving key points is enabled, add back key points
if (strategy.preserveKeyPoints) {
this.preserveKeyPoints(points, simplifiedPoints);
}
// Extract StoredData from simplified points
const result = simplifiedPoints.map(point => point.data);
// Add downsampling metadata
for (const item of result) {
item.metadata.tags = {
...item.metadata.tags,
downsampled: 'true',
downsampledFrom: data.length.toString(),
downsamplingStrategy: 'geospatial'
};
}
return result;
}
catch (error) {
console.error('Failed to apply geospatial downsampling:', error);
// Return original data on error
return data;
}
}
/**
* Extract points from data
*
* @param data - Data to extract points from
* @returns Extracted points
*/
extractPoints(data) {
const points = [];
for (const item of data) {
// Try to extract latitude and longitude from data
const lat = this.extractCoordinate(item, 'latitude', 'lat');
const lon = this.extractCoordinate(item, 'longitude', 'lon', 'lng');
if (lat !== null && lon !== null) {
points.push({
latitude: lat,
longitude: lon,
data: item
});
}
}
return points;
}
/**
* Extract coordinate from data
*
* @param item - Data item
* @param primaryKey - Primary key to look for
* @param alternateKeys - Alternate keys to look for
* @returns Coordinate value or null if not found
*/
extractCoordinate(item, primaryKey, ...alternateKeys) {
// Check primary key
if (typeof item.data[primaryKey] === 'number') {
return item.data[primaryKey];
}
// Check alternate keys
for (const key of alternateKeys) {
if (typeof item.data[key] === 'number') {
return item.data[key];
}
}
// Check if coordinates are in a nested object
for (const key of ['position', 'location', 'coordinates', 'coord', 'gps']) {
if (item.data[key] && typeof item.data[key] === 'object') {
const obj = item.data[key];
// Check primary key in nested object
if (typeof obj[primaryKey] === 'number') {
return obj[primaryKey];
}
// Check alternate keys in nested object
for (const altKey of alternateKeys) {
if (typeof obj[altKey] === 'number') {
return obj[altKey];
}
}
}
}
return null;
}
/**
* Calculate perpendicular distance from point to line
*
* @param point - Point
* @param lineStart - Line start point
* @param lineEnd - Line end point
* @returns Perpendicular distance
*/
perpendicularDistance(point, lineStart, lineEnd) {
// Convert to Cartesian coordinates for simplicity
// This is an approximation that works well for small distances
const earthRadius = 6371000; // Earth radius in meters
const x1 = lineStart.longitude * Math.cos(lineStart.latitude * Math.PI / 180) * earthRadius;
const y1 = lineStart.latitude * earthRadius;
const x2 = lineEnd.longitude * Math.cos(lineEnd.latitude * Math.PI / 180) * earthRadius;
const y2 = lineEnd.latitude * earthRadius;
const x0 = point.longitude * Math.cos(point.latitude * Math.PI / 180) * earthRadius;
const y0 = point.latitude * earthRadius;
// Calculate perpendicular distance
const numerator = Math.abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1);
const denominator = Math.sqrt(Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2));
return numerator / denominator;
}
/**
* Douglas-Peucker algorithm for path simplification
*
* @param points - Points to simplify
* @param epsilon - Simplification tolerance
* @returns Simplified points
*/
douglasPeucker(points, epsilon) {
if (points.length <= 2) {
return [...points];
}
// Find the point with the maximum distance
let maxDistance = 0;
let maxIndex = 0;
for (let i = 1; i < points.length - 1; i++) {
const distance = this.perpendicularDistance(points[i], points[0], points[points.length - 1]);
if (distance > maxDistance) {
maxDistance = distance;
maxIndex = i;
}
}
// If max distance is greater than epsilon, recursively simplify
if (maxDistance > epsilon) {
// Recursive case
const firstHalf = this.douglasPeucker(points.slice(0, maxIndex + 1), epsilon);
const secondHalf = this.douglasPeucker(points.slice(maxIndex), epsilon);
// Concatenate the two parts (avoiding duplicating the middle point)
return [...firstHalf.slice(0, -1), ...secondHalf];
}
else {
// Base case
return [points[0], points[points.length - 1]];
}
}
/**
* Preserve direction changes
*
* @param originalPoints - Original points
* @param simplifiedPoints - Simplified points
* @param minAngleChange - Minimum angle change to preserve
*/
preserveDirectionChanges(originalPoints, simplifiedPoints, minAngleChange) {
// Create a set of simplified point indices for quick lookup
const simplifiedIndices = new Set();
for (const point of simplifiedPoints) {
const index = originalPoints.findIndex(p => p === point);
if (index !== -1) {
simplifiedIndices.add(index);
}
}
// Check each point for significant direction change
for (let i = 1; i < originalPoints.length - 1; i++) {
if (simplifiedIndices.has(i)) {
continue; // Skip points that are already in the simplified set
}
// Find the previous and next points that are in the simplified set
let prevIndex = i - 1;
while (prevIndex >= 0 && !simplifiedIndices.has(prevIndex)) {
prevIndex--;
}
let nextIndex = i + 1;
while (nextIndex < originalPoints.length && !simplifiedIndices.has(nextIndex)) {
nextIndex++;
}
// If we found both previous and next points
if (prevIndex >= 0 && nextIndex < originalPoints.length) {
const prev = originalPoints[prevIndex];
const current = originalPoints[i];
const next = originalPoints[nextIndex];
// Calculate bearings
const bearing1 = this.calculateBearing(prev, current);
const bearing2 = this.calculateBearing(current, next);
// Calculate absolute angle change
let angleChange = Math.abs(bearing2 - bearing1);
if (angleChange > 180) {
angleChange = 360 - angleChange;
}
// If angle change is significant, add the point
if (angleChange >= minAngleChange) {
simplifiedPoints.push(current);
simplifiedIndices.add(i);
}
}
}
}
/**
* Calculate bearing between two points
*
* @param point1 - First point
* @param point2 - Second point
* @returns Bearing in degrees
*/
calculateBearing(point1, point2) {
const lat1 = point1.latitude * Math.PI / 180;
const lat2 = point2.latitude * Math.PI / 180;
const lon1 = point1.longitude * Math.PI / 180;
const lon2 = point2.longitude * Math.PI / 180;
const y = Math.sin(lon2 - lon1) * Math.cos(lat2);
const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1);
let bearing = Math.atan2(y, x) * 180 / Math.PI;
if (bearing < 0) {
bearing += 360;
}
return bearing;
}
/**
* Preserve key points
*
* @param originalPoints - Original points
* @param simplifiedPoints - Simplified points
*/
preserveKeyPoints(originalPoints, simplifiedPoints) {
// Create a set of simplified point indices for quick lookup
const simplifiedIndices = new Set();
for (const point of simplifiedPoints) {
const index = originalPoints.findIndex(p => p === point);
if (index !== -1) {
simplifiedIndices.add(index);
}
}
// Check each point for key point status
for (let i = 0; i < originalPoints.length; i++) {
if (simplifiedIndices.has(i)) {
continue; // Skip points that are already in the simplified set
}
const point = originalPoints[i];
// Check if point is marked as a key point
const isKeyPoint = this.isKeyPoint(point.data);
if (isKeyPoint) {
simplifiedPoints.push(point);
simplifiedIndices.add(i);
}
}
}
/**
* Check if data is a key point
*
* @param data - Data to check
* @returns Whether data is a key point
*/
isKeyPoint(data) {
// Check if data has a key point tag
if (data.metadata.tags && data.metadata.tags.keyPoint === 'true') {
return true;
}
// Check if data has a key point flag
if (data.data.keyPoint === true || data.data.isKeyPoint === true) {
return true;
}
// Check if data has a key point type
if (data.data.type === 'keyPoint' || data.data.pointType === 'key') {
return true;
}
// Check if data has a waypoint flag
if (data.data.waypoint === true || data.data.isWaypoint === true) {
return true;
}
// Check if data has a waypoint type
if (data.data.type === 'waypoint' || data.data.pointType === 'waypoint') {
return true;
}
return false;
}
}
exports.GeospatialDownsampler = GeospatialDownsampler;
/**
* Create a new geospatial downsampler
*
* @returns Geospatial downsampler
*/
function createGeospatialDownsampler() {
return new GeospatialDownsampler();
}
exports.createGeospatialDownsampler = createGeospatialDownsampler;
//# sourceMappingURL=geospatial.js.map