carthorse
Version:
A geospatial trail data processing pipeline for building 3D trail databases with elevation data
224 lines • 10.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RouteGenerationBusinessLogic = void 0;
const route_discovery_config_loader_1 = require("../../config/route-discovery-config-loader");
class RouteGenerationBusinessLogic {
/**
* Calculate tolerance levels for route generation
*/
static getToleranceLevels(pattern) {
try {
const configLoader = route_discovery_config_loader_1.RouteDiscoveryConfigLoader.getInstance();
const tolerances = configLoader.getRecommendationTolerances();
// Adjust tolerances based on route length - longer routes need more flexibility
const isLongRoute = pattern.target_distance_km >= 15;
const toleranceMultiplier = isLongRoute ? 1.5 : 1.0;
// Try wider tolerances first to accept longer routes earlier, fall back to stricter
const levels = [
{
name: 'wide',
distance: tolerances.wide.distance * toleranceMultiplier,
elevation: tolerances.wide.elevation * toleranceMultiplier,
quality: tolerances.wide.quality
},
{
name: 'medium',
distance: tolerances.medium.distance * toleranceMultiplier,
elevation: tolerances.medium.elevation * toleranceMultiplier,
quality: tolerances.medium.quality
},
{
name: 'custom',
distance: tolerances.custom.distance * toleranceMultiplier,
elevation: tolerances.custom.elevation * toleranceMultiplier,
quality: tolerances.custom.quality
},
{
name: 'strict',
distance: tolerances.strict.distance * toleranceMultiplier,
elevation: tolerances.strict.elevation * toleranceMultiplier,
quality: tolerances.strict.quality
}
];
return levels;
}
catch (error) {
console.warn('⚠️ Failed to load configurable tolerances, using defaults:', error);
// Fallback to hardcoded values if config loading fails
const isLongRoute = pattern.target_distance_km >= 15;
const toleranceMultiplier = isLongRoute ? 1.5 : 1.0;
// Reverse order here as well (wide -> medium -> strict)
return [
{ name: 'wide', distance: 100 * toleranceMultiplier, elevation: 100 * toleranceMultiplier, quality: 0.6 },
{ name: 'medium', distance: 50 * toleranceMultiplier, elevation: 50 * toleranceMultiplier, quality: 0.8 },
{ name: 'strict', distance: pattern.tolerance_percent * toleranceMultiplier, elevation: pattern.tolerance_percent * toleranceMultiplier, quality: 1.0 }
];
}
}
/**
* Check if a geographic area is already used
*/
static isAreaUsed(startLon, startLat, usedAreas, minDistanceBetweenRoutes = 2.0) {
return usedAreas.some(area => {
const distance = Math.sqrt(Math.pow((startLon - area.lon) * 111.32 * Math.cos(startLat * Math.PI / 180), 2) +
Math.pow((startLat - area.lat) * 111.32, 2));
return distance < minDistanceBetweenRoutes;
});
}
/**
* Calculate route metrics from edges
*/
static calculateRouteMetrics(routeEdges) {
let totalDistance = 0;
let totalElevationGain = 0;
for (const edge of routeEdges) {
totalDistance += edge.length_km || 0;
totalElevationGain += edge.elevation_gain || 0;
}
return { totalDistance, totalElevationGain };
}
/**
* Calculate out-and-back metrics
*/
static calculateOutAndBackMetrics(oneWayDistance, oneWayElevation) {
return {
outAndBackDistance: oneWayDistance * 2,
outAndBackElevation: oneWayElevation * 2
};
}
/**
* Check if route meets tolerance criteria
*/
static meetsToleranceCriteria(outAndBackDistance, outAndBackElevation, pattern, tolerance) {
const distanceOk = outAndBackDistance >= pattern.target_distance_km * (1 - tolerance.distance / 100) &&
outAndBackDistance <= pattern.target_distance_km * (1 + tolerance.distance / 100);
const elevationOk = outAndBackElevation >= pattern.target_elevation_gain * (1 - tolerance.elevation / 100) &&
outAndBackElevation <= pattern.target_elevation_gain * (1 + tolerance.elevation / 100);
return { distanceOk, elevationOk };
}
/**
* Calculate route quality score with improved metrics
*/
static calculateRouteScore(outAndBackDistance, outAndBackElevation, pattern, tolerance, routeEdges = []) {
// Base score from tolerance quality (0-100)
let score = tolerance.quality * 100;
// Distance accuracy bonus/penalty (0-20)
const distanceAccuracy = Math.max(0, 1.0 - Math.abs(outAndBackDistance - pattern.target_distance_km) / pattern.target_distance_km);
score += distanceAccuracy * 15;
// Elevation accuracy bonus/penalty (0-20)
const elevationAccuracy = Math.max(0, 1.0 - Math.abs(outAndBackElevation - pattern.target_elevation_gain) / Math.max(pattern.target_elevation_gain, 1));
score += elevationAccuracy * 15;
// Enhanced trail diversity bonus (0-15) - favor multi-trail routes
if (routeEdges.length > 0) {
const uniqueTrails = new Set(routeEdges.map(edge => edge.app_uuid)).size;
const trailDiversityBonus = Math.min(uniqueTrails / 2, 1.0) * 15; // Increased from 10 to 15
score += trailDiversityBonus;
// Additional bonus for routes with 3+ trails
if (uniqueTrails >= 3) {
score += 5; // Extra bonus for complex multi-trail routes
}
}
// Route length bonus (0-15) - favor longer routes (increased from 10 to 15)
const lengthBonus = Math.max(0, 15 - Math.abs(outAndBackDistance - pattern.target_distance_km));
score += lengthBonus;
// Elevation gain bonus (0-10)
const elevationBonus = Math.max(0, 10 - Math.abs(outAndBackElevation - pattern.target_elevation_gain) / Math.max(pattern.target_elevation_gain / 10, 1));
score += elevationBonus;
// Enhanced bonus for longer routes (encourage longer, more challenging routes)
if (outAndBackDistance >= 20) {
score += 15; // Significant bonus for very long routes (increased from 10)
}
else if (outAndBackDistance >= 15) {
score += 12; // Large bonus for long routes (increased from 10)
}
else if (outAndBackDistance >= 10) {
score += 8; // Moderate bonus for medium routes (increased from 5)
}
else if (outAndBackDistance >= 5) {
score += 3; // Small bonus for shorter routes
}
// Bonus for routes with good elevation gain
if (outAndBackElevation >= 800) {
score += 8; // Bonus for very challenging elevation (increased from 5)
}
else if (outAndBackElevation >= 500) {
score += 5; // Bonus for challenging elevation
}
else if (outAndBackElevation >= 200) {
score += 2; // Small bonus for moderate elevation
}
// Penalty for very short routes to discourage them when longer alternatives exist
if (outAndBackDistance < 2.0) {
score -= 10; // Significant penalty for very short routes
}
else if (outAndBackDistance < 3.0) {
score -= 5; // Moderate penalty for short routes
}
// Ensure score is within 0-100 range
return Math.max(0, Math.min(100, Math.round(score)));
}
/**
* Create route recommendation object
*/
static createRouteRecommendation(pattern, pathId, routeSteps, routeEdges, outAndBackDistance, outAndBackElevation, finalScore, region) {
// Generate route name from trail names, with fallback for unnamed trails
const routeName = routeEdges.length > 0
? routeEdges.map(edge => edge.trail_name || 'Unnamed Trail').join(' → ')
: `${pattern.pattern_name} - KSP Route`;
return {
route_uuid: `ksp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
route_name: routeName,
route_type: 'out-and-back',
route_shape: 'out-and-back',
input_length_km: pattern.target_distance_km,
input_elevation_gain: pattern.target_elevation_gain,
recommended_length_km: outAndBackDistance,
recommended_elevation_gain: outAndBackElevation,
route_path: JSON.stringify({ path_id: pathId, steps: routeSteps }),
route_edges: routeEdges,
trail_count: routeEdges.length,
route_score: Math.floor(finalScore), // finalScore is already 0-100, no need to multiply
similarity_score: finalScore / 100, // Convert from 0-100 to 0-1 range
region: region
};
}
/**
* Group KSP route steps by path ID
*/
static groupKspRouteSteps(kspRows) {
const routeGroups = new Map();
for (const row of kspRows) {
if (!routeGroups.has(row.path_id)) {
routeGroups.set(row.path_id, []);
}
routeGroups.get(row.path_id).push(row);
}
return routeGroups;
}
/**
* Extract edge IDs from route steps
*/
static extractEdgeIds(routeSteps) {
return routeSteps.map((step) => step.edge).filter((edge) => edge !== -1);
}
/**
* Calculate target metrics for out-and-back routes
*/
static calculateTargetMetrics(pattern) {
return {
halfTargetDistance: pattern.target_distance_km / 2,
halfTargetElevation: pattern.target_elevation_gain / 2
};
}
/**
* Calculate distance tolerance range
*/
static calculateDistanceToleranceRange(halfTargetDistance, tolerance) {
return {
minDistance: halfTargetDistance * (1 - tolerance.distance / 100),
maxDistance: halfTargetDistance * (1 + tolerance.distance / 100)
};
}
}
exports.RouteGenerationBusinessLogic = RouteGenerationBusinessLogic;
//# sourceMappingURL=route-generation-business-logic.js.map