Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/V3RNE42/helios/llms.txt

Use this file to discover all available pages before exploring further.

A single-point solar calculation — “what is the sun doing at the origin right now?” — would give a misleading answer for any trip that lasts more than a few minutes: the vehicle moves, and so does the Earth relative to the sun. Helios solves this by treating the journey as a continuous great-circle arc. It moves an imaginary vehicle along that arc at constant speed and, as the vehicle travels, samples solar events (sunrises and sunsets) at the vehicle’s current interpolated position. The result is a list of timestamped, geo-located solar events that divides the journey into discrete daylight sections.

Great-circle interpolation

The geometric foundation is getNewCoords in trigo.js. Given an origin, a destination, and a fraction between 0 and 1, it returns the coordinates of the point that lies that fraction of the way along the great-circle arc:
function getNewCoords(startCoords, endCoords, fraction)
Internally the function proceeds in three steps:
  1. Angular distance — the haversine formula converts the latitude/longitude pairs into an angular distance (in kilometres, using an Earth radius of 6,371 km).
  2. BearinggetBearing computes the initial compass heading from origin to destination using the standard spherical-trigonometry bearing formula.
  3. Destination coordinatesgetDestinationCoords projects from the start point along the computed bearing for fraction × distance kilometres, returning the interpolated { lat, lon } object.
The fraction value is called RATE inside computeRouteSections and advances from 0 (departure) to 1 (arrival) as the algorithm walks through the journey.

The computeRouteSections loop

computeRouteSections in routeSolar.js is the core of the route model. It accepts the departure and arrival coordinates and timestamps, the getDayInfo solar calculator, and an optional getOffset timezone resolver. It works as follows:
  1. Initialise at the origin. Set RATE = 0 (position = origin), datePointer = diasalida (departure time), and call isThereDaylightNow to decide whether to search for the next sunset (if it is already daytime) or the next sunrise (if it is currently night).
  2. Outer loop — advance through solar events. Repeat until arrival time is reached or the loop budget is exhausted:
    • If looking for a sunrise, advance datePointer by one day so the search window covers the next morning.
    • Enter the inner convergence loop.
  3. Inner convergence loop — binary-search-like refinement. Iterate until the difference between two consecutive time estimates is under MAX_DIFF = 2500 milliseconds:
    • Call updateTime() to compute the sunset or sunrise time at the current (RATE, coordPoint).
    • Update RATE to the fraction corresponding to that time.
    • Move coordPoint to the new RATE position.
    • Check isThereDaylightNow at the new position. If the vehicle is still on the “wrong” side of the solar event (e.g. still in daylight when searching for a sunset), nudge RATE forward by increase; otherwise nudge it backward. This steers the estimate toward the crossing point.
    • Recalculate coordPoint and datePointer and measure diff.
    The inner loop is guarded by MAX_INNER_ITERATIONS = 100000. Exceeding that limit throws an explicit error rather than hanging the browser tab.
  4. Record the event. Once converged, push an entry onto the sections array containing the event type ("sunrise" or "sunset"), the converged timestamp (datePointer), the geographic coordinates (coordPoint), and the UTC offset if local time display is enabled.
  5. Flip and repeat. Toggle seekSunset and return to step 2 to search for the next solar event.

Section formatting

The raw sections array produced by computeRouteSections is a flat list of timestamped solar-event objects bookended by the departure and arrival entries. sectionFormatter in index_def.js transforms this list into paired day-segments:
  • The array is sorted chronologically and trimmed so that it always starts with a sunrise entry and ends with a sunset entry.
  • Elements are consumed in pairs: sections[i] (sunrise) and sections[i+1] (sunset) form one section.
  • For each pair, sectionFormatter computes:
    • noon — the midpoint timestamp between sunrise and sunset: new Date(Math.floor((sunset.getTime() - sunrise.getTime()) / 2) + sunrise.getTime()).
    • noonCoords — the midpoint between the sunrise and sunset geographic coordinates.
    • NaS (North-to-South) — true if the sunrise latitude is greater than the sunset latitude, meaning the vehicle is moving south through that segment.
NaS = subSection[i]["coords"].lat > subSection[i + 1]["coords"].lat;
Each formatted section therefore carries everything needed for the seat recommendation: a sunrise time, a sunset time, a solar noon, and the direction of travel during that segment.

Multi-day journeys

The outer loop budget in computeRouteSections scales with trip duration:
let limit = (Math.floor(total_time * 2 / ONE_DAY) >= 1)
    ? Math.floor(total_time * 2 / ONE_DAY)
    : 2;
let MAX_LOOPS = limit ** 2;
A one-day trip gets limit = 2, so MAX_LOOPS = 4 — enough for one sunrise and one sunset. A three-day trip gets limit = 6 and MAX_LOOPS = 36. The squaring gives extra headroom for edge cases where more iterations are needed near the poles.

Section structure

After sectionFormatter runs, the journey looks like this:
Departure → [Sunrise₁] → [Sunset₁] → [Sunrise₂] → [Sunset₂] → ... → Arrival
            └──── Section 1 ────┘    └──── Section 2 ────┘
Each section spans from one sunrise to the next sunset. Sections that fall entirely within a single calendar day have their NaS direction computed from the interpolated coordinates at that section’s sunrise and sunset events, not from the overall origin-to-destination bearing — so a route that curves significantly mid-journey will have per-section directions that reflect the local bearing at that point in time.
If computeRouteSections cannot converge — either because MAX_INNER_ITERATIONS is exceeded in the inner loop, or because MAX_LOOPS is exhausted in the outer loop before arrival is reached — it throws an Error with an explicit message. Helios surfaces that message directly to the user via window.alert and aborts rendering rather than displaying a result based on incomplete or incorrect data.

Build docs developers (and LLMs) love