getStarHoppingPath
Calculate a step-by-step star hopping path from a bright start star to a target celestial object, ensuring each hop fits within the specified Field of View (FOV) and magnitude constraints.
Instructions
Calculates a star hopping path from a bright start star to a target celestial object. Each hop is within the specified Field of View (FOV).
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| fovDegrees | Yes | The Field of View (FOV) of the user's equipment in degrees. | |
| initialSearchRadiusDegrees | Yes | The angular radius around the target object to search for a suitable bright starting star. Default: 20.0 degrees. | |
| maxHopMagnitude | Yes | The maximum (dimmest) stellar magnitude for stars in the hopping path. Default: 8.0. | |
| startStarMagnitudeThreshold | Yes | The maximum (dimmest) magnitude for a star to be a good, bright "starting star." Default: 3.5. | |
| targetObjectName | Yes | The name or catalog identifier of the celestial object to find (e.g., "M13", "Andromeda Galaxy", "Mars"). |
Implementation Reference
- src/tools/StarHoppingTool.ts:31-35 (registration)Tool class definition and name registration as 'getStarHoppingPath'.class StarHoppingTool extends MCPTool<StarHoppingInput> { name = 'getStarHoppingPath'; description = 'Calculates a star hopping path from a bright start star to a target celestial object. Each hop is within the specified Field of View (FOV).';
- src/tools/StarHoppingTool.ts:36-59 (schema)Zod-based input schema defining parameters like targetObjectName, fovDegrees, maxHopMagnitude, etc.protected schema = { targetObjectName: { type: z.string(), description: 'The name or catalog identifier of the celestial object to find (e.g., "M13", "Andromeda Galaxy", "Mars").', }, fovDegrees: { type: z.number().positive(), description: "The Field of View (FOV) of the user's equipment in degrees.", }, maxHopMagnitude: { type: z.number().optional().default(8.0), description: 'The maximum (dimmest) stellar magnitude for stars in the hopping path. Default: 8.0.', }, initialSearchRadiusDegrees: { type: z.number().positive().optional().default(20.0), description: 'The angular radius around the target object to search for a suitable bright starting star. Default: 20.0 degrees.', }, startStarMagnitudeThreshold: { type: z.number().optional().default(3.5), description: 'The maximum (dimmest) magnitude for a star to be a good, bright "starting star." Default: 3.5.', }, };
- src/tools/StarHoppingTool.ts:75-283 (handler)Core handler function implementing the star hopping algorithm: selects starting star, iteratively finds hops to stars closer to target within FOV, computes bearings and distances, returns path or error status.async execute(params: StarHoppingInput) { const date = new Date(); const observer: Observer = { latitude: OBSERVER_CONFIG.latitude, longitude: OBSERVER_CONFIG.longitude, elevation: OBSERVER_CONFIG.altitude, temperature: OBSERVER_CONFIG.temperature, pressure: OBSERVER_CONFIG.pressure, }; let targetEquatorial: EquatorialCoordinates; try { targetEquatorial = await getEquatorialCoordinates(params.targetObjectName, date); } catch (error: any) { return { targetObjectName: params.targetObjectName, status: 'TargetNotFound', summaryMessage: `Target object "${params.targetObjectName}" not found in catalogs. ${error.message}`, }; } const targetAltAz = convertToAltAz(targetEquatorial, observer, date); if (targetAltAz.altitude <= 0) { return { targetObjectName: params.targetObjectName, targetCoordinates: { ...this.formatCoordsForOutput(targetEquatorial), ...this.formatAltAzForOutput(targetAltAz), }, fieldOfViewDegrees: params.fovDegrees, status: 'TargetNotVisible', summaryMessage: `Target "${params.targetObjectName}" is currently below the horizon.`, }; } const targetData: CelestialObjectData = { ...targetEquatorial, id: params.targetObjectName.toLowerCase(), altAz: targetAltAz, }; // Starting Star Selection let potentialStartStars: CelestialObjectData[] = []; // The above block was removed because it allowed non-star targets (like DSOs) // to be considered as starting stars if they had a magnitude, which is incorrect. // Starting stars must be actual stars from the STAR_CATALOG. for (const [starId, starEq] of STAR_CATALOG.entries()) { if (starEq.magnitude === undefined || starEq.magnitude > params.startStarMagnitudeThreshold!) { continue; } const separation = calculateAngularSeparation(starEq, targetEquatorial); if (separation > params.initialSearchRadiusDegrees!) { continue; } const starAltAz = convertToAltAz(starEq, observer, date); if (starAltAz.altitude <= 0) { continue; } potentialStartStars.push({ ...starEq, id: starId, altAz: starAltAz }); } if (potentialStartStars.length === 0) { return { targetObjectName: params.targetObjectName, targetCoordinates: { ...this.formatCoordsForOutput(targetEquatorial), ...this.formatAltAzForOutput(targetAltAz), }, fieldOfViewDegrees: params.fovDegrees, status: 'NoStartingStarFound', summaryMessage: `No suitable starting star found within ${params.initialSearchRadiusDegrees}° of "${params.targetObjectName}" and brighter than magnitude ${params.startStarMagnitudeThreshold}.`, }; } potentialStartStars.sort((a, b) => (a.magnitude ?? Infinity) - (b.magnitude ?? Infinity)); const startStar = potentialStartStars[0]; const initialSeparationToTarget = calculateAngularSeparation(startStar, targetEquatorial); const baseResponse = { targetObjectName: params.targetObjectName, targetCoordinates: { ...this.formatCoordsForOutput(targetEquatorial), ...this.formatAltAzForOutput(targetAltAz), }, fieldOfViewDegrees: params.fovDegrees, startStar: { name: startStar.name!, magnitude: startStar.magnitude!, ...this.formatCoordsForOutput(startStar), }, }; if (initialSeparationToTarget <= params.fovDegrees) { const bearingToTarget = calculateBearing(startStar, targetEquatorial); return { ...baseResponse, hopSequence: [], finalStep: { fromStar: { name: startStar.name!, magnitude: startStar.magnitude! }, message: `The target ${params.targetObjectName} should be within your FOV, approx ${initialSeparationToTarget.toFixed(1)}° towards ${bearingToTarget.cardinal} (Bearing: ${bearingToTarget.degrees}°) from ${startStar.name}.`, }, status: 'TargetInStartFOV', summaryMessage: `Target "${params.targetObjectName}" is already within FOV of the starting star "${startStar.name}".`, }; } // Star Hopping Algorithm const hopSequence: any[] = []; let currentHopStar = startStar; let currentDistanceToTarget = initialSeparationToTarget; const visitedStarIds = new Set<string>([startStar.id]); for (let hopNum = 1; hopNum <= 20; hopNum++) { // Max 20 hops to prevent infinite loops let bestNextHop: CelestialObjectData | null = null; let smallestDistToTargetForNextHop = currentDistanceToTarget; for (const [candidateId, candidateEq] of STAR_CATALOG.entries()) { if (visitedStarIds.has(candidateId) || candidateId === targetData.id) { continue; // Skip already visited or the target itself as an intermediate hop } if (candidateEq.magnitude === undefined || candidateEq.magnitude > params.maxHopMagnitude!) { continue; } const hopSeparation = calculateAngularSeparation(currentHopStar, candidateEq); if (hopSeparation > params.fovDegrees) { continue; } const candidateDistToTarget = calculateAngularSeparation(candidateEq, targetEquatorial); if (candidateDistToTarget >= currentDistanceToTarget) { // Must be closer to target continue; } const candidateAltAz = convertToAltAz(candidateEq, observer, date); if (candidateAltAz.altitude <= 0) { continue; } // Prefer candidate that makes most progress towards target if (candidateDistToTarget < smallestDistToTargetForNextHop) { smallestDistToTargetForNextHop = candidateDistToTarget; bestNextHop = { ...candidateEq, id: candidateId }; // altAz is checked for visibility but not stored in the hop object } } if (bestNextHop) { const bearingToNextHop = calculateBearing(currentHopStar, bestNextHop); const hopDistance = calculateAngularSeparation(currentHopStar, bestNextHop); hopSequence.push({ hopNumber: hopNum, fromStar: { name: currentHopStar.name!, magnitude: currentHopStar.magnitude! }, toStar: { name: bestNextHop.name!, magnitude: bestNextHop.magnitude!, ...this.formatCoordsForOutput(bestNextHop), }, direction: `towards ${bearingToNextHop.cardinal} (Bearing: ${bearingToNextHop.degrees}°)`, angularDistanceDegrees: parseFloat(hopDistance.toFixed(1)), }); currentHopStar = bestNextHop; currentDistanceToTarget = smallestDistToTargetForNextHop; visitedStarIds.add(currentHopStar.id); if (currentDistanceToTarget <= params.fovDegrees) { const bearingToTarget = calculateBearing(currentHopStar, targetEquatorial); return { ...baseResponse, hopSequence, finalStep: { fromStar: { name: currentHopStar.name!, magnitude: currentHopStar.magnitude! }, message: `The target ${params.targetObjectName} should now be within your FOV, approx ${currentDistanceToTarget.toFixed(1)}° towards ${bearingToTarget.cardinal} (Bearing: ${bearingToTarget.degrees}°) from ${currentHopStar.name}.`, }, status: 'Success', summaryMessage: `Successfully found a path with ${hopSequence.length} hop(s) to "${params.targetObjectName}".`, }; } } else { // No suitable next hop found const bearingToTarget = calculateBearing(currentHopStar, targetEquatorial); return { ...baseResponse, hopSequence, finalStep: { fromStar: { name: currentHopStar.name!, magnitude: currentHopStar.magnitude! }, message: `Pathfinding stopped. Target ${params.targetObjectName} is approx ${currentDistanceToTarget.toFixed(1)}° towards ${bearingToTarget.cardinal} (Bearing: ${bearingToTarget.degrees}°) from ${currentHopStar.name}, but no further hops could be found.`, }, status: 'PathNotFound', summaryMessage: `Could not find a complete hopping path to "${params.targetObjectName}". Path generated with ${hopSequence.length} hop(s).`, }; } } // Max hops reached const bearingToTarget = calculateBearing(currentHopStar, targetEquatorial); return { ...baseResponse, hopSequence, finalStep: { fromStar: { name: currentHopStar.name!, magnitude: currentHopStar.magnitude! }, message: `Pathfinding stopped after maximum hops. Target ${params.targetObjectName} is approx ${currentDistanceToTarget.toFixed(1)}° towards ${bearingToTarget.cardinal} (Bearing: ${bearingToTarget.degrees}°) from ${currentHopStar.name}.`, }, status: 'PathNotFound', summaryMessage: `Path to "${params.targetObjectName}" could not be completed within the maximum hop limit. Path generated with ${hopSequence.length} hop(s).`, }; }
- src/tools/StarHoppingTool.ts:61-73 (helper)Helper methods to format equatorial and alt-az coordinates for output.private formatCoordsForOutput(coords: EquatorialCoordinates) { return { rightAscension: `${coords.rightAscension.toFixed(2)}h`, declination: `${coords.declination.toFixed(2)}°`, }; } private formatAltAzForOutput(altAz: HorizontalCoordinates) { return { altitude: `${altAz.altitude.toFixed(1)}°`, azimuth: `${altAz.azimuth.toFixed(1)}°`, }; }