louis-e/arnis · error

select_level_for_cell_size called with empty levels

Error message

select_level_for_cell_size called with empty levels

What it means

This panic is an internal invariant check in select_level_for_cell_size. The function picks the elevation resolution level whose meters-per-pixel best matches the requested cell size, and it cannot do so if the provider was configured with zero resolution levels. The library deliberately panics (with a comment saying 'this is a bug') because an empty level list can only result from a misconfigured provider, not from runtime data conditions.

Source

Thrown at src/elevation/providers/fixed_tile.rs:227

/// whose native pixels are no more than 1.5× finer than the output
/// cell. The factor tolerates a modest amount of *downsampling* from
/// the source (up to 1.5× finer-than-needed) before we give up on it
/// and step to the next coarser level, which avoids pulling dense
/// LiDAR tiles we'd immediately average away. Upsampling the other
/// direction (output finer than source) is unbounded by this rule —
/// if the user asks for 0.4 m cells on a 1 m source, the condition
/// `1.0 * 1.5 ≥ 0.4` holds easily and we use the 1 m level with
/// bilinear fill-in.
///
/// `levels` must be ordered finest-to-coarsest. When no level qualifies
/// the coarsest is returned as a fallback.
pub(super) fn select_level_for_cell_size<R: Resolution + Copy>(
    levels: &[R],
    cell_size_m: f64,
) -> R {
    if levels.is_empty() {
        // Caller must configure at least one level; this is a bug.
        panic!("select_level_for_cell_size called with empty levels");
    }
    if !cell_size_m.is_finite() || cell_size_m <= 0.0 {
        return levels[0];
    }
    for &level in levels {
        if level.meters_per_pixel() * 1.5 >= cell_size_m {
            return level;
        }
    }
    *levels.last().unwrap()
}

/// Approximate physical bbox dimensions in meters. Precise enough for
/// resolution-level selection.
pub(super) fn bbox_dimensions_m(bbox: &LLBBox) -> (f64, f64) {
    let mid_lat = (bbox.min().lat() + bbox.max().lat()) * 0.5;
    let mid_lat_cos = mid_lat.to_radians().cos().abs().max(1e-6);
    let width_deg = bbox.max().lng() - bbox.min().lng();

View on GitHub (pinned to 34048924d9)

Solutions

  1. Configure the provider with at least one resolution level (levels ordered finest-to-coarsest) before calling fetch.
  2. Validate the levels list at provider construction time and fail fast (return an error or assert) instead of letting the panic surface deep inside fetch_fixed_tile_grid.
  3. If levels come from config deserialization, add a validation step after parsing that rejects empty level lists with a clear user-facing message.

Example fix

// before
let provider = FixedTileProvider { levels: vec![] };
provider.fetch_fixed_tile_grid(...); // panics
// after
let provider = FixedTileProvider { levels: vec![Level::res1m(), Level::res10m()] };
assert!(!provider.levels.is_empty(), "provider requires at least one resolution level");
provider.fetch_fixed_tile_grid(...);
Defensive patterns

Strategy: validation

Validate before calling

if provider.levels.is_empty() {
    return Err("elevation provider must be configured with at least one resolution level".into());
}
let grid = provider.fetch_fixed_tile_grid(...)?;

Type guard

fn has_levels<R: Resolution>(levels: &[R]) -> bool { !levels.is_empty() }

Try / catch

// panics are not catchable in normal Rust; validate before calling
let result = std::panic::catch_unwind(|| provider.fetch_fixed_tile_grid(...));
match result { Ok(grid) => grid, Err(_) => fallback_grid() }

Prevention

When it happens

Trigger: Calling fetch_fixed_tile_grid (directly or via a public fetch API) on a fixed-tile elevation provider whose configured levels slice is empty — e.g. a provider constructed with no resolution levels, or levels filtered out during configuration/deserialization.

Common situations: A provider config struct built programmatically and left with an empty levels vec; a config file where all level entries were removed or failed to deserialize into the levels list; a refactor that changed level discovery to return an empty vec without validating it.

Related errors


AI-assisted analysis of louis-e/arnis@34048924d9 (2026-09-03). Data as JSON: /api/errors/113422b56b5b3d90. Report an issue: GitHub.