carthorse
Version:
A geospatial trail data processing pipeline for building 3D trail databases with elevation data
339 lines • 15.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ValidationService = void 0;
class ValidationService {
constructor(pgClient) {
this.pgClient = pgClient;
}
/**
* Validate bbox data for all trails
*/
async validateBboxData(schemaName) {
console.log('🔍 Validating bbox data...');
const result = {
isValid: true,
errors: [],
missingBboxCount: 0,
invalidBboxCount: 0,
shortTrailsWithInvalidBbox: []
};
// Check for trails with missing bbox data
const missingBboxResult = await this.pgClient.query(`
SELECT COUNT(*) as count FROM ${schemaName}.trails
WHERE bbox_min_lng IS NULL OR bbox_max_lng IS NULL
OR bbox_min_lat IS NULL OR bbox_max_lat IS NULL
`);
result.missingBboxCount = parseInt(missingBboxResult.rows[0].count);
if (result.missingBboxCount > 0) {
const error = `${result.missingBboxCount} trails have missing bbox data`;
console.error(`❌ BBOX VALIDATION FAILED: ${error}`);
result.errors.push(error);
result.isValid = false;
}
// Check for trails with invalid bbox data (min > max)
const invalidBboxResult = await this.pgClient.query(`
SELECT COUNT(*) as count FROM ${schemaName}.trails
WHERE bbox_min_lng > bbox_max_lng OR bbox_min_lat > bbox_max_lat
`);
result.invalidBboxCount = parseInt(invalidBboxResult.rows[0].count);
if (result.invalidBboxCount > 0) {
const error = `${result.invalidBboxCount} trails have invalid bbox data (min > max)`;
console.error(`❌ BBOX VALIDATION FAILED: ${error}`);
result.errors.push(error);
result.isValid = false;
}
// Special validation for trails with identical coordinates (edge case)
// These must be horizontal or vertical flat trails with minimum length
const identicalCoordsResult = await this.pgClient.query(`
SELECT name, app_uuid, ST_Length(geometry::geography) as length_meters,
bbox_min_lng, bbox_max_lng, bbox_min_lat, bbox_max_lat,
(bbox_max_lng - bbox_min_lng) as lng_range,
(bbox_max_lat - bbox_min_lat) as lat_range
FROM ${schemaName}.trails
WHERE (bbox_min_lng = bbox_max_lng OR bbox_min_lat = bbox_max_lat)
AND ST_Length(geometry::geography) < 0.5
ORDER BY length_meters ASC
`);
const invalidFlatTrails = identicalCoordsResult.rows.map(row => ({
name: row.name,
app_uuid: row.app_uuid,
length_meters: parseFloat(row.length_meters),
bbox_min_lng: row.bbox_min_lng,
bbox_max_lng: row.bbox_max_lng,
bbox_min_lat: row.bbox_min_lat,
bbox_max_lat: row.bbox_max_lat
}));
if (invalidFlatTrails.length > 0) {
const error = `${invalidFlatTrails.length} trails have identical bbox coordinates but are too short (< 0.5m)`;
console.error(`❌ BBOX VALIDATION FAILED: ${error}`);
console.error('📋 Invalid flat trails details:');
invalidFlatTrails.forEach(trail => {
console.error(` - ${trail.name} (${trail.app_uuid}): ${trail.length_meters.toFixed(2)}m`);
});
result.errors.push(error);
result.isValid = false;
result.shortTrailsWithInvalidBbox = invalidFlatTrails;
}
// Check for trails with small bbox ranges that might be problematic
// Get details of short trails with small bbox ranges for debugging
const smallBboxTrailsResult = await this.pgClient.query(`
SELECT name, app_uuid, ST_Length(geometry::geography) as length_meters,
bbox_min_lng, bbox_max_lng, bbox_min_lat, bbox_max_lat,
(bbox_max_lng - bbox_min_lng) as lng_range,
(bbox_max_lat - bbox_min_lat) as lat_range
FROM ${schemaName}.trails
WHERE (bbox_max_lng - bbox_min_lng) < 0.001 OR (bbox_max_lat - bbox_min_lat) < 0.001
AND ST_Length(geometry::geography) < 10
AND NOT (bbox_min_lng = bbox_max_lng OR bbox_min_lat = bbox_max_lat)
ORDER BY length_meters ASC
`);
// Combine and deduplicate based on app_uuid
const allShortTrails = [
...result.shortTrailsWithInvalidBbox,
...smallBboxTrailsResult.rows.map(row => ({
name: row.name,
app_uuid: row.app_uuid,
length_meters: parseFloat(row.length_meters),
bbox_min_lng: row.bbox_min_lng,
bbox_max_lng: row.bbox_max_lng,
bbox_min_lat: row.bbox_min_lat,
bbox_max_lat: row.bbox_max_lat
}))
];
// Deduplicate based on app_uuid
const seenUuids = new Set();
result.shortTrailsWithInvalidBbox = allShortTrails.filter(trail => {
if (seenUuids.has(trail.app_uuid)) {
return false;
}
seenUuids.add(trail.app_uuid);
return true;
});
if (result.shortTrailsWithInvalidBbox.length > 0) {
console.warn(`⚠️ Found ${result.shortTrailsWithInvalidBbox.length} short trails with small bbox ranges:`);
result.shortTrailsWithInvalidBbox.forEach(trail => {
console.warn(` - ${trail.name} (${trail.app_uuid}): ${trail.length_meters.toFixed(2)}m`);
});
}
if (result.isValid) {
console.log('✅ Bbox data validation passed');
}
return result;
}
/**
* Validate geometry data for all trails
*/
async validateGeometryData(schemaName) {
console.log('🔍 Validating geometry data...');
const result = {
isValid: true,
errors: [],
invalidGeometryCount: 0,
emptyGeometryCount: 0
};
// Check for trails with empty or invalid geometry
const emptyGeometryResult = await this.pgClient.query(`
SELECT COUNT(*) as count FROM ${schemaName}.trails
WHERE geometry IS NULL OR ST_IsEmpty(geometry) OR NOT ST_IsValid(geometry)
`);
result.emptyGeometryCount = parseInt(emptyGeometryResult.rows[0].count);
if (result.emptyGeometryCount > 0) {
const error = `${result.emptyGeometryCount} trails have empty or invalid geometry`;
console.error(`❌ GEOMETRY VALIDATION FAILED: ${error}`);
result.errors.push(error);
result.isValid = false;
}
// Check for trails with wrong geometry type
const invalidGeometryResult = await this.pgClient.query(`
SELECT COUNT(*) as count FROM ${schemaName}.trails
WHERE NOT ST_GeometryType(geometry) = 'ST_LineString'
`);
result.invalidGeometryCount = parseInt(invalidGeometryResult.rows[0].count);
if (result.invalidGeometryCount > 0) {
const error = `${result.invalidGeometryCount} trails have wrong geometry type (not LineString)`;
console.error(`❌ GEOMETRY VALIDATION FAILED: ${error}`);
result.errors.push(error);
result.isValid = false;
}
if (result.isValid) {
console.log('✅ Geometry data validation passed');
}
return result;
}
/**
* Validate trail lengths - fail export if any trails are under minimum length
*/
async validateTrailLengths(schemaName, minLengthMeters = 2) {
console.log(`🔍 Validating trail lengths (minimum: ${minLengthMeters}m)...`);
const result = {
isValid: true,
errors: [],
shortTrailsCount: 0,
shortTrails: []
};
// Get short trails with details for debugging
const shortTrailsResult = await this.pgClient.query(`
SELECT name, app_uuid, ST_Length(geometry::geography) as length_meters, region
FROM ${schemaName}.trails
WHERE ST_Length(geometry::geography) < $1
ORDER BY length_meters ASC
`, [minLengthMeters]);
result.shortTrailsCount = shortTrailsResult.rows.length;
result.shortTrails = shortTrailsResult.rows.map(row => ({
name: row.name,
app_uuid: row.app_uuid,
length_meters: parseFloat(row.length_meters),
region: row.region
}));
if (result.shortTrailsCount > 0) {
const error = `${result.shortTrailsCount} trails are shorter than ${minLengthMeters} meter(s)`;
console.error(`❌ TRAIL LENGTH VALIDATION FAILED: ${error}`);
// Log detailed information about short trails for debugging
console.error('📋 Short trails details:');
result.shortTrails.forEach(trail => {
console.error(` - ${trail.name} (${trail.app_uuid}): ${trail.length_meters.toFixed(2)}m (${trail.region})`);
});
result.errors.push(error);
result.isValid = false;
}
if (result.isValid) {
console.log('✅ Trail length validation passed');
}
return result;
}
/**
* Comprehensive validation of all trail data
*/
async validateAllTrailData(schemaName) {
console.log('🔍 Performing comprehensive trail data validation...');
const result = {
isValid: true,
errors: [],
warnings: [],
summary: {
totalTrails: 0,
validTrails: 0,
invalidTrails: 0
}
};
// Get total trail count
const totalResult = await this.pgClient.query(`
SELECT COUNT(*) as count FROM ${schemaName}.trails
`);
result.summary.totalTrails = parseInt(totalResult.rows[0].count);
// Validate bbox data (commented out to avoid duplicate validation - handled by validate-bbox-data hook)
// const bboxValidation = await this.validateBboxData(schemaName);
// if (!bboxValidation.isValid) {
// result.errors.push(...bboxValidation.errors);
// result.isValid = false;
// }
// Validate geometry data
const geometryValidation = await this.validateGeometryData(schemaName);
if (!geometryValidation.isValid) {
result.errors.push(...geometryValidation.errors);
result.isValid = false;
}
// Validate trail lengths (fail export if any trails under 0.5 meters - very lenient for split trails)
const lengthValidation = await this.validateTrailLengths(schemaName, 0.5);
if (!lengthValidation.isValid) {
result.errors.push(...lengthValidation.errors);
result.isValid = false;
}
// Check for trails with missing required fields
const missingFieldsResult = await this.pgClient.query(`
SELECT COUNT(*) as count FROM ${schemaName}.trails
WHERE name IS NULL OR name = ''
OR app_uuid IS NULL OR app_uuid = ''
OR region IS NULL OR region = ''
`);
const missingFieldsCount = parseInt(missingFieldsResult.rows[0].count);
if (missingFieldsCount > 0) {
const error = `${missingFieldsCount} trails have missing required fields (name, app_uuid, region)`;
console.error(`❌ DATA VALIDATION FAILED: ${error}`);
result.errors.push(error);
result.isValid = false;
}
// Calculate valid trails
result.summary.validTrails = result.isValid ? result.summary.totalTrails : 0;
result.summary.invalidTrails = result.summary.totalTrails - result.summary.validTrails;
if (result.isValid) {
console.log('✅ Comprehensive trail data validation passed');
console.log(`📊 Summary: ${result.summary.validTrails}/${result.summary.totalTrails} trails are valid`);
}
else {
console.error('❌ Comprehensive trail data validation failed');
console.error('Errors:', result.errors);
}
return result;
}
/**
* Validate routing graph data
*/
async validateRoutingGraph(schemaName) {
console.log('🔍 Validating routing graph data...');
const result = {
isValid: true,
errors: [],
nodeCount: 0,
edgeCount: 0,
orphanedNodes: 0,
selfLoops: 0
};
// Check if routing tables exist
const tablesExist = await this.pgClient.query(`
SELECT
(SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = $1 AND table_name = 'ways_noded_vertices_pgr') as nodes_exist,
(SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = $1 AND table_name = 'routing_edges') as edges_exist
`, [schemaName]);
const nodesExist = parseInt(tablesExist.rows[0].nodes_exist) > 0;
const edgesExist = parseInt(tablesExist.rows[0].edges_exist) > 0;
if (!nodesExist || !edgesExist) {
const error = 'Routing tables do not exist';
console.error(`❌ ROUTING VALIDATION FAILED: ${error}`);
result.errors.push(error);
result.isValid = false;
return result;
}
// Get node and edge counts
const countsResult = await this.pgClient.query(`
SELECT
(SELECT COUNT(*) FROM ${schemaName}.ways_noded_vertices_pgr) as node_count,
(SELECT COUNT(*) FROM ${schemaName}.routing_edges) as edge_count
`);
result.nodeCount = parseInt(countsResult.rows[0].node_count);
result.edgeCount = parseInt(countsResult.rows[0].edge_count);
// Check for orphaned nodes (nodes not connected by any edges)
const orphanedResult = await this.pgClient.query(`
SELECT COUNT(*) as count FROM ${schemaName}.ways_noded_vertices_pgr n
WHERE NOT EXISTS (
SELECT 1 FROM ${schemaName}.routing_edges e
WHERE e.source = n.id OR e.target = n.id
)
`);
result.orphanedNodes = parseInt(orphanedResult.rows[0].count);
if (result.orphanedNodes > 0) {
const warning = `${result.orphanedNodes} orphaned nodes found (not connected by any edges)`;
console.warn(`⚠️ ROUTING WARNING: ${warning}`);
result.errors.push(warning);
}
// Check for self-loops (edges where source = target)
const selfLoopsResult = await this.pgClient.query(`
SELECT COUNT(*) as count FROM ${schemaName}.routing_edges
WHERE source = target
`);
result.selfLoops = parseInt(selfLoopsResult.rows[0].count);
if (result.selfLoops > 0) {
const error = `${result.selfLoops} self-loops found in routing edges`;
console.error(`❌ ROUTING VALIDATION FAILED: ${error}`);
result.errors.push(error);
result.isValid = false;
}
if (result.isValid) {
console.log('✅ Routing graph validation passed');
console.log(`📊 Routing summary: ${result.nodeCount} nodes, ${result.edgeCount} edges`);
}
return result;
}
}
exports.ValidationService = ValidationService;
//# sourceMappingURL=validation-service.js.map