signalk-parquet
Version:
Vessel data Parquet file archive with automated value and geospatial triggers. History API compliant with cloud backups and queries.
271 lines • 9.77 kB
JavaScript
;
/**
* Spatial filtering utilities for History API
* Supports bbox and radius filters for DuckDB Parquet queries and JavaScript buffer filtering
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseBboxParam = parseBboxParam;
exports.parseRadiusParam = parseRadiusParam;
exports.parseSpatialParams = parseSpatialParams;
exports.buildSpatialSqlClause = buildSpatialSqlClause;
exports.filterBufferRecordsSpatially = filterBufferRecordsSpatially;
exports.isPositionPath = isPositionPath;
const geo_calculator_1 = require("./geo-calculator");
/**
* Parse bbox query parameter in "west,south,east,north" format
* @param bboxStr - Comma-separated bbox string
* @returns BoundingBox or null if invalid
*/
function parseBboxParam(bboxStr) {
if (!bboxStr)
return null;
const parts = bboxStr.split(',').map(s => parseFloat(s.trim()));
if (parts.length !== 4 || parts.some(isNaN)) {
return null;
}
const [west, south, east, north] = parts;
// Validate latitude range (-90 to 90)
if (south < -90 || south > 90 || north < -90 || north > 90) {
return null;
}
// Validate longitude range (-180 to 180)
if (west < -180 || west > 180 || east < -180 || east > 180) {
return null;
}
// Validate south <= north
if (south > north) {
return null;
}
return { west, south, east, north };
}
/**
* Parse radius query parameter in "lon,lat,meters" format (GeoJSON convention)
* @param radiusStr - Comma-separated radius string
* @returns SpatialFilter with bbox approximation and precise radius info, or null if invalid
*/
function parseRadiusParam(radiusStr) {
if (!radiusStr)
return null;
const parts = radiusStr.split(',').map(s => parseFloat(s.trim()));
if (parts.length !== 3 || parts.some(isNaN)) {
return null;
}
const [lon, lat, meters] = parts;
// Validate latitude range (-90 to 90)
if (lat < -90 || lat > 90) {
return null;
}
// Validate longitude range (-180 to 180)
if (lon < -180 || lon > 180) {
return null;
}
// Validate positive radius
if (meters <= 0) {
return null;
}
// Convert radius to bounding box for initial filtering
const bbox = (0, geo_calculator_1.createBoundingBoxFromRadius)(lat, lon, meters);
return {
type: 'radius',
bbox,
centerLat: lat,
centerLon: lon,
radiusMeters: meters,
};
}
/**
* Parse bbox or radius query parameters into a SpatialFilter
* @param bboxStr - Optional bbox parameter
* @param radiusStr - Optional radius parameter
* @returns SpatialFilter or null if no valid spatial params
*/
function parseSpatialParams(bboxStr, radiusStr) {
// Radius takes precedence over bbox
if (radiusStr) {
return parseRadiusParam(radiusStr);
}
if (bboxStr) {
const bbox = parseBboxParam(bboxStr);
if (bbox) {
return { type: 'bbox', bbox };
}
}
return null;
}
/**
* Build DuckDB WHERE clause for spatial filtering on position data
* Uses value_latitude and value_longitude columns
* @param filter - Spatial filter configuration
* @param latColumn - Column name for latitude (default: 'value_latitude')
* @param lonColumn - Column name for longitude (default: 'value_longitude')
* @returns SQL WHERE clause fragment (without leading AND)
*/
function buildSpatialSqlClause(filter, latColumn = 'TRY_CAST(value_latitude AS DOUBLE)', lonColumn = 'TRY_CAST(value_longitude AS DOUBLE)') {
const { bbox } = filter;
// Latitude check is always simple (no wrap-around)
const latCondition = `${latColumn} >= ${bbox.south} AND ${latColumn} <= ${bbox.north}`;
// Longitude check needs to handle 180° meridian crossing
let lonCondition;
if (bbox.west <= bbox.east) {
// Normal case: bbox doesn't cross 180° meridian
lonCondition = `${lonColumn} >= ${bbox.west} AND ${lonColumn} <= ${bbox.east}`;
}
else {
// Box crosses 180° meridian (e.g., west=170, east=-170)
// Point is in bbox if lon >= west OR lon <= east
lonCondition = `(${lonColumn} >= ${bbox.west} OR ${lonColumn} <= ${bbox.east})`;
}
// For radius filters, we use the bbox as a coarse filter in SQL
// The precise distance check happens in post-processing or JS filter
// For now, just return bbox filter - DuckDB will do the heavy lifting
if (filter.type === 'radius' &&
filter.centerLat !== undefined &&
filter.centerLon !== undefined &&
filter.radiusMeters !== undefined) {
// Use ST_Distance_Spheroid for precise radius filtering in DuckDB
// This requires the spatial extension which is already loaded
const distanceCondition = `ST_Distance_Spheroid(
ST_Point(${lonColumn}, ${latColumn}),
ST_Point(${filter.centerLon}, ${filter.centerLat})
) <= ${filter.radiusMeters}`;
// Return bbox filter AND precise distance check
return `(${latCondition}) AND (${lonCondition}) AND (${distanceCondition})`;
}
return `(${latCondition}) AND (${lonCondition})`;
}
/**
* Filter buffer records spatially using JavaScript
* @param records - DataRecord array from SQLite buffer
* @param filter - Spatial filter configuration
* @returns Filtered records
*/
function filterBufferRecordsSpatially(records, filter) {
return records.filter(record => {
// Extract lat/lon from record - could be in multiple formats
const lat = extractLatitude(record);
const lon = extractLongitude(record);
if (lat === null || lon === null) {
// Can't filter without position data
return true; // Include it (non-position record)
}
// Check bounding box first (quick rejection)
if (!(0, geo_calculator_1.isPointInBoundingBox)(lat, lon, filter.bbox)) {
return false;
}
// For radius filters, do precise distance check
if (filter.type === 'radius' &&
filter.centerLat !== undefined &&
filter.centerLon !== undefined &&
filter.radiusMeters !== undefined) {
const distance = (0, geo_calculator_1.calculateDistance)(lat, lon, filter.centerLat, filter.centerLon);
return distance <= filter.radiusMeters;
}
return true;
});
}
/**
* Extract latitude from a DataRecord
* Handles multiple storage formats
*/
function extractLatitude(record) {
// Check for flattened component columns (value_latitude)
if (typeof record.value_latitude === 'number') {
return record.value_latitude;
}
// Check for value object with latitude property
if (record.value && typeof record.value === 'object') {
if (typeof record.value.latitude === 'number') {
return record.value.latitude;
}
}
// Check for value_json as object
if (record.value_json && typeof record.value_json === 'object') {
const json = record.value_json;
if (typeof json.latitude === 'number') {
return json.latitude;
}
}
// Check for value_json as string
if (typeof record.value_json === 'string') {
try {
const parsed = JSON.parse(record.value_json);
if (typeof parsed.latitude === 'number') {
return parsed.latitude;
}
}
catch {
// Not valid JSON
}
}
return null;
}
/**
* Extract longitude from a DataRecord
* Handles multiple storage formats
*/
function extractLongitude(record) {
// Check for flattened component columns (value_longitude)
if (typeof record.value_longitude === 'number') {
return record.value_longitude;
}
// Check for value object with longitude property
if (record.value && typeof record.value === 'object') {
if (typeof record.value.longitude === 'number') {
return record.value.longitude;
}
}
// Check for value_json as object
if (record.value_json && typeof record.value_json === 'object') {
const json = record.value_json;
if (typeof json.longitude === 'number') {
return json.longitude;
}
}
// Check for value_json as string
if (typeof record.value_json === 'string') {
try {
const parsed = JSON.parse(record.value_json);
if (typeof parsed.longitude === 'number') {
return parsed.longitude;
}
}
catch {
// Not valid JSON
}
}
return null;
}
/**
* Check if a SignalK path supports spatial filtering
* Only paths that contain position data can be spatially filtered
* @param signalkPath - The SignalK path to check
* @returns true if path contains position data
*/
function isPositionPath(signalkPath) {
// Common position-related paths
const positionPatterns = [
'navigation.position',
'navigation.gnss.antennaPosition',
'navigation.anchor.position',
'navigation.destination.waypoint.position',
'.position', // Any path ending in .position
];
const lowerPath = signalkPath.toLowerCase();
for (const pattern of positionPatterns) {
if (pattern.startsWith('.')) {
// Suffix pattern
if (lowerPath.endsWith(pattern.toLowerCase())) {
return true;
}
}
else {
// Exact or prefix match
if (lowerPath === pattern.toLowerCase() ||
lowerPath.startsWith(pattern.toLowerCase() + '.')) {
return true;
}
}
}
return false;
}
//# sourceMappingURL=spatial-queries.js.map