UNPKG

@sailboat-computer/data-storage

Version:

Shared data storage library for sailboat computer v3

325 lines 12.6 kB
"use strict"; /** * 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